Compare commits
1 Commits
thumbnail-
...
1dbac33359
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dbac33359 |
@@ -4,6 +4,12 @@ App.favorites = App.favorites || {};
|
||||
(function() {
|
||||
const { FAVORITES_KEY, FAVORITES_VISIBILITY_KEY } = App.constants;
|
||||
|
||||
// Both identities of every favorite, in one pass, held until the list
|
||||
// changes (see setAll). This is read once per card built, and re-parsing the
|
||||
// whole favorites list out of localStorage that often is what makes a long
|
||||
// list felt on a scrolling grid.
|
||||
let identityCache = null;
|
||||
|
||||
// Favorites storage helpers.
|
||||
App.favorites.getAll = function() {
|
||||
try {
|
||||
@@ -87,6 +93,7 @@ App.favorites = App.favorites || {};
|
||||
};
|
||||
|
||||
App.favorites.setAll = function(items) {
|
||||
identityCache = null;
|
||||
localStorage.setItem(FAVORITES_KEY, JSON.stringify(items));
|
||||
};
|
||||
|
||||
@@ -181,19 +188,45 @@ App.favorites = App.favorites || {};
|
||||
return { added, skipped, total: favorites.length };
|
||||
};
|
||||
|
||||
const identities = function() {
|
||||
if (!identityCache) {
|
||||
const keys = new Set();
|
||||
const urls = new Set();
|
||||
// getAll may rewrite the list (the repair pass), which clears the
|
||||
// cache -- so build it only after that has run.
|
||||
const items = App.favorites.getAll();
|
||||
items.forEach((item) => {
|
||||
if (!item) return;
|
||||
if (item.key) keys.add(item.key);
|
||||
const urlKey = App.favorites.urlKey(item.url);
|
||||
if (urlKey) urls.add(urlKey);
|
||||
});
|
||||
identityCache = { keys: keys, urls: urls };
|
||||
}
|
||||
return identityCache;
|
||||
};
|
||||
|
||||
App.favorites.getSet = function() {
|
||||
return new Set(App.favorites.getAll().map((item) => item.key));
|
||||
return identities().keys;
|
||||
};
|
||||
|
||||
// Same set, addressed by URL. Imported favorites are keyed by URL rather
|
||||
// than by a server id, so a listing card can only recognise one this way.
|
||||
App.favorites.getUrlSet = function() {
|
||||
const urls = new Set();
|
||||
App.favorites.getAll().forEach((item) => {
|
||||
const urlKey = item && App.favorites.urlKey(item.url);
|
||||
if (urlKey) urls.add(urlKey);
|
||||
});
|
||||
return urls;
|
||||
return identities().urls;
|
||||
};
|
||||
|
||||
// Is this video a favorite, under either identity? A video reaches us from
|
||||
// the listing keyed by the server's id and from a Hot Tub backup keyed by
|
||||
// its URL, and the same video must light up its heart whichever way the
|
||||
// copy on disk got there.
|
||||
App.favorites.has = function(video) {
|
||||
const index = identities();
|
||||
const key = App.favorites.getKey(video);
|
||||
if (key && index.keys.has(key)) return true;
|
||||
const meta = (video && video.meta) || video || {};
|
||||
const urlKey = App.favorites.urlKey(video && (video.url || meta.url));
|
||||
return !!(urlKey && index.urls.has(urlKey));
|
||||
};
|
||||
|
||||
// Is this video already a favorite, whichever way it got saved? Checked by
|
||||
@@ -209,6 +242,58 @@ App.favorites = App.favorites || {};
|
||||
return favorites.findIndex((item) => item && App.favorites.urlKey(item.url) === urlKey);
|
||||
};
|
||||
|
||||
// Everything an entry carries, so a change can be told from a no-op.
|
||||
const ENTRY_FIELDS = ['key', 'id', 'url', 'title', 'thumb', 'channel',
|
||||
'uploader', 'duration', 'isLive', 'favoriteDate'];
|
||||
|
||||
const sameEntry = function(a, b) {
|
||||
return ENTRY_FIELDS.every((field) =>
|
||||
String(a[field] === undefined || a[field] === null ? '' : a[field]) ===
|
||||
String(b[field] === undefined || b[field] === null ? '' : b[field]));
|
||||
};
|
||||
|
||||
// Brings stored favorites up to date from a page of listing videos.
|
||||
//
|
||||
// The same video reaches this client under two identities: saved from a
|
||||
// card it carries the server's id, imported from a Hot Tub backup it
|
||||
// carries only its URL. Matching on the URL is what recognises them as one
|
||||
// thing -- and once they are matched, the listing's copy is the better one.
|
||||
// It has the id every card keys on, and a thumbnail URL that hasn't been
|
||||
// sitting in localStorage since whenever the backup was taken. So the
|
||||
// stored entry is replaced by it, keeping only the date it was first saved:
|
||||
// that is the one fact the listing doesn't know and the favorites sort
|
||||
// depends on.
|
||||
//
|
||||
// Writes only when something actually differs, so the steady state of
|
||||
// scrolling a listing full of favorites is no writes at all.
|
||||
App.favorites.reconcile = function(videos) {
|
||||
const items = Array.isArray(videos) ? videos : [];
|
||||
if (!items.length) return 0;
|
||||
const favorites = App.favorites.getAll();
|
||||
if (!favorites.length) return 0;
|
||||
|
||||
let changed = 0;
|
||||
items.forEach((video) => {
|
||||
if (!video || !video.url) return;
|
||||
const index = App.favorites.indexOfEntry(favorites, video);
|
||||
if (index < 0) return;
|
||||
const existing = favorites[index];
|
||||
const upgraded = App.favorites.normalize(Object.assign({}, video, {
|
||||
favoriteDate: existing.favoriteDate
|
||||
}));
|
||||
if (!upgraded || sameEntry(existing, upgraded)) return;
|
||||
favorites[index] = upgraded;
|
||||
changed++;
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
App.favorites.setAll(favorites);
|
||||
App.favorites.renderBar();
|
||||
App.favorites.syncButtons();
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
|
||||
App.favorites.isVisible = function() {
|
||||
return localStorage.getItem(FAVORITES_VISIBILITY_KEY) !== 'false';
|
||||
};
|
||||
|
||||
@@ -469,7 +469,7 @@ App.feed = App.feed || {};
|
||||
|
||||
const favBtn = slide.querySelector('.feed-fav-btn');
|
||||
if (favBtn && App.favorites) {
|
||||
App.favorites.setButtonState(favBtn, App.favorites.indexOfEntry(App.favorites.getAll(), v) >= 0);
|
||||
App.favorites.setButtonState(favBtn, App.favorites.has(v));
|
||||
favBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
App.favorites.toggle(v);
|
||||
|
||||
@@ -295,7 +295,7 @@ App.player = App.player || {};
|
||||
const key = App.favorites.getKey(videoData);
|
||||
if (!key) { btn.hidden = true; return; }
|
||||
btn.dataset.favKey = key;
|
||||
App.favorites.setButtonState(btn, App.favorites.getSet().has(key));
|
||||
App.favorites.setButtonState(btn, App.favorites.has(videoData));
|
||||
const onClick = (event) => {
|
||||
event.stopPropagation();
|
||||
App.favorites.toggle(videoData);
|
||||
|
||||
@@ -714,7 +714,6 @@ App.videos = App.videos || {};
|
||||
// mounting so the virtualizer can create a card the moment it needs to be
|
||||
// on screen and throw it away once it scrolls out of the window.
|
||||
App.videos.buildCard = function(v, options) {
|
||||
const favoritesSet = App.favorites.getSet();
|
||||
const card = document.createElement('div');
|
||||
card.className = 'video-card';
|
||||
card.dataset.videoId = v.id;
|
||||
@@ -755,7 +754,9 @@ App.videos = App.videos || {};
|
||||
}
|
||||
const favoriteBtn = card.querySelector('.favorite-btn');
|
||||
if (favoriteBtn && favoriteKey) {
|
||||
App.favorites.setButtonState(favoriteBtn, favoritesSet.has(favoriteKey));
|
||||
// By either identity: a favorite imported from a backup is keyed
|
||||
// by its URL, not by the id this card carries.
|
||||
App.favorites.setButtonState(favoriteBtn, App.favorites.has(v));
|
||||
favoriteBtn.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
App.favorites.toggle(v);
|
||||
@@ -847,6 +848,15 @@ App.videos = App.videos || {};
|
||||
App.virtualGrid.ensureInit();
|
||||
|
||||
const items = videos && Array.isArray(videos.items) ? videos.items : [];
|
||||
// The server's copy of a video it already has saved is the better one,
|
||||
// so bring the favorites up to date from it. Skipped while the favorites
|
||||
// grid is the thing being rendered -- those items *are* the favorites,
|
||||
// reshaped for the grid, and reconciling them against themselves would
|
||||
// only write back what they came from.
|
||||
if (App.favorites && typeof App.favorites.reconcile === 'function' &&
|
||||
!(App.favoritesView && App.favoritesView.isActive())) {
|
||||
App.favorites.reconcile(items);
|
||||
}
|
||||
const startLen = state.loadedVideos.length;
|
||||
items.forEach((v) => {
|
||||
if (state.renderedVideoIds.has(v.id)) return;
|
||||
|
||||
Reference in New Issue
Block a user