diff --git a/frontend/js/posters.js b/frontend/js/posters.js
index 944f534..450ec9d 100644
--- a/frontend/js/posters.js
+++ b/frontend/js/posters.js
@@ -200,10 +200,18 @@ App.posters = App.posters || {};
writeIndex();
};
+ // Throws every captured picture away, and with it this session's memory of
+ // which items there was no point asking about. That memory is the whole
+ // reason a dead favorite is cheap to scroll past, so it only goes when the
+ // reader asks for the pictures to be taken again (Settings -> Captured
+ // Thumbnails) -- otherwise an item that failed once would be retried on
+ // every mount for the rest of the session.
App.posters.clear = function() {
readIndex().forEach(drop);
index = [];
writeIndex();
+ failed.clear();
+ rechecks.clear();
};
App.posters.stats = function() {
diff --git a/frontend/js/ui.js b/frontend/js/ui.js
index 03c71a3..fec27ac 100644
--- a/frontend/js/ui.js
+++ b/frontend/js/ui.js
@@ -288,6 +288,11 @@ App.ui = App.ui || {};
if (otherBtn) otherBtn.classList.remove('active');
if (willOpen) {
+ // What the captured-thumbnail note says is a count, and captures
+ // happen while the drawer is shut, so it is read as it opens.
+ if (!isMenu && typeof App.ui.syncPosterCache === 'function') {
+ App.ui.syncPosterCache();
+ }
targetDrawer.classList.add('open');
if (targetBtn) targetBtn.classList.add('active');
overlay.classList.add('open');
@@ -1696,6 +1701,56 @@ App.ui = App.ui || {};
});
};
+ // Settings -> Captured Thumbnails. A favorite whose listed picture is gone
+ // gets a frame out of the video instead, kept for good so no later session
+ // pays for it again (see App.posters). Kept for good is also the problem:
+ // a picture taken from the wrong moment, or from a version of this that
+ // kept frames with nothing in them, has no other way out. So the reader
+ // can throw the lot away and have them taken again.
+ const posterCacheNote = function() {
+ if (!App.posters || typeof App.posters.stats !== 'function') return '';
+ const stats = App.posters.stats();
+ if (!stats.count) {
+ return 'No pictures taken from videos yet. A favorite gets one when ' +
+ 'its own thumbnail is gone for good.';
+ }
+ const plural = stats.count === 1 ? 'picture' : 'pictures';
+ const kb = Math.max(1, Math.round(stats.bytes / 1024));
+ return `${stats.count} ${plural} kept, ${kb} KB. Throwing them away has ` +
+ 'each one taken again from the video the next time its card is shown.';
+ };
+
+ App.ui.syncPosterCache = function() {
+ const status = document.getElementById('clear-posters-status');
+ if (status) status.textContent = posterCacheNote();
+ };
+
+ App.ui.bindPosterCacheControls = function() {
+ const button = document.getElementById('clear-posters-btn');
+ if (!button) return;
+ App.ui.syncPosterCache();
+ button.addEventListener('click', () => {
+ const before = App.posters.stats().count;
+ App.posters.clear();
+ // The cards on screen are still showing what was just thrown away,
+ // so they go back to the start of the thumbnail ladder -- which is
+ // where a fresh capture comes from.
+ if (typeof App.videos.refreshThumbnails === 'function') {
+ App.videos.refreshThumbnails();
+ }
+ if (App.favorites && typeof App.favorites.renderBar === 'function') {
+ App.favorites.renderBar();
+ }
+ const status = document.getElementById('clear-posters-status');
+ if (status) {
+ status.textContent = before
+ ? `Threw away ${before} ${before === 1 ? 'picture' : 'pictures'}. ` +
+ 'They are being taken again.'
+ : posterCacheNote();
+ }
+ });
+ };
+
// Favorites bar header: the sort order (which applies to the bar and the
// favorites grid alike) and the toggle into that grid.
App.ui.bindFavoritesControls = function() {
@@ -1721,6 +1776,7 @@ App.ui = App.ui || {};
App.ui.bindGlobalHandlers = function() {
App.ui.bindBackupImport();
+ App.ui.bindPosterCacheControls();
App.ui.bindFavoritesControls();
window.toggleDrawer = App.ui.toggleDrawer;
diff --git a/frontend/js/videos.js b/frontend/js/videos.js
index fc62318..06d9830 100644
--- a/frontend/js/videos.js
+++ b/frontend/js/videos.js
@@ -542,6 +542,20 @@ App.videos = App.videos || {};
}
};
+ // Puts every thumbnail on the page back to the start of the ladder. For the
+ // settings action that throws the captured pictures away (see
+ // App.posters.clear): a card that is already mounted would otherwise keep
+ // showing the picture that was just discarded until something scrolled it
+ // out of the window and back.
+ App.videos.refreshThumbnails = function() {
+ document.querySelectorAll('img[data-thumb-token]').forEach((img) => {
+ const video = img._thumbVideo;
+ if (!video) return;
+ App.videos.attachThumbnail(img, video.thumb || '',
+ img.dataset.thumbPage || App.videos.sourcePageUrl(video), video);
+ });
+ };
+
// Points `img` at `url` by whichever route is known to work for its host,
// racing the two the first time that host is seen. `page` is optional: the
// item's own page, asked for a replacement if the picture turns out to be
diff --git a/tests/smoke_poster_capture.py b/tests/smoke_poster_capture.py
index 75d6a27..26e20ed 100644
--- a/tests/smoke_poster_capture.py
+++ b/tests/smoke_poster_capture.py
@@ -278,6 +278,26 @@ def main():
c.ok("the bar card is the captured frame as well",
bar is not None and bar.startswith("data:image/jpeg"), str(bar))
+ print("\nand the reader can have them taken again")
+ streamed.clear()
+ page.click(".settings-toggle")
+ c.ok("the setting says what is being kept",
+ "1 picture kept" in page.inner_text("#clear-posters-status"),
+ page.inner_text("#clear-posters-status"))
+ page.click("#clear-posters-btn")
+ c.ok("nothing is kept any more", page.evaluate(CACHE) == [], str(page.evaluate(CACHE)))
+ c.ok("and it says what it threw away",
+ "Threw away 1 picture" in page.inner_text("#clear-posters-status"),
+ page.inner_text("#clear-posters-status"))
+ page.wait_for_function(CAPTURED, arg="vid-0", timeout=40000)
+ shot = page.evaluate(SHOT, "vid-0")
+ c.ok("the card has a frame out of the video again",
+ shot and shot["src"].startswith("data:image/jpeg"), str(shot))
+ c.ok("which was fetched for it, this being the point of the setting",
+ any("/api/stream" in url for url in streamed), str(streamed[:2]))
+ c.ok("and kept again, as one picture",
+ len(page.evaluate(CACHE)) == 1, str(page.evaluate(CACHE)))
+
c.ok("nothing threw along the way", not crashes, str(crashes[:2]))
browser.close()