Keep a whole search, and say what is new in it
A search here is more than the words in the box: it is the source, the channel, every filter that channel offers, and the query together. The bookmark in the search bar keeps all of it, the one in the header opens the list, and tapping a row puts the whole thing back. Each saved search remembers the ids of the newest videos it showed the last time it was opened. Once a day -- at startup, when the tab is looked at again, and on a slow heartbeat for tabs left open -- the client asks each one's first page again and counts how many of those videos it has never shown. That count is the badge, on the row and summed in the header, and it stops at 9+. Opening a saved search is viewing it, so the count goes out and the mark moves to what is on screen now. Nothing new leaves the device: the check is the same listing request the grid would have made, and everything else is localStorage. Also: handleSearch's "put this in the box without searching for it" half is now App.videos.setSearchBox, which is what applying a saved search needs. tests/smoke_saved_searches.py covers it end to end against a fixture listing whose ids the test shifts to mean "new uploads": 46 checks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
This commit is contained in:
@@ -139,7 +139,8 @@ body.theme-light .top-bar {
|
||||
|
||||
.search-container input {
|
||||
width: 100%;
|
||||
padding: 10px 44px 10px 16px;
|
||||
/* Room on the right for the clear button and the bookmark beside it. */
|
||||
padding: 10px 78px 10px 16px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
@@ -162,7 +163,7 @@ body.theme-light .top-bar {
|
||||
.search-clear-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 12px;
|
||||
right: 44px;
|
||||
transform: translateY(-50%);
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
@@ -3092,3 +3093,363 @@ body.theme-light .video-card img:not(.is-loaded) {
|
||||
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||||
.privacy-veil { background: var(--bg-primary); }
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
Saved searches (see savedSearches.js + App.ui.savedSearches)
|
||||
|
||||
One hairline per row and no boxes inside boxes: the list is read as a
|
||||
document, with the brass reserved for the one thing that is news -- how
|
||||
many videos a saved search has picked up since it was last opened.
|
||||
===================================================================== */
|
||||
|
||||
/* Line icons drawn in place rather than fetched: these sit inside buttons
|
||||
that change colour with state, and a tinted <img> cannot follow. */
|
||||
.icon-line {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: block;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.7;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* --- The bookmark in the search bar ---------------------------------- */
|
||||
.search-save-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.2s ease, background 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.search-save-btn:hover { color: var(--text-primary); background: var(--bg-tertiary); }
|
||||
.search-save-btn:active { transform: translateY(-50%) scale(0.94); }
|
||||
.search-save-btn:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* Saved: the glyph fills in, which is the whole state in one look. */
|
||||
.search-save-btn.is-saved { color: var(--accent); }
|
||||
.search-save-btn.is-saved .icon-line { fill: currentColor; stroke: currentColor; }
|
||||
|
||||
@keyframes saved-flash {
|
||||
0% { transform: translateY(-50%) scale(1); }
|
||||
35% { transform: translateY(-50%) scale(1.22); }
|
||||
100% { transform: translateY(-50%) scale(1); }
|
||||
}
|
||||
|
||||
.search-save-btn.is-just-saved { animation: saved-flash 0.42s cubic-bezier(0.2, 0.7, 0.2, 1); }
|
||||
|
||||
/* --- The header button and its count --------------------------------- */
|
||||
.saved-toggle { position: relative; }
|
||||
|
||||
.saved-badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 5px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
color: var(--bg-primary);
|
||||
font-size: 9.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 0 2px rgba(20, 17, 13, 0.9);
|
||||
}
|
||||
|
||||
body.theme-light .saved-badge { box-shadow: 0 0 0 2px rgba(246, 241, 231, 0.95); }
|
||||
|
||||
/* --- The list ---------------------------------------------------------- */
|
||||
.saved-panel {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 4200;
|
||||
display: none;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 8vh 12px 12px 12px;
|
||||
background: rgba(10, 8, 4, 0.55);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.saved-panel.open { display: flex; }
|
||||
|
||||
.saved-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(620px, 100%);
|
||||
max-height: min(78vh, 760px);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 30px 70px var(--shadow);
|
||||
overflow: hidden;
|
||||
animation: card-rise 0.2s ease both;
|
||||
}
|
||||
|
||||
.saved-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 20px 20px 14px 20px;
|
||||
}
|
||||
|
||||
.saved-head h3 {
|
||||
font-family: var(--font-display);
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.saved-head-note {
|
||||
margin-top: 4px;
|
||||
max-width: 44ch;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.saved-head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.saved-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 10px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
@keyframes saved-row-in {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.saved-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
animation: saved-row-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
animation-delay: calc(var(--i, 0) * 40ms);
|
||||
}
|
||||
|
||||
.saved-list .saved-row:last-child { border-bottom: none; }
|
||||
|
||||
.saved-row-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 10px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: none;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease;
|
||||
}
|
||||
|
||||
.saved-row-main:hover { background: var(--bg-tertiary); }
|
||||
.saved-row-main:disabled { cursor: default; }
|
||||
|
||||
/* The count. Brass and filled when there is something new; a hairline ring
|
||||
and a single dot when there isn't, so an up-to-date row reads as quiet
|
||||
rather than as a zero. */
|
||||
.saved-count {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-hover);
|
||||
background: rgba(201, 165, 103, 0.16);
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.saved-count.is-quiet {
|
||||
border-color: var(--border);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.saved-row.is-unreachable .saved-count { border-style: dashed; }
|
||||
|
||||
.saved-row-text {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.saved-row-name {
|
||||
font-family: var(--font-display);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.saved-row-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.saved-row-stamp {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.saved-row-tools {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
padding-right: 6px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
|
||||
.saved-row:hover .saved-row-tools,
|
||||
.saved-row:focus-within .saved-row-tools { opacity: 1; }
|
||||
|
||||
.saved-tool {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.saved-tool .icon-line { width: 18px; height: 18px; }
|
||||
.saved-tool:hover { color: var(--accent); background: var(--bg-tertiary); border-color: var(--border); }
|
||||
|
||||
.saved-rename {
|
||||
width: 100%;
|
||||
padding: 5px 9px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-hover);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-display);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.saved-rename:focus { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); }
|
||||
|
||||
/* Removing asks first, in the row itself -- no dialog on top of a dialog. */
|
||||
.saved-confirm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.saved-confirm-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.saved-confirm-btn {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.saved-confirm-btn:hover { background: var(--bg-tertiary); border-color: var(--border-hover); }
|
||||
|
||||
.saved-confirm-btn.is-danger { color: #e2a2a2; }
|
||||
body.theme-light .saved-confirm-btn.is-danger { color: #9f2f2d; }
|
||||
.saved-confirm-btn.is-danger:hover { border-color: currentColor; }
|
||||
|
||||
.saved-row.is-confirming .saved-row-main { opacity: 0.5; }
|
||||
|
||||
.saved-empty {
|
||||
padding: 4px 20px 20px 20px;
|
||||
max-width: 52ch;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.saved-foot {
|
||||
padding: 14px 20px 18px 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.saved-add { width: 100%; }
|
||||
.saved-add:disabled { opacity: 0.55; cursor: default; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.saved-panel { padding: 0; }
|
||||
|
||||
.saved-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Nothing to hover with, so the row's tools are simply there. */
|
||||
.saved-row-tools { opacity: 1; }
|
||||
.saved-row-stamp { white-space: normal; }
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.saved-row-tools { opacity: 1; }
|
||||
.search-save-btn { width: 34px; height: 34px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user