${safeDisplayTitle}
@@ -509,6 +514,14 @@ function renderVodGridFromCurrentState(): void {
grid.replaceChildren();
updateVodFilterCount(filtered.length, total);
+ // Build the downloaded-ids lookup once per render — Set.has is O(1) vs
+ // Array.includes which would be O(n*m) across all cards.
+ const downloadedIds = new Set(
+ Array.isArray(config.downloaded_vod_ids)
+ ? (config.downloaded_vod_ids as string[]).filter((id) => typeof id === 'string')
+ : []
+ );
+
const scheduleNextChunk = (nextStartIndex: number): void => {
const delayMs = document.hidden ? 16 : 0;
window.setTimeout(() => {
@@ -526,7 +539,7 @@ function renderVodGridFromCurrentState(): void {
return;
}
- grid.insertAdjacentHTML('beforeend', chunk.map((vod) => buildVodCardHtml(vod, lastLoadedStreamer || '')).join(''));
+ grid.insertAdjacentHTML('beforeend', chunk.map((vod) => buildVodCardHtml(vod, lastLoadedStreamer || '', downloadedIds)).join(''));
if (startIndex + chunk.length < filtered.length) {
scheduleNextChunk(startIndex + chunk.length);
diff --git a/src/renderer-texts.ts b/src/renderer-texts.ts
index c5b31c4..3958974 100644
--- a/src/renderer-texts.ts
+++ b/src/renderer-texts.ts
@@ -114,6 +114,9 @@ function applyLanguageToStaticUI(): void {
setTitle('smartSchedulerToggle', UI_TEXT.static.smartSchedulerHint);
setText('duplicatePreventionLabel', UI_TEXT.static.duplicatePreventionLabel);
setText('persistQueueLabel', UI_TEXT.static.persistQueueLabel);
+ setText('autoResumeQueueLabel', UI_TEXT.static.autoResumeQueueLabel);
+ setTitle('autoResumeQueueLabel', UI_TEXT.static.autoResumeQueueHint);
+ setTitle('autoResumeQueueToggle', UI_TEXT.static.autoResumeQueueHint);
setText('metadataCacheMinutesLabel', UI_TEXT.static.metadataCacheMinutesLabel);
setText('filenameTemplatesTitle', UI_TEXT.static.filenameTemplatesTitle);
setText('vodTemplateLabel', UI_TEXT.static.vodTemplateLabel);
diff --git a/src/renderer.ts b/src/renderer.ts
index 242a7a6..2853998 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -63,8 +63,25 @@ async function init(): Promise
{
// Restore last active tab from previous session (default 'vods')
showTab(loadPersistedActiveTab());
- window.api.onQueueUpdated((q: QueueItem[]) => {
- queue = mergeQueueState(Array.isArray(q) ? q : []);
+ window.api.onQueueUpdated(async (q: QueueItem[]) => {
+ const previouslyCompleted = new Set(queue.filter((i) => i.status === 'completed').map((i) => i.id));
+ const next = Array.isArray(q) ? q : [];
+ const newlyCompletedItem = next.some((i) => i.status === 'completed' && !previouslyCompleted.has(i.id));
+ queue = mergeQueueState(next);
+
+ // When an item flips to 'completed' the main process appends its
+ // VOD ID to config.downloaded_vod_ids. Refresh our local config
+ // copy so the "already downloaded" badge on the VOD grid updates
+ // live without waiting for a settings save.
+ if (newlyCompletedItem) {
+ try {
+ config = await window.api.getConfig();
+ } catch { /* network blip — next sync will refresh */ }
+ if (typeof renderVodGridFromCurrentState === 'function' && lastLoadedStreamer) {
+ renderVodGridFromCurrentState();
+ }
+ }
+
renderQueue();
updateStatusBarQueueSummary();
markQueueActivity();
diff --git a/src/styles.css b/src/styles.css
index 8d4b010..b8a4093 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -592,6 +592,29 @@ body {
box-shadow: 0 0 0 2px #9146FF, 0 8px 25px rgba(145, 70, 255, 0.25);
}
+.vod-downloaded-badge {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background: rgba(0, 200, 83, 0.92);
+ color: white;
+ border-radius: 50%;
+ width: 22px;
+ height: 22px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 700;
+ z-index: 2;
+ pointer-events: none;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+}
+
+.vod-card.already-downloaded .vod-thumbnail {
+ opacity: 0.6;
+}
+
.streamer-item.dragging {
opacity: 0.4;
}