Compare commits
1 Commits
thumbnail-
...
1dbac33359
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dbac33359 |
@@ -4,6 +4,12 @@ App.favorites = App.favorites || {};
|
|||||||
(function() {
|
(function() {
|
||||||
const { FAVORITES_KEY, FAVORITES_VISIBILITY_KEY } = App.constants;
|
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.
|
// Favorites storage helpers.
|
||||||
App.favorites.getAll = function() {
|
App.favorites.getAll = function() {
|
||||||
try {
|
try {
|
||||||
@@ -87,6 +93,7 @@ App.favorites = App.favorites || {};
|
|||||||
};
|
};
|
||||||
|
|
||||||
App.favorites.setAll = function(items) {
|
App.favorites.setAll = function(items) {
|
||||||
|
identityCache = null;
|
||||||
localStorage.setItem(FAVORITES_KEY, JSON.stringify(items));
|
localStorage.setItem(FAVORITES_KEY, JSON.stringify(items));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,19 +188,45 @@ App.favorites = App.favorites || {};
|
|||||||
return { added, skipped, total: favorites.length };
|
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() {
|
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
|
// 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.
|
// than by a server id, so a listing card can only recognise one this way.
|
||||||
App.favorites.getUrlSet = function() {
|
App.favorites.getUrlSet = function() {
|
||||||
const urls = new Set();
|
return identities().urls;
|
||||||
App.favorites.getAll().forEach((item) => {
|
};
|
||||||
const urlKey = item && App.favorites.urlKey(item.url);
|
|
||||||
if (urlKey) urls.add(urlKey);
|
// 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
|
||||||
return urls;
|
// 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
|
// 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);
|
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() {
|
App.favorites.isVisible = function() {
|
||||||
return localStorage.getItem(FAVORITES_VISIBILITY_KEY) !== 'false';
|
return localStorage.getItem(FAVORITES_VISIBILITY_KEY) !== 'false';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ App.feed = App.feed || {};
|
|||||||
|
|
||||||
const favBtn = slide.querySelector('.feed-fav-btn');
|
const favBtn = slide.querySelector('.feed-fav-btn');
|
||||||
if (favBtn && App.favorites) {
|
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) => {
|
favBtn.addEventListener('click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
App.favorites.toggle(v);
|
App.favorites.toggle(v);
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ App.player = App.player || {};
|
|||||||
const key = App.favorites.getKey(videoData);
|
const key = App.favorites.getKey(videoData);
|
||||||
if (!key) { btn.hidden = true; return; }
|
if (!key) { btn.hidden = true; return; }
|
||||||
btn.dataset.favKey = key;
|
btn.dataset.favKey = key;
|
||||||
App.favorites.setButtonState(btn, App.favorites.getSet().has(key));
|
App.favorites.setButtonState(btn, App.favorites.has(videoData));
|
||||||
const onClick = (event) => {
|
const onClick = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
App.favorites.toggle(videoData);
|
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
|
// 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.
|
// on screen and throw it away once it scrolls out of the window.
|
||||||
App.videos.buildCard = function(v, options) {
|
App.videos.buildCard = function(v, options) {
|
||||||
const favoritesSet = App.favorites.getSet();
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'video-card';
|
card.className = 'video-card';
|
||||||
card.dataset.videoId = v.id;
|
card.dataset.videoId = v.id;
|
||||||
@@ -755,7 +754,9 @@ App.videos = App.videos || {};
|
|||||||
}
|
}
|
||||||
const favoriteBtn = card.querySelector('.favorite-btn');
|
const favoriteBtn = card.querySelector('.favorite-btn');
|
||||||
if (favoriteBtn && favoriteKey) {
|
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) => {
|
favoriteBtn.onclick = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
App.favorites.toggle(v);
|
App.favorites.toggle(v);
|
||||||
@@ -847,6 +848,15 @@ App.videos = App.videos || {};
|
|||||||
App.virtualGrid.ensureInit();
|
App.virtualGrid.ensureInit();
|
||||||
|
|
||||||
const items = videos && Array.isArray(videos.items) ? videos.items : [];
|
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;
|
const startLen = state.loadedVideos.length;
|
||||||
items.forEach((v) => {
|
items.forEach((v) => {
|
||||||
if (state.renderedVideoIds.has(v.id)) return;
|
if (state.renderedVideoIds.has(v.id)) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user