Files
jacuzzi/frontend/js/main.js
Simon bc7c0047b7 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
2026-10-17 05:59:19 +00:00

66 lines
2.4 KiB
JavaScript

window.App = window.App || {};
(function() {
// App bootstrap: initialize storage, render UI, and load the first page.
async function initApp() {
// Armed before anything is on screen: there is nothing to show behind
// a blur yet, but the window can lose focus during startup too.
App.privacy.start();
await App.storage.ensureDefaults();
App.ui.applyTheme();
App.ui.applyPreferredQuality();
App.ui.applyFeedEndBehavior();
App.ui.applyDensity();
// Set the text-size CSS variable before the first pack so initial card
// heights are measured at the user's chosen size.
document.documentElement.style.setProperty('--card-font-scale', App.storage.getFontScale());
App.ui.renderMenu();
App.favorites.renderBar();
App.ui.bindGlobalHandlers();
App.videos.observeSentinel();
const loadMoreBtn = document.getElementById('load-more-btn');
if (loadMoreBtn) {
loadMoreBtn.onclick = () => {
App.videos.loadVideos({ force: true });
};
}
const errorToastClose = document.getElementById('error-toast-close');
if (errorToastClose) {
errorToastClose.onclick = () => {
const toast = document.getElementById('error-toast');
if (toast) toast.classList.remove('show');
};
}
window.addEventListener('resize', () => {
App.videos.ensureViewportFilled();
});
await App.videos.loadVideos();
App.favorites.syncButtons();
// The grid is showing something now. If that something is one of the
// saved searches, the reader is looking at it -- so it stops being
// "new" from here, and the once-a-day check starts running.
App.savedSearches.syncViewed();
App.savedSearches.start();
// The UI above is rendered entirely from the last known status cached in
// localStorage, so startup never blocks on (or breaks because of) a slow
// or failing status endpoint. Now fetch fresh status in the background and
// reconcile the UI with whatever comes back.
App.storage.refreshServerStatusInBackground();
// Watch for frontend deploys and seamlessly reload/hot-swap changed assets.
if (App.version && App.version.start) {
App.version.start();
}
}
initApp();
})();