feat: stats-bar pause-on-hidden + bulk-mark downloaded + title tooltip

Three Phase-13 wins.

1. Stats bar polling pauses while document.hidden. Previously
   setInterval(updateStatsBar, 5000) ran forever, including while
   the user had a different tab focused or the window minimised.
   Now wraps start/stopStatsBarPolling and listens to
   visibilitychange. When the page becomes visible the interval
   restarts; while hidden it sleeps. Saves an IPC round-trip every
   5s when nobody's looking.

2. Bulk mark / unmark "as downloaded" on the VOD bulk-bar. Companion
   to the per-card right-click context menu's mark/unmark items —
   when the user has 5 VODs selected they now get one click to
   toggle the green check on all of them instead of right-clicking
   each. Uses the existing markVodDownloaded IPC, refreshes the
   local config copy + re-renders the grid so badges update live.

3. VOD card title tooltip. The card title is text-overflow:ellipsis
   so longer titles get cut off. Adding title="${full title}"
   surfaces the full text on hover via the native browser tooltip
   — no custom UI needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-05-10 20:20:42 +02:00
parent 092932d8d5
commit e098708398
6 changed files with 65 additions and 5 deletions

View File

@ -266,10 +266,12 @@
<span id="vodHideDownloadedText">Hide downloaded</span> <span id="vodHideDownloadedText">Hide downloaded</span>
</label> </label>
</div> </div>
<div id="vodBulkBar" class="vod-bulk-bar" style="display:none; align-items:center; gap:10px; padding:8px 12px; background: rgba(145, 70, 255, 0.12); border:1px solid rgba(145, 70, 255, 0.4); border-radius:6px; margin-bottom:12px;"> <div id="vodBulkBar" class="vod-bulk-bar" style="display:none; align-items:center; gap:10px; padding:8px 12px; background: rgba(145, 70, 255, 0.12); border:1px solid rgba(145, 70, 255, 0.4); border-radius:6px; margin-bottom:12px; flex-wrap:wrap;">
<span id="vodBulkCount" style="color: var(--text); font-size:13px; font-weight:600;">0 selected</span> <span id="vodBulkCount" style="color: var(--text); font-size:13px; font-weight:600;">0 selected</span>
<span style="flex:1;"></span> <span style="flex:1;"></span>
<button id="vodBulkAddBtn" type="button" onclick="bulkAddSelectedVodsToQueue()" style="background:var(--accent); border:none; border-radius:6px; padding:6px 14px; color:#fff; font-size:13px; font-weight:600; cursor:pointer;">+ Queue</button> <button id="vodBulkAddBtn" type="button" onclick="bulkAddSelectedVodsToQueue()" style="background:var(--accent); border:none; border-radius:6px; padding:6px 14px; color:#fff; font-size:13px; font-weight:600; cursor:pointer;">+ Queue</button>
<button id="vodBulkMarkBtn" type="button" onclick="bulkMarkSelectedDownloaded(true)" style="background:transparent; border:1px solid var(--border-soft); border-radius:6px; padding:6px 12px; color:var(--text-secondary); font-size:13px; cursor:pointer;">Mark as downloaded</button>
<button id="vodBulkUnmarkBtn" type="button" onclick="bulkMarkSelectedDownloaded(false)" style="background:transparent; border:1px solid var(--border-soft); border-radius:6px; padding:6px 12px; color:var(--text-secondary); font-size:13px; cursor:pointer;">Unmark</button>
<button id="vodBulkClearBtn" type="button" onclick="clearVodSelection()" style="background:transparent; border:1px solid var(--border-soft); border-radius:6px; padding:6px 12px; color:var(--text-secondary); font-size:13px; cursor:pointer;">Clear</button> <button id="vodBulkClearBtn" type="button" onclick="clearVodSelection()" style="background:transparent; border:1px solid var(--border-soft); border-radius:6px; padding:6px 12px; color:var(--text-secondary); font-size:13px; cursor:pointer;">Clear</button>
</div> </div>
<div class="vod-grid" id="vodGrid"> <div class="vod-grid" id="vodGrid">

View File

