feat: manual scan-now buttons + automation status line

Pairs with 4.6.10 (auto-VOD) and 4.6.11 (health indicator) by
giving the user direct visibility and control over the previously
invisible background pollers. Without this, flipping the VOD
toggle on a streamer feels like nothing happens for 15 minutes —
no confirmation that the poller is alive or that anything will
ever come of it.

Both run* functions now return the count they handled. Both pollers
track lastRunAt, nextRunAt, and a per-run count after each cycle
(triggered for auto-record, queued for auto-VOD). Three new IPC
handlers expose this:

- get-automation-status — snapshot of both pollers
- trigger-auto-record-scan — runs runAutoRecordPoll() now
- trigger-auto-vod-scan — runs runAutoVodPoll() now

Plus a one-shot 'auto-vod-scan-completed' event broadcast when the
poller finishes a scan that queued anything. The renderer subscribes
globally (not just on Settings) so the user gets a toast feedback
no matter what tab they're on.

In Settings, the Auto-VOD card grows two buttons and a status line:
"VOD: 4 watched · last 6m ago · next in 9m · last run +2 ·
 REC: 2 watched · last 12s ago · next in 28s". Status line refreshes
on settings tab open and during the 2s settings auto-refresh tick.
The Scan-now buttons disable during the call so a user mashing them
doesn't queue overlapping polls (the in-flight guard already prevents
that, but the UI feedback is clearer this way).

Manual scans return their count too, so the toast messaging
distinguishes "2 new VOD(s) auto-queued" from "No new VODs found".
Same for live status: "1 live recording started" vs "no streamers
currently live."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-05-10 22:09:59 +02:00
parent 398206e01c
commit 28692b2e54
9 changed files with 180 additions and 7 deletions

View File

@ -702,6 +702,11 @@
<span id="autoVodMaxAgeHoursLabel" style="font-size:12px; color:var(--text-secondary); margin-left:12px;">Max. Alter (Stunden)</span> <span id="autoVodMaxAgeHoursLabel" style="font-size:12px; color:var(--text-secondary); margin-left:12px;">Max. Alter (Stunden)</span>
<input type="number" id="autoVodMaxAgeHours" min="1" max="720" value="24" style="width:90px;"> <input type="number" id="autoVodMaxAgeHours" min="1" max="720" value="24" style="width:90px;">
</div> </div>
<div class="form-row" style="align-items: center; gap: 12px; flex-wrap: wrap;">
<button class="btn-secondary" id="btnAutoVodScanNow" onclick="triggerManualAutoVodScan()">Jetzt scannen</button>
<button class="btn-secondary" id="btnAutoRecordScanNow" onclick="triggerManualAutoRecordScan()">Live-Status pruefen</button>
<span id="autoVodStatusLine" style="font-size:12px; color: var(--text-secondary);"></span>
</div>
</div> </div>
<div class="settings-card"> <div class="settings-card">

View File

