The favorites bar carries the "Browse all" button and the sort control, so switching the bar off in settings hid the way in to both. The command palette now offers "Browse favorites" (or "Back to videos") and each sort order, and opening the grid re-renders the bar so its header -- the way back out -- is mounted even when settings say hidden. Tests (scratchpad): a new palette test that starts with the bar switched off, opens the grid through the palette, re-sorts through it, and returns to the listing. It caught the second half of this: opening from the palette did not re-render the bar, leaving no visible way out. Also fixes the favorites playback test, which had been wedging headless Chrome all session. It was reloading by navigating to the URL already loaded; the first evaluate after that is answered by the outgoing execution context and every one after it hangs forever. It now does its second visit in a fresh tab -- localStorage is shared per origin, so it models "next day" the same way -- and asserts what it had only been printing. It also runs hermetically now (no favorites left over from another test, CDN icons and fonts blocked) and no longer runs its whole body on import, which is what made it hijack a debugging session earlier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
111 lines
4.3 KiB
JavaScript
111 lines
4.3 KiB
JavaScript
window.App = window.App || {};
|
|
App.favoritesView = App.favoritesView || {};
|
|
|
|
// Browsing favorites as a full grid, the same way the channel listing is
|
|
// browsed: same cards, same virtualized masonry, same infinite scroll, same
|
|
// reels mode. The only difference is where the pages come from -- localStorage
|
|
// instead of the server -- so App.videos.loadVideos routes here while this view
|
|
// is active and every page hands its slice to App.videos.renderVideos.
|
|
(function() {
|
|
const state = App.state;
|
|
|
|
// A page of a local list can be bigger than a page from the server: there's
|
|
// no request behind it, only the cost of building cards, which the
|
|
// virtualizer already keeps to what's on screen.
|
|
const PAGE_SIZE = 24;
|
|
|
|
const view = {
|
|
active: false,
|
|
queue: [], // the sorted favorites still to be handed to the grid
|
|
offset: 0
|
|
};
|
|
|
|
// A favorite as the grid expects a video: `id` has to be unique per card
|
|
// (the virtualizer and renderedVideoIds key on it), and an imported
|
|
// favorite has no server id -- its key, which is the URL, stands in.
|
|
const toVideo = function(entry) {
|
|
return Object.assign({}, entry, { id: entry.key, tags: [] });
|
|
};
|
|
|
|
App.favoritesView.isActive = function() {
|
|
return view.active;
|
|
};
|
|
|
|
App.favoritesView.loadNext = function() {
|
|
if (!view.active) return false;
|
|
const slice = view.queue.slice(view.offset, view.offset + PAGE_SIZE);
|
|
view.offset += slice.length;
|
|
state.hasNextPage = view.offset < view.queue.length;
|
|
if (!slice.length) {
|
|
App.videos.updateLoadMoreState();
|
|
return false;
|
|
}
|
|
App.videos.renderVideos({ items: slice.map(toVideo) });
|
|
App.videos.updateLoadMoreState();
|
|
return true;
|
|
};
|
|
|
|
// Starts (or restarts, after a sort change) the favorites grid.
|
|
App.favoritesView.open = function(options) {
|
|
const sort = (options && options.sort) || App.favorites.getSort();
|
|
const favorites = App.favorites.sorted(sort);
|
|
if (!favorites.length) {
|
|
App.ui.showError('No favorites yet. Tap the heart on a video to save one.');
|
|
return false;
|
|
}
|
|
App.videos.resetGrid();
|
|
view.active = true;
|
|
view.queue = favorites;
|
|
view.offset = 0;
|
|
state.hasNextPage = true;
|
|
document.body.classList.add('favorites-view-open');
|
|
// Re-render the bar so it re-decides whether to be mounted: hidden in
|
|
// settings or not, its header has to be on screen now, since that's
|
|
// where the way back out lives (the palette can open this view too).
|
|
App.favorites.renderBar();
|
|
App.favoritesView.syncControls();
|
|
App.favoritesView.loadNext();
|
|
window.scrollTo({ top: 0, behavior: 'auto' });
|
|
return true;
|
|
};
|
|
|
|
App.favoritesView.close = function(options) {
|
|
if (!view.active) return;
|
|
view.active = false;
|
|
view.queue = [];
|
|
view.offset = 0;
|
|
document.body.classList.remove('favorites-view-open');
|
|
// Back to whatever the settings say, and with its cards built again.
|
|
App.favorites.renderBar();
|
|
App.favoritesView.syncControls();
|
|
// Back to the channel listing, unless the caller is about to load
|
|
// something itself (a search, a channel switch).
|
|
if (!(options && options.silent)) App.videos.resetAndReload();
|
|
};
|
|
|
|
App.favoritesView.toggle = function() {
|
|
if (view.active) App.favoritesView.close();
|
|
else App.favoritesView.open();
|
|
};
|
|
|
|
// Re-pages the grid under a new order, and re-renders the bar so both show
|
|
// favorites the same way round.
|
|
App.favoritesView.applySort = function(sort) {
|
|
App.favorites.setSort(sort);
|
|
App.favorites.renderBar();
|
|
if (view.active) App.favoritesView.open({ sort });
|
|
};
|
|
|
|
App.favoritesView.syncControls = function() {
|
|
const button = document.getElementById('favorites-browse-btn');
|
|
if (button) {
|
|
button.textContent = view.active ? 'Back to videos' : 'Browse all';
|
|
button.setAttribute('aria-pressed', view.active ? 'true' : 'false');
|
|
}
|
|
const select = document.getElementById('favorites-sort');
|
|
if (select && select.value !== App.favorites.getSort()) {
|
|
select.value = App.favorites.getSort();
|
|
}
|
|
};
|
|
})();
|