release: 5.0.1 — fix VOD hover preview + merge-group progress bar
Bug 1 — VOD-Hover Storyboard zeigte am unteren Rand einen statischen Streifen vom Original-Thumbnail (Subpixel-Mismatch + Aspect-Ratio-Konflikt). Fix: Overlay haengt jetzt an .vod-thumb-wrap statt .vod-card, mit explizitem width+height aus dem Thumbnail-BoundingRect — keine CSS-aspect-ratio-Interferenz mehr. Bug 2 — Merge-Group Download zeigte einen eingefrorenen Progress-Bar bei Multi-Part-VODs (Part X/Y). Root Cause: der weighted-progress Wrapper clamped progress=-1 (HLS unknown-total 1s-Tick) auf 0, was overallProgress auf priorWeight fix-nagelte. Bar oszillierte zwischen indeterminate-animation und einem fixen ~10% Wert. Fix: lastVodProgress persistiert zwischen Path-A-Ticks und Path-B-Streamlink-%-Lines, sodass der Bar smooth waehrend einer Part hochzaehlt. 210 unit tests + e2e:release gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c2b9b5759a
commit
44daa65fe6
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "5.1.0-alpha.2",
|
"version": "5.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "5.1.0-alpha.2",
|
"version": "5.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "5.1.0-alpha.2",
|
"version": "5.0.1",
|
||||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"author": "xRangerDE",
|
"author": "xRangerDE",
|
||||||
|
|||||||
13
src/main.ts
13
src/main.ts
@ -5373,15 +5373,24 @@ async function processDownloadMergeGroup(
|
|||||||
const vodWeight = vodDuration / totalDurationSec;
|
const vodWeight = vodDuration / totalDurationSec;
|
||||||
const priorWeight = mg.items.slice(0, i).reduce((s, v) => s + parseDuration(v.duration_str), 0) / totalDurationSec;
|
const priorWeight = mg.items.slice(0, i).reduce((s, v) => s + parseDuration(v.duration_str), 0) / totalDurationSec;
|
||||||
|
|
||||||
|
// Persistente per-part vodProgress, damit Path-A-Ticks (progress=-1,
|
||||||
|
// 1s-rhythmus mit unknown-total) den determinate Bar nicht auf
|
||||||
|
// priorWeight zuruecksetzen. Wir merken uns die letzte positive
|
||||||
|
// Streamlink-Prozentangabe (Path B) und nutzen sie, bis ein neuer
|
||||||
|
// Wert kommt. Ohne das oszilliert die Bar zwischen indeterminate
|
||||||
|
// und priorWeight, optisch eingefroren.
|
||||||
|
let lastVodProgress = 0;
|
||||||
const result = await downloadVODPart(
|
const result = await downloadVODPart(
|
||||||
vodItem.url,
|
vodItem.url,
|
||||||
tmpFilename,
|
tmpFilename,
|
||||||
null, // startTime: null = full VOD
|
null, // startTime: null = full VOD
|
||||||
null, // endTime: null = full VOD
|
null, // endTime: null = full VOD
|
||||||
(progress) => {
|
(progress) => {
|
||||||
|
if (progress.progress > 0 && progress.progress <= 100) {
|
||||||
|
lastVodProgress = progress.progress;
|
||||||
|
}
|
||||||
// Weighted progress: download phase = 0-70%
|
// Weighted progress: download phase = 0-70%
|
||||||
const vodProgress = progress.progress > 0 ? progress.progress : 0;
|
const overallProgress = (priorWeight + vodWeight * (lastVodProgress / 100)) * 70;
|
||||||
const overallProgress = (priorWeight + vodWeight * (vodProgress / 100)) * 70;
|
|
||||||
onProgress({
|
onProgress({
|
||||||
...progress,
|
...progress,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
|||||||
@ -12,6 +12,7 @@ interface ActiveHover {
|
|||||||
vodId: string;
|
vodId: string;
|
||||||
intervalId: number;
|
intervalId: number;
|
||||||
overlay: HTMLElement;
|
overlay: HTMLElement;
|
||||||
|
card: HTMLElement; // .vod-card, fuer preview-active toggle (separat vom overlay-host)
|
||||||
}
|
}
|
||||||
|
|
||||||
const vodStoryboardClientCache = new Map<string, VodStoryboard | null>();
|
const vodStoryboardClientCache = new Map<string, VodStoryboard | null>();
|
||||||
@ -79,8 +80,7 @@ function clearHoverPreview(): void {
|
|||||||
pendingHoverVodId = null;
|
pendingHoverVodId = null;
|
||||||
if (!activeHover) return;
|
if (!activeHover) return;
|
||||||
window.clearInterval(activeHover.intervalId);
|
window.clearInterval(activeHover.intervalId);
|
||||||
const card = activeHover.overlay.parentElement;
|
activeHover.card.classList.remove('preview-active');
|
||||||
if (card) card.classList.remove('preview-active');
|
|
||||||
// Brief opacity fade-out, then remove from DOM.
|
// Brief opacity fade-out, then remove from DOM.
|
||||||
activeHover.overlay.style.opacity = '0';
|
activeHover.overlay.style.opacity = '0';
|
||||||
const overlayToRemove = activeHover.overlay;
|
const overlayToRemove = activeHover.overlay;
|
||||||
@ -124,20 +124,29 @@ async function activateHoverPreview(card: HTMLElement, vodId: string): Promise<v
|
|||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'vod-storyboard-preview';
|
overlay.className = 'vod-storyboard-preview';
|
||||||
// Scale the sprite so a single cell exactly fills the card width.
|
|
||||||
// The thumbnail aspect-ratio (16:9) matches typical cell aspect
|
// Anchor an .vod-thumb-wrap statt .vod-card: das wrap-Element hat
|
||||||
// (e.g. 220x124 ≈ 1.77) so width-stretch keeps proportions.
|
// exakt Thumbnail-Bounds (kein card-border, kein info/actions-Bereich
|
||||||
const cardWidth = card.getBoundingClientRect().width;
|
// darunter). Faellt zurueck auf das card selbst, falls das Markup
|
||||||
const cellAspect = storyboard.cellWidth / storyboard.cellHeight;
|
// mal anders ist.
|
||||||
const scale = cardWidth / storyboard.cellWidth;
|
const anchor = card.querySelector('.vod-thumb-wrap') as HTMLElement | null;
|
||||||
|
const host = anchor ?? card;
|
||||||
|
const thumb = card.querySelector('.vod-thumbnail') as HTMLElement | null;
|
||||||
|
const thumbRect = (thumb ?? host).getBoundingClientRect();
|
||||||
|
const width = thumbRect.width;
|
||||||
|
const height = thumbRect.height;
|
||||||
|
|
||||||
|
const scale = width / storyboard.cellWidth;
|
||||||
overlay.style.backgroundImage = `url("${storyboard.spriteDataUrl.replace(/"/g, '%22')}")`;
|
overlay.style.backgroundImage = `url("${storyboard.spriteDataUrl.replace(/"/g, '%22')}")`;
|
||||||
overlay.style.backgroundSize = `${storyboard.cols * storyboard.cellWidth * scale}px ${storyboard.rows * storyboard.cellHeight * scale}px`;
|
overlay.style.backgroundSize = `${storyboard.cols * storyboard.cellWidth * scale}px ${storyboard.rows * storyboard.cellHeight * scale}px`;
|
||||||
overlay.style.height = `${cardWidth / cellAspect}px`;
|
overlay.style.backgroundRepeat = 'no-repeat';
|
||||||
|
overlay.style.width = `${width}px`;
|
||||||
|
overlay.style.height = `${height}px`;
|
||||||
// Initial position = first chosen cell.
|
// Initial position = first chosen cell.
|
||||||
const first = cellsToShow[0];
|
const first = cellsToShow[0];
|
||||||
overlay.style.backgroundPosition = `-${first.col * storyboard.cellWidth * scale}px -${first.row * storyboard.cellHeight * scale}px`;
|
overlay.style.backgroundPosition = `-${first.col * storyboard.cellWidth * scale}px -${first.row * storyboard.cellHeight * scale}px`;
|
||||||
|
|
||||||
card.appendChild(overlay);
|
host.appendChild(overlay);
|
||||||
// Trigger CSS transition to opacity:1 on the next frame.
|
// Trigger CSS transition to opacity:1 on the next frame.
|
||||||
requestAnimationFrame(() => { card.classList.add('preview-active'); });
|
requestAnimationFrame(() => { card.classList.add('preview-active'); });
|
||||||
|
|
||||||
@ -148,7 +157,7 @@ async function activateHoverPreview(card: HTMLElement, vodId: string): Promise<v
|
|||||||
frameIdx++;
|
frameIdx++;
|
||||||
}, FRAME_INTERVAL_MS);
|
}, FRAME_INTERVAL_MS);
|
||||||
|
|
||||||
activeHover = { vodId, intervalId, overlay };
|
activeHover = { vodId, intervalId, overlay, card };
|
||||||
}
|
}
|
||||||
|
|
||||||
(window as unknown as { ensureVodHoverHandlersBound: typeof ensureVodHoverHandlersBound }).ensureVodHoverHandlersBound = ensureVodHoverHandlersBound;
|
(window as unknown as { ensureVodHoverHandlersBound: typeof ensureVodHoverHandlersBound }).ensureVodHoverHandlersBound = ensureVodHoverHandlersBound;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user