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:
parent
092932d8d5
commit
e098708398
@ -266,10 +266,12 @@
|
||||
<span id="vodHideDownloadedText">Hide downloaded</span>
|
||||
</label>
|
||||
</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 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="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>
|
||||
</div>
|
||||
<div class="vod-grid" id="vodGrid">
|
||||
|
||||
@ -227,6 +227,10 @@ const UI_TEXT_DE = {
|
||||
bulkClear: 'Loeschen',
|
||||
bulkAddedToQueue: '{count} VODs zur Warteschlange hinzugefuegt.',
|
||||
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',
|
||||
hideDownloaded: 'Bereits geladene ausblenden',
|
||||
hideDownloadedTitle: 'VODs ausblenden, die als bereits heruntergeladen markiert sind',
|
||||
|
||||
@ -227,6 +227,10 @@ const UI_TEXT_EN = {
|
||||
bulkClear: 'Clear',
|
||||
bulkAddedToQueue: 'Added {count} VODs to the queue.',
|
||||
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',
|
||||
hideDownloaded: 'Hide downloaded',
|
||||
hideDownloadedTitle: 'Hide VODs that are marked as already downloaded',
|
||||
|
||||
@ -228,7 +228,7 @@ function buildVodCardHtml(vod: VOD, streamer: string, downloadedIds?: Set<string
|
||||
${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>'">
|
||||
<div class="vod-info">
|
||||
<div class="vod-title">${safeDisplayTitle}</div>
|
||||
<div class="vod-title" title="${escapeHtml(vod.title || '')}">${safeDisplayTitle}</div>
|
||||
<div class="vod-meta">
|
||||
<span>${date}</span>
|
||||
<span>${escapeHtml(vod.duration)}</span>
|
||||
@ -831,6 +831,34 @@ function clearVodSelection(): void {
|
||||
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> {
|
||||
const urls = Array.from(selectedVodUrls);
|
||||
if (urls.length === 0 || !lastLoadedStreamer) return;
|
||||
|
||||
@ -198,6 +198,8 @@ function applyLanguageToStaticUI(): void {
|
||||
refreshVodSortSelectLabels();
|
||||
}
|
||||
setText('vodBulkAddBtn', UI_TEXT.vods.bulkAddToQueue);
|
||||
setText('vodBulkMarkBtn', UI_TEXT.vods.bulkMarkDownloaded);
|
||||
setText('vodBulkUnmarkBtn', UI_TEXT.vods.bulkUnmark);
|
||||
setText('vodBulkClearBtn', UI_TEXT.vods.bulkClear);
|
||||
if (typeof updateVodBulkBar === 'function') {
|
||||
// Repopulate the count text in the new locale
|
||||
|
||||
@ -143,9 +143,14 @@ async function init(): Promise<void> {
|
||||
byId('mergeProgressText').textContent = Math.round(percent) + '%';
|
||||
});
|
||||
|
||||
// Update stats bar
|
||||
updateStatsBar();
|
||||
const _statsInterval = setInterval(updateStatsBar, 5000);
|
||||
// Update stats bar — paused while the window is hidden so we don't
|
||||
// burn IPC chatter on a tab nobody is looking at.
|
||||
void updateStatsBar();
|
||||
startStatsBarPolling();
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) stopStatsBarPolling();
|
||||
else startStatsBarPolling();
|
||||
});
|
||||
|
||||
if (config.client_id && config.client_secret) {
|
||||
await connect();
|
||||
@ -302,6 +307,21 @@ function updateStatusBarQueueSummary(): void {
|
||||
.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> {
|
||||
try {
|
||||
const metrics = await window.api.getRuntimeMetrics();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user