Favorites now carry `favoriteDate`. Ones saved before this had no way of knowing when they were saved, so they are all stamped with the moment the client first reads them -- they sort together as one batch, at the point favorites learned to keep dates. An import brings the date the other client recorded instead, so a restored library keeps its history. The bar used to build a card per favorite, which an import of several hundred made an expensive way to open the app. It now renders a screenful and appends more as the strip is scrolled. "Browse all" turns the whole grid into favorites: the same cards, the same virtualized masonry, the same infinite scroll and reels mode as a channel listing -- App.videos.loadVideos simply pages out of localStorage instead of the server while that view is open. Sort applies to the bar and the grid together: recently added (default), oldest, title, longest, shortest, and a shuffle for rediscovering a long list. Tests (scratchpad): dates backfilled onto undated favorites, the bar paging as it scrolls rather than building every card, the grid paging to the full list with zero server calls, each sort order reordering it, and the way back to the channel listing. Also measured: 515 imported favorites load with the page responsive and 24 bar cards built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
105 lines
4.0 KiB
JavaScript
105 lines
4.0 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');
|
|
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');
|
|
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();
|
|
}
|
|
};
|
|
})();
|