feat: hide-downloaded filter + reset list + config export/import
Three companion features around the 4.5.22 already-downloaded badge. 1. "Hide downloaded" toggle in the VOD filter row. Persisted to localStorage so power users who keep it on across sessions don't re-flip it on every launch. Filter applies before the title-search filter so the match counter stays consistent. 2. "Reset downloaded list" button in a new Backup & Maintenance settings card. Confirm-dialog before clearing, IPC returns the removed count for a "cleared N entries" toast. Renderer refreshes its config copy + re-renders the VOD grid so badges disappear immediately. No files are touched. 3. Config export / import via dialog.show*Dialog. Export strips client_secret (should never travel as plain text via cloud sync), tags the file with __exportVersion + __exportedAt. Import runs the JSON through normalizeConfigTemplates so out-of-range fields fall back to defaults; if the imported file lacks client_secret, the existing value is preserved. After import the renderer reloads config + relocalizes if language changed + re-renders streamers / settings form / VOD grid. DE + EN locale strings for every label, button, toast, and confirm dialog. New backupCardTitle / backupCardIntro section header in Settings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56d4e0904f
commit
e2c0e3a2bf
@ -257,6 +257,10 @@
|
|||||||
<option value="duration_asc">Shortest first</option>
|
<option value="duration_asc">Shortest first</option>
|
||||||
</select>
|
</select>
|
||||||
<span id="vodFilterCount" style="color: var(--text-secondary); font-size:12px; min-width:80px;"></span>
|
<span id="vodFilterCount" style="color: var(--text-secondary); font-size:12px; min-width:80px;"></span>
|
||||||
|
<label id="vodHideDownloadedLabel" style="display:flex; align-items:center; gap:6px; color: var(--text-secondary); font-size:12px; cursor:pointer; user-select:none;" title="">
|
||||||
|
<input type="checkbox" id="vodHideDownloadedToggle" onchange="onVodHideDownloadedChange()" style="accent-color: var(--accent); cursor:pointer;">
|
||||||
|
<span id="vodHideDownloadedText">Hide downloaded</span>
|
||||||
|
</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;">
|
||||||
<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>
|
||||||
@ -558,6 +562,16 @@
|
|||||||
<pre id="debugLogOutput" class="log-panel">Lade...</pre>
|
<pre id="debugLogOutput" class="log-panel">Lade...</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3 id="backupCardTitle">Sicherung & Wartung</h3>
|
||||||
|
<p id="backupCardIntro" style="color: var(--text-secondary); font-size:13px; margin-bottom:12px; line-height:1.5;">Konfiguration sichern, auf einem anderen Geraet wiederherstellen, oder die Liste der bereits heruntergeladenen VODs zuruecksetzen.</p>
|
||||||
|
<div class="form-row" style="margin-bottom: 10px; flex-wrap: wrap;">
|
||||||
|
<button class="btn-secondary" id="btnExportConfig" onclick="exportConfigToFile()">Konfiguration exportieren</button>
|
||||||
|
<button class="btn-secondary" id="btnImportConfig" onclick="importConfigFromFile()">Konfiguration importieren</button>
|
||||||
|
<button class="btn-secondary" id="btnResetDownloadedIds" onclick="resetDownloadedIds()">Downloaded-VODs zuruecksetzen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 id="runtimeMetricsTitle">Runtime Metrics</h3>
|
<h3 id="runtimeMetricsTitle">Runtime Metrics</h3>
|
||||||
<div class="form-row" style="margin-bottom: 10px; align-items: center;">
|
<div class="form-row" style="margin-bottom: 10px; align-items: center;">
|
||||||
|
|||||||
78
src/main.ts
78
src/main.ts
@ -4276,6 +4276,84 @@ ipcMain.handle('export-runtime-metrics', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('reset-downloaded-vod-ids', () => {
|
||||||
|
const count = Array.isArray(config.downloaded_vod_ids) ? config.downloaded_vod_ids.length : 0;
|
||||||
|
config.downloaded_vod_ids = [];
|
||||||
|
saveConfig(config);
|
||||||
|
appendDebugLog('reset-downloaded-vod-ids', { previousCount: count });
|
||||||
|
return { success: true, removedCount: count };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('export-config', async () => {
|
||||||
|
try {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const defaultName = `twitch-vod-manager-config-${timestamp}.json`;
|
||||||
|
const preferredDir = fs.existsSync(config.download_path) ? config.download_path : app.getPath('desktop');
|
||||||
|
|
||||||
|
const dialogResult = await dialog.showSaveDialog(mainWindow!, {
|
||||||
|
defaultPath: path.join(preferredDir, defaultName),
|
||||||
|
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dialogResult.canceled || !dialogResult.filePath) {
|
||||||
|
return { success: false, cancelled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the secrets from the export — Client Secret should not
|
||||||
|
// travel as plain text across machines / cloud sync. The user
|
||||||
|
// re-enters it on the new machine after import.
|
||||||
|
const exportable = {
|
||||||
|
...config,
|
||||||
|
client_secret: '',
|
||||||
|
__exportVersion: 1,
|
||||||
|
__exportedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
writeFileAtomicSync(dialogResult.filePath, JSON.stringify(exportable, null, 2));
|
||||||
|
return { success: true, filePath: dialogResult.filePath };
|
||||||
|
} catch (e) {
|
||||||
|
appendDebugLog('config-export-failed', String(e));
|
||||||
|
return { success: false, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('import-config', async () => {
|
||||||
|
try {
|
||||||
|
const dialogResult = await dialog.showOpenDialog(mainWindow!, {
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||||
|
});
|
||||||
|
if (dialogResult.canceled || !dialogResult.filePaths[0]) {
|
||||||
|
return { success: false, cancelled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const importPath = dialogResult.filePaths[0];
|
||||||
|
const raw = fs.readFileSync(importPath, 'utf-8');
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!isPlainObject(parsed)) {
|
||||||
|
return { success: false, error: 'Imported file is not a JSON object.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge over current config so unknown / missing keys keep their
|
||||||
|
// existing values. Then run normalizeConfigTemplates so any
|
||||||
|
// out-of-range field falls back to defaults.
|
||||||
|
const merged = normalizeConfigTemplates({ ...config, ...parsed } as Config);
|
||||||
|
|
||||||
|
// Preserve the existing client_secret if the import stripped it
|
||||||
|
// (export does this on purpose) — the user shouldn't lose creds.
|
||||||
|
if (!merged.client_secret && config.client_secret) {
|
||||||
|
merged.client_secret = config.client_secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
config = merged;
|
||||||
|
saveConfig(config);
|
||||||
|
appendDebugLog('config-import-applied', { source: importPath });
|
||||||
|
return { success: true, filePath: importPath };
|
||||||
|
} catch (e) {
|
||||||
|
appendDebugLog('config-import-failed', String(e));
|
||||||
|
return { success: false, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Video Cutter IPC
|
// Video Cutter IPC
|
||||||
ipcMain.handle('get-video-info', async (_, filePath: string) => {
|
ipcMain.handle('get-video-info', async (_, filePath: string) => {
|
||||||
return await getVideoInfo(filePath);
|
return await getVideoInfo(filePath);
|
||||||
|
|||||||
@ -109,6 +109,12 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
getRuntimeMetrics: (): Promise<RuntimeMetricsSnapshot> => ipcRenderer.invoke('get-runtime-metrics'),
|
getRuntimeMetrics: (): Promise<RuntimeMetricsSnapshot> => ipcRenderer.invoke('get-runtime-metrics'),
|
||||||
exportRuntimeMetrics: (): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }> =>
|
exportRuntimeMetrics: (): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }> =>
|
||||||
ipcRenderer.invoke('export-runtime-metrics'),
|
ipcRenderer.invoke('export-runtime-metrics'),
|
||||||
|
resetDownloadedVodIds: (): Promise<{ success: boolean; removedCount: number }> =>
|
||||||
|
ipcRenderer.invoke('reset-downloaded-vod-ids'),
|
||||||
|
exportConfig: (): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }> =>
|
||||||
|
ipcRenderer.invoke('export-config'),
|
||||||
|
importConfig: (): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }> =>
|
||||||
|
ipcRenderer.invoke('import-config'),
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
onDownloadProgress: (callback: (progress: DownloadProgress) => void) => {
|
onDownloadProgress: (callback: (progress: DownloadProgress) => void) => {
|
||||||
|
|||||||
3
src/renderer-globals.d.ts
vendored
3
src/renderer-globals.d.ts
vendored
@ -217,6 +217,9 @@ interface ApiBridge {
|
|||||||
getDebugLog(lines: number): Promise<string>;
|
getDebugLog(lines: number): Promise<string>;
|
||||||
getRuntimeMetrics(): Promise<RuntimeMetricsSnapshot>;
|
getRuntimeMetrics(): Promise<RuntimeMetricsSnapshot>;
|
||||||
exportRuntimeMetrics(): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }>;
|
exportRuntimeMetrics(): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }>;
|
||||||
|
resetDownloadedVodIds(): Promise<{ success: boolean; removedCount: number }>;
|
||||||
|
exportConfig(): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }>;
|
||||||
|
importConfig(): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }>;
|
||||||
onDownloadProgress(callback: (progress: DownloadProgress) => void): void;
|
onDownloadProgress(callback: (progress: DownloadProgress) => void): void;
|
||||||
onQueueUpdated(callback: (queue: QueueItem[]) => void): void;
|
onQueueUpdated(callback: (queue: QueueItem[]) => void): void;
|
||||||
onQueueDuplicateSkipped(callback: (payload: { title: string; streamer: string; url: string }) => void): void;
|
onQueueDuplicateSkipped(callback: (payload: { title: string; streamer: string; url: string }) => void): void;
|
||||||
|
|||||||
@ -54,6 +54,17 @@ const UI_TEXT_DE = {
|
|||||||
apiHelpIntro: 'Du brauchst eine Client-ID und ein Client-Secret von Twitch.',
|
apiHelpIntro: 'Du brauchst eine Client-ID und ein Client-Secret von Twitch.',
|
||||||
apiHelpLinkText: 'dev.twitch.tv/console/apps',
|
apiHelpLinkText: 'dev.twitch.tv/console/apps',
|
||||||
openDebugLogFile: 'Log-Datei oeffnen',
|
openDebugLogFile: 'Log-Datei oeffnen',
|
||||||
|
backupCardTitle: 'Sicherung & Wartung',
|
||||||
|
backupCardIntro: 'Konfiguration sichern, auf einem anderen Geraet wiederherstellen oder die Liste der bereits heruntergeladenen VODs zuruecksetzen.',
|
||||||
|
exportConfig: 'Konfiguration exportieren',
|
||||||
|
importConfig: 'Konfiguration importieren',
|
||||||
|
resetDownloadedIds: 'Downloaded-VODs zuruecksetzen',
|
||||||
|
configExported: 'Konfiguration exportiert.',
|
||||||
|
configExportFailed: 'Export der Konfiguration fehlgeschlagen.',
|
||||||
|
configImported: 'Konfiguration importiert. Einige Aenderungen erfordern evtl. einen Neustart.',
|
||||||
|
configImportFailed: 'Import der Konfiguration fehlgeschlagen.',
|
||||||
|
resetDownloadedConfirm: 'Liste der heruntergeladenen VODs zuruecksetzen? Karten verlieren das gruene Haekchen, es werden aber keine Dateien geloescht.',
|
||||||
|
resetDownloadedDone: '{count} Eintraege aus der Downloaded-Liste entfernt.',
|
||||||
duplicatePreventionLabel: 'Duplikate in Queue verhindern',
|
duplicatePreventionLabel: 'Duplikate in Queue verhindern',
|
||||||
persistQueueLabel: 'Queue zwischen App-Starts speichern',
|
persistQueueLabel: 'Queue zwischen App-Starts speichern',
|
||||||
autoResumeQueueLabel: 'Queue beim Start automatisch fortsetzen',
|
autoResumeQueueLabel: 'Queue beim Start automatisch fortsetzen',
|
||||||
@ -194,7 +205,9 @@ 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).',
|
||||||
alreadyDownloaded: 'Bereits heruntergeladen'
|
alreadyDownloaded: 'Bereits heruntergeladen',
|
||||||
|
hideDownloaded: 'Bereits geladene ausblenden',
|
||||||
|
hideDownloadedTitle: 'VODs ausblenden, die als bereits heruntergeladen markiert sind'
|
||||||
},
|
},
|
||||||
clips: {
|
clips: {
|
||||||
dialogTitle: 'VOD zuschneiden',
|
dialogTitle: 'VOD zuschneiden',
|
||||||
|
|||||||
@ -54,6 +54,17 @@ const UI_TEXT_EN = {
|
|||||||
apiHelpIntro: 'You need a Client ID and Client Secret from Twitch.',
|
apiHelpIntro: 'You need a Client ID and Client Secret from Twitch.',
|
||||||
apiHelpLinkText: 'dev.twitch.tv/console/apps',
|
apiHelpLinkText: 'dev.twitch.tv/console/apps',
|
||||||
openDebugLogFile: 'Open log file',
|
openDebugLogFile: 'Open log file',
|
||||||
|
backupCardTitle: 'Backup & Maintenance',
|
||||||
|
backupCardIntro: 'Back up your configuration, restore it on another machine, or reset the list of already-downloaded VODs.',
|
||||||
|
exportConfig: 'Export config',
|
||||||
|
importConfig: 'Import config',
|
||||||
|
resetDownloadedIds: 'Reset downloaded list',
|
||||||
|
configExported: 'Configuration exported.',
|
||||||
|
configExportFailed: 'Configuration export failed.',
|
||||||
|
configImported: 'Configuration imported. Some changes may need a restart.',
|
||||||
|
configImportFailed: 'Configuration import failed.',
|
||||||
|
resetDownloadedConfirm: 'Reset the downloaded-VODs list? Cards will lose the green check mark, but no files are deleted.',
|
||||||
|
resetDownloadedDone: 'Cleared {count} entries from the downloaded list.',
|
||||||
duplicatePreventionLabel: 'Prevent duplicate queue entries',
|
duplicatePreventionLabel: 'Prevent duplicate queue entries',
|
||||||
persistQueueLabel: 'Keep queue between app restarts',
|
persistQueueLabel: 'Keep queue between app restarts',
|
||||||
autoResumeQueueLabel: 'Auto-resume the queue on startup',
|
autoResumeQueueLabel: 'Auto-resume the queue on startup',
|
||||||
@ -194,7 +205,9 @@ 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).',
|
||||||
alreadyDownloaded: 'Already downloaded'
|
alreadyDownloaded: 'Already downloaded',
|
||||||
|
hideDownloaded: 'Hide downloaded',
|
||||||
|
hideDownloadedTitle: 'Hide VODs that are marked as already downloaded'
|
||||||
},
|
},
|
||||||
clips: {
|
clips: {
|
||||||
dialogTitle: 'Trim VOD',
|
dialogTitle: 'Trim VOD',
|
||||||
|
|||||||
@ -265,6 +265,60 @@ async function runPreflight(autoFix = false): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportConfigToFile(): Promise<void> {
|
||||||
|
const result = await window.api.exportConfig();
|
||||||
|
const toast = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
||||||
|
if (result.success) {
|
||||||
|
if (toast) toast(UI_TEXT.static.configExported, 'info');
|
||||||
|
} else if (result.cancelled) {
|
||||||
|
// User cancelled the dialog — no toast needed.
|
||||||
|
} else if (toast) {
|
||||||
|
toast(UI_TEXT.static.configExportFailed + (result.error ? `\n${result.error}` : ''), 'warn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importConfigFromFile(): Promise<void> {
|
||||||
|
const result = await window.api.importConfig();
|
||||||
|
const toast = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
||||||
|
if (result.success) {
|
||||||
|
// Reload local config copy + refresh forms / streamer list / VOD grid
|
||||||
|
try {
|
||||||
|
config = await window.api.getConfig();
|
||||||
|
if (typeof setLanguage === 'function' && typeof config.language === 'string') {
|
||||||
|
setLanguage(config.language);
|
||||||
|
}
|
||||||
|
if (typeof renderStreamers === 'function') renderStreamers();
|
||||||
|
if (typeof syncSettingsFormFromConfig === 'function') syncSettingsFormFromConfig();
|
||||||
|
if (typeof renderVodGridFromCurrentState === 'function' && lastLoadedStreamer) {
|
||||||
|
renderVodGridFromCurrentState();
|
||||||
|
}
|
||||||
|
} catch { /* ignore — next refresh will catch up */ }
|
||||||
|
if (toast) toast(UI_TEXT.static.configImported, 'info');
|
||||||
|
} else if (result.cancelled) {
|
||||||
|
// User cancelled the dialog — no toast needed.
|
||||||
|
} else if (toast) {
|
||||||
|
toast(UI_TEXT.static.configImportFailed + (result.error ? `\n${result.error}` : ''), 'warn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetDownloadedIds(): Promise<void> {
|
||||||
|
if (!confirm(UI_TEXT.static.resetDownloadedConfirm)) return;
|
||||||
|
const result = await window.api.resetDownloadedVodIds();
|
||||||
|
const toast = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
||||||
|
if (result.success) {
|
||||||
|
// Refresh local config so the badges disappear immediately
|
||||||
|
try {
|
||||||
|
config = await window.api.getConfig();
|
||||||
|
if (typeof renderVodGridFromCurrentState === 'function' && lastLoadedStreamer) {
|
||||||
|
renderVodGridFromCurrentState();
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
if (toast) {
|
||||||
|
toast(UI_TEXT.static.resetDownloadedDone.replace('{count}', String(result.removedCount)), 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openDebugLogFile(): Promise<void> {
|
async function openDebugLogFile(): Promise<void> {
|
||||||
const ok = await window.api.openDebugLogFile();
|
const ok = await window.api.openDebugLogFile();
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
|||||||
@ -16,6 +16,32 @@ const VOD_FILTER_STORAGE_KEY = 'twitch-vod-manager:vod-filter';
|
|||||||
const selectedVodUrls = new Set<string>();
|
const selectedVodUrls = new Set<string>();
|
||||||
let vodGridDelegationInitialized = false;
|
let vodGridDelegationInitialized = false;
|
||||||
|
|
||||||
|
// Hide-downloaded toggle: when enabled, the VOD grid skips entries whose
|
||||||
|
// vod.id is in config.downloaded_vod_ids. Persisted to localStorage so a
|
||||||
|
// power user who keeps it enabled doesn't have to re-flip it every launch.
|
||||||
|
const VOD_HIDE_DOWNLOADED_STORAGE_KEY = 'twitch-vod-manager:vod-hide-downloaded';
|
||||||
|
let vodHideDownloaded = false;
|
||||||
|
|
||||||
|
function loadPersistedHideDownloaded(): boolean {
|
||||||
|
try { return localStorage.getItem(VOD_HIDE_DOWNLOADED_STORAGE_KEY) === '1'; } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistHideDownloaded(value: boolean): void {
|
||||||
|
try { localStorage.setItem(VOD_HIDE_DOWNLOADED_STORAGE_KEY, value ? '1' : '0'); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVodHideDownloadedChange(): void {
|
||||||
|
const cb = byId<HTMLInputElement>('vodHideDownloadedToggle');
|
||||||
|
vodHideDownloaded = cb.checked;
|
||||||
|
persistHideDownloaded(vodHideDownloaded);
|
||||||
|
if (lastLoadedStreamer) renderVodGridFromCurrentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncVodHideDownloadedToggle(): void {
|
||||||
|
const cb = document.getElementById('vodHideDownloadedToggle') as HTMLInputElement | null;
|
||||||
|
if (cb) cb.checked = vodHideDownloaded;
|
||||||
|
}
|
||||||
|
|
||||||
type VodSortKey = 'date_desc' | 'date_asc' | 'views_desc' | 'duration_desc' | 'duration_asc';
|
type VodSortKey = 'date_desc' | 'date_asc' | 'views_desc' | 'duration_desc' | 'duration_asc';
|
||||||
const VALID_VOD_SORTS: ReadonlyArray<VodSortKey> = ['date_desc', 'date_asc', 'views_desc', 'duration_desc', 'duration_asc'];
|
const VALID_VOD_SORTS: ReadonlyArray<VodSortKey> = ['date_desc', 'date_asc', 'views_desc', 'duration_desc', 'duration_asc'];
|
||||||
const VOD_SORT_STORAGE_KEY = 'twitch-vod-manager:vod-sort';
|
const VOD_SORT_STORAGE_KEY = 'twitch-vod-manager:vod-sort';
|
||||||
@ -503,7 +529,15 @@ function renderVodGridFromCurrentState(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sorted = sortVods(lastLoadedVods, vodSortKey);
|
const sorted = sortVods(lastLoadedVods, vodSortKey);
|
||||||
const filtered = filterVodsByQuery(sorted, vodFilterQuery);
|
const downloadedIdsForFilter = new Set(
|
||||||
|
Array.isArray(config.downloaded_vod_ids)
|
||||||
|
? (config.downloaded_vod_ids as string[]).filter((id) => typeof id === 'string')
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
const sortedAndHidden = vodHideDownloaded
|
||||||
|
? sorted.filter((vod) => !downloadedIdsForFilter.has(vod.id))
|
||||||
|
: sorted;
|
||||||
|
const filtered = filterVodsByQuery(sortedAndHidden, vodFilterQuery);
|
||||||
|
|
||||||
if (filtered.length === 0 && vodFilterQuery.trim()) {
|
if (filtered.length === 0 && vodFilterQuery.trim()) {
|
||||||
setVodGridEmptyState(grid, UI_TEXT.vods.filterNoMatchTitle, UI_TEXT.vods.filterNoMatchText);
|
setVodGridEmptyState(grid, UI_TEXT.vods.filterNoMatchTitle, UI_TEXT.vods.filterNoMatchText);
|
||||||
|
|||||||
@ -152,6 +152,13 @@ function applyLanguageToStaticUI(): void {
|
|||||||
setText('debugLogTitle', UI_TEXT.static.debugLogTitle);
|
setText('debugLogTitle', UI_TEXT.static.debugLogTitle);
|
||||||
setText('btnRefreshLog', UI_TEXT.static.refreshLog);
|
setText('btnRefreshLog', UI_TEXT.static.refreshLog);
|
||||||
setText('btnOpenDebugLogFile', UI_TEXT.static.openDebugLogFile);
|
setText('btnOpenDebugLogFile', UI_TEXT.static.openDebugLogFile);
|
||||||
|
setText('backupCardTitle', UI_TEXT.static.backupCardTitle);
|
||||||
|
setText('backupCardIntro', UI_TEXT.static.backupCardIntro);
|
||||||
|
setText('btnExportConfig', UI_TEXT.static.exportConfig);
|
||||||
|
setText('btnImportConfig', UI_TEXT.static.importConfig);
|
||||||
|
setText('btnResetDownloadedIds', UI_TEXT.static.resetDownloadedIds);
|
||||||
|
setText('vodHideDownloadedText', UI_TEXT.vods.hideDownloaded);
|
||||||
|
setTitle('vodHideDownloadedLabel', UI_TEXT.vods.hideDownloadedTitle);
|
||||||
setText('autoRefreshText', UI_TEXT.static.autoRefresh);
|
setText('autoRefreshText', UI_TEXT.static.autoRefresh);
|
||||||
setText('runtimeMetricsTitle', UI_TEXT.static.runtimeMetricsTitle);
|
setText('runtimeMetricsTitle', UI_TEXT.static.runtimeMetricsTitle);
|
||||||
setText('btnRefreshMetrics', UI_TEXT.static.runtimeMetricsRefresh);
|
setText('btnRefreshMetrics', UI_TEXT.static.runtimeMetricsRefresh);
|
||||||
|
|||||||
@ -60,6 +60,10 @@ async function init(): Promise<void> {
|
|||||||
refreshVodSortSelectLabels();
|
refreshVodSortSelectLabels();
|
||||||
syncVodSortSelect();
|
syncVodSortSelect();
|
||||||
|
|
||||||
|
// Restore "hide downloaded" toggle state.
|
||||||
|
vodHideDownloaded = loadPersistedHideDownloaded();
|
||||||
|
syncVodHideDownloadedToggle();
|
||||||
|
|
||||||
// Restore last active tab from previous session (default 'vods')
|
// Restore last active tab from previous session (default 'vods')
|
||||||
showTab(loadPersistedActiveTab());
|
showTab(loadPersistedActiveTab());
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user