@ -2946,6 +2946,9 @@ function downloadVODPart(
const autoRecordLastLiveState = new Map<string, boolean>(); const autoRecordLastLiveState = new Map<string, boolean>();
let autoRecordPollTimer: NodeJS.Timeout | null = null; let autoRecordPollTimer: NodeJS.Timeout | null = null;
let autoRecordPollInFlight = false; let autoRecordPollInFlight = false;
let autoRecordLastRunAt = 0;
let autoRecordNextRunAt = 0;
let autoRecordLastTriggerCount = 0;
function stopAutoRecordPoller(): void { function stopAutoRecordPoller(): void {
if (autoRecordPollTimer) { if (autoRecordPollTimer) {
@ -2965,14 +2968,16 @@ function restartAutoRecordPoller(): void {
appendDebugLog('auto-record-poller-start', { streamers: list.length, seconds }); appendDebugLog('auto-record-poller-start', { streamers: list.length, seconds });
autoRecordPollTimer = setInterval(() => { void runAutoRecordPoll(); }, seconds * 1000); autoRecordPollTimer = setInterval(() => { void runAutoRecordPoll(); }, seconds * 1000);
autoRecordPollTimer.unref?.(); autoRecordPollTimer.unref?.();
autoRecordNextRunAt = Date.now() + seconds * 1000;
// Kick off an immediate first poll so a freshly-enabled streamer that's // Kick off an immediate first poll so a freshly-enabled streamer that's
// already live gets picked up without waiting a full interval. // already live gets picked up without waiting a full interval.
setTimeout(() => { void runAutoRecordPoll(); }, 1500); setTimeout(() => { void runAutoRecordPoll(); }, 1500);
} }
async function runAutoRecordPoll(): Promise<void> { async function runAutoRecordPoll(): Promise<number> {
if (autoRecordPollInFlight) return; if (autoRecordPollInFlight) return 0;
autoRecordPollInFlight = true; autoRecordPollInFlight = true;
let triggered = 0;
try { try {
const list = Array.isArray(config.auto_record_streamers) ? [...config.auto_record_streamers] : []; const list = Array.isArray(config.auto_record_streamers) ? [...config.auto_record_streamers] : [];
for (const streamer of list) { for (const streamer of list) {
@ -3018,6 +3023,7 @@ async function runAutoRecordPoll(): Promise<void> {
downloadQueue.push(liveItem); downloadQueue.push(liveItem);
saveQueue(downloadQueue); saveQueue(downloadQueue);
emitQueueUpdated(); emitQueueUpdated();
triggered++;
appendDebugLog('auto-record-triggered', { streamer, title: liveItem.title }); appendDebugLog('auto-record-triggered', { streamer, title: liveItem.title });
if (!isDownloading) { if (!isDownloading) {
@ -3028,7 +3034,12 @@ async function runAutoRecordPoll(): Promise<void> {
appendDebugLog('auto-record-poll-failed', String(e)); appendDebugLog('auto-record-poll-failed', String(e));
} finally { } finally {
autoRecordPollInFlight = false; autoRecordPollInFlight = false;
autoRecordLastRunAt = Date.now();
autoRecordLastTriggerCount = triggered;
const seconds = normalizeAutoRecordPollSeconds(config.auto_record_poll_seconds);
autoRecordNextRunAt = Date.now() + seconds * 1000;
} }
return triggered;
} }
// ========================================== // ==========================================
@ -3042,6 +3053,9 @@ async function runAutoRecordPoll(): Promise<void> {
// minute-level lag is fine. // minute-level lag is fine.
let autoVodPollTimer: NodeJS.Timeout | null = null; let autoVodPollTimer: NodeJS.Timeout | null = null;
let autoVodPollInFlight = false; let autoVodPollInFlight = false;
let autoVodLastRunAt = 0;
let autoVodNextRunAt = 0;
let autoVodLastQueuedCount = 0;
function stopAutoVodPoller(): void { function stopAutoVodPoller(): void {
if (autoVodPollTimer) { if (autoVodPollTimer) {
@ -3065,15 +3079,17 @@ function restartAutoVodPoller(): void {
appendDebugLog('auto-vod-poller-start', { streamers: list.length, minutes }); appendDebugLog('auto-vod-poller-start', { streamers: list.length, minutes });
autoVodPollTimer = setInterval(() => { void runAutoVodPoll(); }, minutes * 60 * 1000); autoVodPollTimer = setInterval(() => { void runAutoVodPoll(); }, minutes * 60 * 1000);
autoVodPollTimer.unref?.(); autoVodPollTimer.unref?.();
autoVodNextRunAt = Date.now() + minutes * 60 * 1000;
setTimeout(() => { void runAutoVodPoll(); }, 5000); setTimeout(() => { void runAutoVodPoll(); }, 5000);
} }
async function runAutoVodPoll(): Promise<void> { async function runAutoVodPoll(): Promise<number> {
if (autoVodPollInFlight) return; if (autoVodPollInFlight) return 0;
autoVodPollInFlight = true; autoVodPollInFlight = true;
let queuedCount = 0;
try { try {
const list = Array.isArray(config.auto_vod_download_streamers) ? [...config.auto_vod_download_streamers] : []; const list = Array.isArray(config.auto_vod_download_streamers) ? [...config.auto_vod_download_streamers] : [];
if (list.length === 0) return; if (list.length === 0) return 0;
const maxAgeHours = (() => { const maxAgeHours = (() => {
const n = Number(config.auto_vod_max_age_hours); const n = Number(config.auto_vod_max_age_hours);
@ -3123,6 +3139,7 @@ async function runAutoVodPoll(): Promise<void> {
}; };
downloadQueue.push(queueItem); downloadQueue.push(queueItem);
queuedUrls.add(vod.url); queuedUrls.add(vod.url);
queuedCount++;
appendDebugLog('auto-vod-queued', { streamer, vodId: vod.id, title: queueItem.title }); appendDebugLog('auto-vod-queued', { streamer, vodId: vod.id, title: queueItem.title });
if (config.discord_notify_vod_auto_queued) { if (config.discord_notify_vod_auto_queued) {
@ -3152,8 +3169,20 @@ async function runAutoVodPoll(): Promise<void> {
appendDebugLog('auto-vod-poll-failed', String(e)); appendDebugLog('auto-vod-poll-failed', String(e));
} finally { } finally {
autoVodPollInFlight = false; autoVodPollInFlight = false;
autoVodLastRunAt = Date.now();
autoVodLastQueuedCount = queuedCount;
const minutes = (() => {
const n = Number(config.auto_vod_download_poll_minutes);
if (!Number.isFinite(n)) return 15;
return Math.max(5, Math.min(360, Math.floor(n)));
})();
autoVodNextRunAt = Date.now() + minutes * 60 * 1000;
if (queuedCount > 0 && mainWindow) {
mainWindow.webContents.send('auto-vod-scan-completed', { queuedCount });
} }
} }
return queuedCount;
}
// ========================================== // ==========================================
// CHAT REPLAY DOWNLOAD // CHAT REPLAY DOWNLOAD
@ -5327,6 +5356,33 @@ function setupAutoUpdater() {
// ========================================== // ==========================================
ipcMain.handle('get-config', () => config); ipcMain.handle('get-config', () => config);
ipcMain.handle('get-automation-status', () => ({
autoRecord: {
watching: Array.isArray(config.auto_record_streamers) ? config.auto_record_streamers.length : 0,
lastRunAt: autoRecordLastRunAt,
nextRunAt: autoRecordNextRunAt,
lastTriggeredCount: autoRecordLastTriggerCount,
inFlight: autoRecordPollInFlight
},
autoVod: {
watching: Array.isArray(config.auto_vod_download_streamers) ? config.auto_vod_download_streamers.length : 0,
lastRunAt: autoVodLastRunAt,
nextRunAt: autoVodNextRunAt,
lastQueuedCount: autoVodLastQueuedCount,
inFlight: autoVodPollInFlight
}
}));
ipcMain.handle('trigger-auto-record-scan', async () => {
const triggered = await runAutoRecordPoll();
return { triggered };
});
ipcMain.handle('trigger-auto-vod-scan', async () => {
const queuedCount = await runAutoVodPoll();
return { queuedCount };
});
ipcMain.handle('save-config', (_, newConfig: Partial<Config>) => { ipcMain.handle('save-config', (_, newConfig: Partial<Config>) => {
const previousClientId = config.client_id; const previousClientId = config.client_id;
const previousClientSecret = config.client_secret; const previousClientSecret = config.client_secret;

View File

@ -92,6 +92,12 @@ contextBridge.exposeInMainWorld('api', {
getStorageStats: () => ipcRenderer.invoke('get-storage-stats'), getStorageStats: () => ipcRenderer.invoke('get-storage-stats'),
runStorageCleanup: (options?: { dryRun?: boolean }) => ipcRenderer.invoke('run-storage-cleanup', options), runStorageCleanup: (options?: { dryRun?: boolean }) => ipcRenderer.invoke('run-storage-cleanup', options),
readChatFile: (filePath: string) => ipcRenderer.invoke('read-chat-file', filePath), readChatFile: (filePath: string) => ipcRenderer.invoke('read-chat-file', filePath),
getAutomationStatus: () => ipcRenderer.invoke('get-automation-status'),
triggerAutoVodScan: () => ipcRenderer.invoke('trigger-auto-vod-scan'),
triggerAutoRecordScan: () => ipcRenderer.invoke('trigger-auto-record-scan'),
onAutoVodScanCompleted: (callback: (info: { queuedCount: number }) => void) => {
ipcRenderer.on('auto-vod-scan-completed', (_, info) => callback(info));
},
// Video Cutter // Video Cutter
getVideoInfo: (filePath: string): Promise<VideoInfo | null> => ipcRenderer.invoke('get-video-info', filePath), getVideoInfo: (filePath: string): Promise<VideoInfo | null> => ipcRenderer.invoke('get-video-info', filePath),

View File

@ -264,6 +264,13 @@ interface ApiBridge {
getStorageStats(): Promise<StorageStatsResult>; getStorageStats(): Promise<StorageStatsResult>;
runStorageCleanup(options?: { dryRun?: boolean }): Promise<CleanupReport>; runStorageCleanup(options?: { dryRun?: boolean }): Promise<CleanupReport>;
readChatFile(filePath: string): Promise<{ success: boolean; error?: string; format?: 'replay' | 'live'; messages?: Array<Record<string, unknown>>; truncated?: boolean; total?: number }>; readChatFile(filePath: string): Promise<{ success: boolean; error?: string; format?: 'replay' | 'live'; messages?: Array<Record<string, unknown>>; truncated?: boolean; total?: number }>;
getAutomationStatus(): Promise<{
autoRecord: { watching: number; lastRunAt: number; nextRunAt: number; lastTriggeredCount: number; inFlight: boolean };
autoVod: { watching: number; lastRunAt: number; nextRunAt: number; lastQueuedCount: number; inFlight: boolean };
}>;
triggerAutoVodScan(): Promise<{ queuedCount: number }>;
triggerAutoRecordScan(): Promise<{ triggered: number }>;
onAutoVodScanCompleted(callback: (info: { queuedCount: number }) => void): void;
getVideoInfo(filePath: string): Promise<VideoInfo | null>; getVideoInfo(filePath: string): Promise<VideoInfo | null>;
extractFrame(filePath: string, timeSeconds: number): Promise<string | null>; extractFrame(filePath: string, timeSeconds: number): Promise<string | null>;
cutVideo(inputFile: string, startTime: number, endTime: number): Promise<{ success: boolean; outputFile: string | null }>; cutVideo(inputFile: string, startTime: number, endTime: number): Promise<{ success: boolean; outputFile: string | null }>;

View File

@ -93,6 +93,8 @@ const UI_TEXT_DE = {
autoVodCardIntro: 'Streamer mit aktiviertem VOD-Toggle werden in dem hier festgelegten Intervall auf neue Twitch-VODs geprueft. Neue VODs innerhalb des Alters-Fensters werden automatisch zur Download-Queue hinzugefuegt.', autoVodCardIntro: 'Streamer mit aktiviertem VOD-Toggle werden in dem hier festgelegten Intervall auf neue Twitch-VODs geprueft. Neue VODs innerhalb des Alters-Fensters werden automatisch zur Download-Queue hinzugefuegt.',
autoVodPollMinutesLabel: 'Poll-Intervall (Minuten)', autoVodPollMinutesLabel: 'Poll-Intervall (Minuten)',
autoVodMaxAgeHoursLabel: 'Max. Alter (Stunden)', autoVodMaxAgeHoursLabel: 'Max. Alter (Stunden)',
autoVodScanNow: 'Jetzt scannen',
autoRecordScanNow: 'Live-Status pruefen',
discordNotifyVodCompleteLabel: 'Bei abgeschlossenem VOD-Download benachrichtigen', discordNotifyVodCompleteLabel: 'Bei abgeschlossenem VOD-Download benachrichtigen',
backupCardTitle: 'Sicherung & Wartung', backupCardTitle: 'Sicherung & Wartung',
backupCardIntro: 'Konfiguration sichern, auf einem anderen Geraet wiederherstellen oder die Liste der bereits heruntergeladenen VODs zuruecksetzen.', backupCardIntro: 'Konfiguration sichern, auf einem anderen Geraet wiederherstellen oder die Liste der bereits heruntergeladenen VODs zuruecksetzen.',
@ -274,7 +276,11 @@ const UI_TEXT_DE = {
autoRecordDisabled: 'Auto-Aufnahme fuer {streamer} deaktiviert.', autoRecordDisabled: 'Auto-Aufnahme fuer {streamer} deaktiviert.',
autoVodTitle: 'Neue VODs (kuerzlich veroeffentlicht) automatisch herunterladen', autoVodTitle: 'Neue VODs (kuerzlich veroeffentlicht) automatisch herunterladen',
autoVodEnabled: 'Auto-VOD aktiviert fuer {streamer}. Neue VODs werden automatisch geladen.', autoVodEnabled: 'Auto-VOD aktiviert fuer {streamer}. Neue VODs werden automatisch geladen.',
autoVodDisabled: 'Auto-VOD fuer {streamer} deaktiviert.' autoVodDisabled: 'Auto-VOD fuer {streamer} deaktiviert.',
autoVodScanQueued: '{count} neue VOD(s) automatisch eingereiht.',
autoVodScanEmpty: 'Keine neuen VODs gefunden.',
autoRecordScanTriggered: 'Manueller Scan: {count} Live-Aufnahme(n) gestartet.',
autoRecordScanEmpty: 'Manueller Scan: kein Streamer ist gerade live.'
}, },
vods: { vods: {
noneTitle: 'Keine VODs', noneTitle: 'Keine VODs',

View File

@ -94,6 +94,8 @@ const UI_TEXT_EN = {
autoVodCardIntro: 'Streamers with the VOD toggle on are scanned for new Twitch VODs at the interval set here. New VODs within the age window are added to the download queue automatically.', autoVodCardIntro: 'Streamers with the VOD toggle on are scanned for new Twitch VODs at the interval set here. New VODs within the age window are added to the download queue automatically.',
autoVodPollMinutesLabel: 'Poll interval (minutes)', autoVodPollMinutesLabel: 'Poll interval (minutes)',
autoVodMaxAgeHoursLabel: 'Max age (hours)', autoVodMaxAgeHoursLabel: 'Max age (hours)',
autoVodScanNow: 'Scan now',
autoRecordScanNow: 'Check live status',
backupCardTitle: 'Backup & Maintenance', backupCardTitle: 'Backup & Maintenance',
backupCardIntro: 'Back up your configuration, restore it on another machine, or reset the list of already-downloaded VODs.', backupCardIntro: 'Back up your configuration, restore it on another machine, or reset the list of already-downloaded VODs.',
exportConfig: 'Export config', exportConfig: 'Export config',
@ -274,7 +276,11 @@ const UI_TEXT_EN = {
autoRecordDisabled: 'Auto-record disabled for {streamer}.', autoRecordDisabled: 'Auto-record disabled for {streamer}.',
autoVodTitle: 'Auto-download new VODs (recently published) for this streamer', autoVodTitle: 'Auto-download new VODs (recently published) for this streamer',
autoVodEnabled: 'Auto-VOD enabled for {streamer}. Will pick up new VODs.', autoVodEnabled: 'Auto-VOD enabled for {streamer}. Will pick up new VODs.',
autoVodDisabled: 'Auto-VOD disabled for {streamer}.' autoVodDisabled: 'Auto-VOD disabled for {streamer}.',
autoVodScanQueued: '{count} new VOD(s) auto-queued.',
autoVodScanEmpty: 'No new VODs found.',
autoRecordScanTriggered: 'Manual scan: {count} live recording(s) started.',
autoRecordScanEmpty: 'Manual scan: no streamers currently live.'
}, },
vods: { vods: {
noneTitle: 'No VODs', noneTitle: 'No VODs',

View File

@ -162,6 +162,7 @@ function toggleRuntimeMetricsAutoRefresh(enabled: boolean): void {
} }
void refreshRuntimeMetrics(false); void refreshRuntimeMetrics(false);
void refreshAutomationStatusLine();
}, 2000); }, 2000);
} }
} }
@ -199,6 +200,7 @@ function changeLanguage(lang: string): void {
} }
void refreshRuntimeMetrics(); void refreshRuntimeMetrics();
void refreshAutomationStatusLine();
validateFilenameTemplates(); validateFilenameTemplates();
} }
@ -898,3 +900,79 @@ function changeTheme(theme: string): void {
config.theme = theme; config.theme = theme;
void window.api.saveConfig({ theme }); void window.api.saveConfig({ theme });
} }
function formatRelativeTime(ms: number, future: boolean): string {
if (!Number.isFinite(ms) || ms <= 0) {
return future ? UI_TEXT.streamers.autoVodScanEmpty || '' : '-';
}
const seconds = Math.max(0, Math.floor(ms / 1000));
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
return `${hours}h ${minutes % 60}m`;
}
async function refreshAutomationStatusLine(): Promise<void> {
const lineEl = document.getElementById('autoVodStatusLine');
if (!lineEl) return;
try {
const status = await window.api.getAutomationStatus();
const now = Date.now();
const parts: string[] = [];
if (status.autoVod.watching > 0) {
const lastAgo = status.autoVod.lastRunAt > 0 ? formatRelativeTime(now - status.autoVod.lastRunAt, false) : '-';
const nextIn = status.autoVod.nextRunAt > now ? formatRelativeTime(status.autoVod.nextRunAt - now, true) : '-';
parts.push(`VOD: ${status.autoVod.watching} watched · last ${lastAgo} ago · next in ${nextIn} · last run +${status.autoVod.lastQueuedCount}`);
}
if (status.autoRecord.watching > 0) {
const lastAgo = status.autoRecord.lastRunAt > 0 ? formatRelativeTime(now - status.autoRecord.lastRunAt, false) : '-';
const nextIn = status.autoRecord.nextRunAt > now ? formatRelativeTime(status.autoRecord.nextRunAt - now, true) : '-';
parts.push(`REC: ${status.autoRecord.watching} watched · last ${lastAgo} ago · next in ${nextIn}`);
}
if (parts.length === 0) parts.push('No streamers watched.');
lineEl.textContent = parts.join(' · ');
} catch (_) {
lineEl.textContent = '';
}
}
async function triggerManualAutoVodScan(): Promise<void> {
const toast = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
const btn = document.getElementById('btnAutoVodScanNow') as HTMLButtonElement | null;
if (btn) btn.disabled = true;
try {
const result = await window.api.triggerAutoVodScan();
if (toast) {
const tmpl = result.queuedCount > 0
? UI_TEXT.streamers.autoVodScanQueued
: UI_TEXT.streamers.autoVodScanEmpty;
toast((tmpl || '').replace('{count}', String(result.queuedCount)), 'info');
}
} finally {
if (btn) btn.disabled = false;
void refreshAutomationStatusLine();
}
}
async function triggerManualAutoRecordScan(): Promise<void> {
const toast = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
const btn = document.getElementById('btnAutoRecordScanNow') as HTMLButtonElement | null;
if (btn) btn.disabled = true;
try {
const result = await window.api.triggerAutoRecordScan();
if (toast) {
const tmpl = result.triggered > 0
? UI_TEXT.streamers.autoRecordScanTriggered
: UI_TEXT.streamers.autoRecordScanEmpty;
toast((tmpl || '').replace('{count}', String(result.triggered)), 'info');
}
} finally {
if (btn) btn.disabled = false;
void refreshAutomationStatusLine();
}
}
(window as unknown as { triggerManualAutoVodScan: typeof triggerManualAutoVodScan }).triggerManualAutoVodScan = triggerManualAutoVodScan;
(window as unknown as { triggerManualAutoRecordScan: typeof triggerManualAutoRecordScan }).triggerManualAutoRecordScan = triggerManualAutoRecordScan;

View File

@ -202,6 +202,8 @@ function applyLanguageToStaticUI(): void {
setText('autoVodCardIntro', UI_TEXT.static.autoVodCardIntro); setText('autoVodCardIntro', UI_TEXT.static.autoVodCardIntro);
setText('autoVodPollMinutesLabel', UI_TEXT.static.autoVodPollMinutesLabel); setText('autoVodPollMinutesLabel', UI_TEXT.static.autoVodPollMinutesLabel);
setText('autoVodMaxAgeHoursLabel', UI_TEXT.static.autoVodMaxAgeHoursLabel); setText('autoVodMaxAgeHoursLabel', UI_TEXT.static.autoVodMaxAgeHoursLabel);
setText('btnAutoVodScanNow', UI_TEXT.static.autoVodScanNow);
setText('btnAutoRecordScanNow', UI_TEXT.static.autoRecordScanNow);
setText('backupCardTitle', UI_TEXT.static.backupCardTitle); setText('backupCardTitle', UI_TEXT.static.backupCardTitle);
setText('backupCardIntro', UI_TEXT.static.backupCardIntro); setText('backupCardIntro', UI_TEXT.static.backupCardIntro);
setText('btnExportConfig', UI_TEXT.static.exportConfig); setText('btnExportConfig', UI_TEXT.static.exportConfig);

View File

@ -124,6 +124,13 @@ async function init(): Promise<void> {
markQueueActivity(); markQueueActivity();
}); });
window.api.onAutoVodScanCompleted(({ queuedCount }) => {
if (queuedCount > 0) {
const tmpl = UI_TEXT.streamers.autoVodScanQueued || '{count} new VOD(s) auto-queued.';
showAppToast(tmpl.replace('{count}', String(queuedCount)), 'info');
}
});
window.api.onDownloadStarted(() => { window.api.onDownloadStarted(() => {
downloading = true; downloading = true;
updateDownloadButtonState(); updateDownloadButtonState();