@ -227,6 +227,10 @@ const UI_TEXT_DE = {
bulkClear: 'Loeschen', bulkClear: 'Loeschen',
bulkAddedToQueue: '{count} VODs zur Warteschlange hinzugefuegt.', bulkAddedToQueue: '{count} VODs zur Warteschlange hinzugefuegt.',
bulkAddSkipped: 'Keine VODs hinzugefuegt (bereits in Queue oder ungueltig).', bulkAddSkipped: 'Keine VODs hinzugefuegt (bereits in Queue oder ungueltig).',
bulkMarkDownloaded: 'Als heruntergeladen markieren',
bulkUnmark: 'Markierung entfernen',
bulkMarkedDownloaded: '{count} VODs als heruntergeladen markiert.',
bulkUnmarkedDownloaded: 'Markierung von {count} VODs entfernt.',
alreadyDownloaded: 'Bereits heruntergeladen', alreadyDownloaded: 'Bereits heruntergeladen',
hideDownloaded: 'Bereits geladene ausblenden', hideDownloaded: 'Bereits geladene ausblenden',
hideDownloadedTitle: 'VODs ausblenden, die als bereits heruntergeladen markiert sind', hideDownloadedTitle: 'VODs ausblenden, die als bereits heruntergeladen markiert sind',

View File

@ -227,6 +227,10 @@ const UI_TEXT_EN = {
bulkClear: 'Clear', bulkClear: 'Clear',
bulkAddedToQueue: 'Added {count} VODs to the queue.', bulkAddedToQueue: 'Added {count} VODs to the queue.',
bulkAddSkipped: 'No VODs were added (already in queue or invalid).', bulkAddSkipped: 'No VODs were added (already in queue or invalid).',
bulkMarkDownloaded: 'Mark as downloaded',
bulkUnmark: 'Unmark',
bulkMarkedDownloaded: 'Marked {count} VODs as downloaded.',
bulkUnmarkedDownloaded: 'Removed {count} VODs from the downloaded list.',
alreadyDownloaded: 'Already downloaded', alreadyDownloaded: 'Already downloaded',
hideDownloaded: 'Hide downloaded', hideDownloaded: 'Hide downloaded',
hideDownloadedTitle: 'Hide VODs that are marked as already downloaded', hideDownloadedTitle: 'Hide VODs that are marked as already downloaded',

View File

@ -228,7 +228,7 @@ function buildVodCardHtml(vod: VOD, streamer: string, downloadedIds?: Set<string
${downloadedBadge} ${downloadedBadge}
<img class="vod-thumbnail" loading="lazy" decoding="async" src="${thumb}" alt="" title="${escapeHtml(UI_TEXT.vods.openOnTwitch)}" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 320 180%22><rect fill=%22%23333%22 width=%22320%22 height=%22180%22/></svg>'"> <img class="vod-thumbnail" loading="lazy" decoding="async" src="${thumb}" alt="" title="${escapeHtml(UI_TEXT.vods.openOnTwitch)}" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 320 180%22><rect fill=%22%23333%22 width=%22320%22 height=%22180%22/></svg>'">
<div class="vod-info"> <div class="vod-info">
<div class="vod-title">${safeDisplayTitle}</div> <div class="vod-title" title="${escapeHtml(vod.title || '')}">${safeDisplayTitle}</div>
<div class="vod-meta"> <div class="vod-meta">
<span>${date}</span> <span>${date}</span>
<span>${escapeHtml(vod.duration)}</span> <span>${escapeHtml(vod.duration)}</span>
@ -831,6 +831,34 @@ function clearVodSelection(): void {
if (lastLoadedStreamer) renderVodGridFromCurrentState(); if (lastLoadedStreamer) renderVodGridFromCurrentState();
} }
async function bulkMarkSelectedDownloaded(mark: boolean): Promise<void> {
const urls = Array.from(selectedVodUrls);
if (urls.length === 0) return;
let updated = 0;
for (const url of urls) {
const vod = lastLoadedVods.find((v) => v.url === url);
if (!vod || !vod.id) continue;
try {
const result = await window.api.markVodDownloaded(vod.id, mark);
if (result?.success) updated++;
} catch { /* keep going */ }
}
if (updated === 0) return;
try { config = await window.api.getConfig(); } catch { /* ignore */ }
selectedVodUrls.clear();
updateVodBulkBar();
if (lastLoadedStreamer) renderVodGridFromCurrentState();
const toast = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
if (toast) {
const template = mark ? UI_TEXT.vods.bulkMarkedDownloaded : UI_TEXT.vods.bulkUnmarkedDownloaded;
toast(template.replace('{count}', String(updated)), 'info');
}
}
async function bulkAddSelectedVodsToQueue(): Promise<void> { async function bulkAddSelectedVodsToQueue(): Promise<void> {
const urls = Array.from(selectedVodUrls); const urls = Array.from(selectedVodUrls);
if (urls.length === 0 || !lastLoadedStreamer) return; if (urls.length === 0 || !lastLoadedStreamer) return;

View File

@ -198,6 +198,8 @@ function applyLanguageToStaticUI(): void {
refreshVodSortSelectLabels(); refreshVodSortSelectLabels();
} }
setText('vodBulkAddBtn', UI_TEXT.vods.bulkAddToQueue); setText('vodBulkAddBtn', UI_TEXT.vods.bulkAddToQueue);
setText('vodBulkMarkBtn', UI_TEXT.vods.bulkMarkDownloaded);
setText('vodBulkUnmarkBtn', UI_TEXT.vods.bulkUnmark);
setText('vodBulkClearBtn', UI_TEXT.vods.bulkClear); setText('vodBulkClearBtn', UI_TEXT.vods.bulkClear);
if (typeof updateVodBulkBar === 'function') { if (typeof updateVodBulkBar === 'function') {
// Repopulate the count text in the new locale // Repopulate the count text in the new locale

View File

@ -143,9 +143,14 @@ async function init(): Promise<void> {
byId('mergeProgressText').textContent = Math.round(percent) + '%'; byId('mergeProgressText').textContent = Math.round(percent) + '%';
}); });
// Update stats bar // Update stats bar — paused while the window is hidden so we don't
updateStatsBar(); // burn IPC chatter on a tab nobody is looking at.
const _statsInterval = setInterval(updateStatsBar, 5000); void updateStatsBar();
startStatsBarPolling();
document.addEventListener('visibilitychange', () => {
if (document.hidden) stopStatsBarPolling();
else startStatsBarPolling();
});
if (config.client_id && config.client_secret) { if (config.client_id && config.client_secret) {
await connect(); await connect();
@ -302,6 +307,21 @@ function updateStatusBarQueueSummary(): void {
.replace('{pending}', String(pending)); .replace('{pending}', String(pending));
} }
let statsBarPollTimer: number | null = null;
function startStatsBarPolling(): void {
stopStatsBarPolling();
if (document.hidden) return;
statsBarPollTimer = window.setInterval(updateStatsBar, 5000);
}
function stopStatsBarPolling(): void {
if (statsBarPollTimer !== null) {
window.clearInterval(statsBarPollTimer);
statsBarPollTimer = null;
}
}
async function updateStatsBar(): Promise<void> { async function updateStatsBar(): Promise<void> {
try { try {
const metrics = await window.api.getRuntimeMetrics(); const metrics = await window.api.getRuntimeMetrics();