Fix expiring favorites, empty quality menus, rotation scroll jumps
Favorites persisted the *resolved* stream metadata (resolveAndProbe mutates video.meta with yt-dlp's CDN format URLs), so a favorite opened the next day replayed a dead link. They now store only the page URL and identifying fields, and ignore any stale meta left in localStorage -- playback, download and info re-resolve through the backend, which resolves a page URL live in /api/stream. That left the quality switcher empty for anything not yet resolved (favorites, cards clicked before their hover-resolve landed, feed slides), so App.videos.ensureFormats now resolves formats once per session -- cached by video id rather than per object, so any object describing the same video gets them -- and both the player and the reels feed rebuild their format menu when they arrive. Playback isn't blocked: it already starts from the page URL via the proxy. Rotating a phone also jumped the grid to a completely different place: the anchor was read inside the resize handler (by which point the browser has already moved the scroll) and asserted once, and every re-pack discarded known card heights for the 16:9 placeholder estimate. The virtualizer now tracks the anchor on every scroll pass, re-asserts it across a short settling window (ending early on a real gesture), and remembers each thumbnail's true aspect ratio so a re-pack places cards at their real heights. Verified in headless Chrome across a portrait/landscape/portrait cycle: visible videos 37-39 -> 36-40 -> 36-38, against 37-39 -> 34-37 -> 29-30 before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
This commit is contained in:
@@ -273,7 +273,7 @@ App.feed = App.feed || {};
|
||||
|
||||
const formatBtn = slide.querySelector('.feed-format-btn');
|
||||
const formatMenu = slide.querySelector('.feed-format-menu');
|
||||
cleanups.push(App.customPlayer.bindFormatMenu(formatBtn, formatMenu, videoData, (fmt) => {
|
||||
const onFormatPick = (fmt) => {
|
||||
slide._formatOverride = fmt;
|
||||
const t = video.currentTime;
|
||||
if (isFinite(t) && t > 0) resumeTimes.set(videoData.id, t);
|
||||
@@ -292,7 +292,23 @@ App.feed = App.feed || {};
|
||||
video.load();
|
||||
slide.classList.remove('is-loaded');
|
||||
loadSlideSource(slide, videoData, true);
|
||||
}));
|
||||
};
|
||||
const bindFormats = () => App.customPlayer.bindFormatMenu(formatBtn, formatMenu, videoData, onFormatPick);
|
||||
let destroyFormatMenu = bindFormats();
|
||||
cleanups.push(() => destroyFormatMenu());
|
||||
// A slide can go active before its formats have been resolved (feed items
|
||||
// carry only a page URL until then), which would leave the quality menu
|
||||
// empty. Playback already runs from that page URL through the proxy, so
|
||||
// resolve in the background and rebuild the menu once the real qualities
|
||||
// land -- same as the standalone player does.
|
||||
if (App.videos && typeof App.videos.ensureFormats === 'function') {
|
||||
App.videos.ensureFormats(videoData).then((meta) => {
|
||||
// Bail if the slide was torn down (or rebound) in the meantime.
|
||||
if (!meta || slide._sharedControlCleanups !== cleanups) return;
|
||||
destroyFormatMenu();
|
||||
destroyFormatMenu = bindFormats();
|
||||
});
|
||||
}
|
||||
|
||||
cleanups.push(App.customPlayer.attachGestures(slide, {
|
||||
onSingleTap: wakeHud,
|
||||
|
||||
Reference in New Issue
Block a user