diff --git a/src/index.html b/src/index.html index b3f398b..b017a0e 100644 --- a/src/index.html +++ b/src/index.html @@ -314,19 +314,19 @@ @@ -339,11 +339,11 @@
- +
- +
@@ -574,6 +574,7 @@
Nicht verbunden + v4.1.13 diff --git a/src/main.ts b/src/main.ts index c633aa4..3a9ad54 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3841,6 +3841,31 @@ ipcMain.handle('retry-failed-downloads', () => { return downloadQueue; }); +ipcMain.handle('retry-queue-item', (_, id: string) => { + if (typeof id !== 'string' || !id) return downloadQueue; + const idx = downloadQueue.findIndex((it) => it.id === id); + if (idx < 0) return downloadQueue; + const item = downloadQueue[idx]; + if (item.status !== 'error') return downloadQueue; + + downloadQueue[idx] = { + ...item, + status: 'pending', + progress: 0, + last_error: '' + }; + + saveQueue(downloadQueue); + emitQueueUpdated(); + appendDebugLog('queue-item-retry-single', { id, title: item.title }); + + if (!isDownloading) { + void processQueue(); + } + + return downloadQueue; +}); + ipcMain.handle('create-merge-group', (_, itemIds: string[]) => { const selectedItems = downloadQueue.filter(item => itemIds.includes(item.id)); diff --git a/src/preload.ts b/src/preload.ts index a6cdc2c..d921e82 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -68,6 +68,7 @@ contextBridge.exposeInMainWorld('api', { reorderQueue: (orderIds: string[]) => ipcRenderer.invoke('reorder-queue', orderIds), clearCompleted: () => ipcRenderer.invoke('clear-completed'), retryFailedDownloads: () => ipcRenderer.invoke('retry-failed-downloads'), + retryQueueItem: (id: string) => ipcRenderer.invoke('retry-queue-item', id), createMergeGroup: (itemIds: string[]) => ipcRenderer.invoke('create-merge-group', itemIds), // Download diff --git a/src/renderer-globals.d.ts b/src/renderer-globals.d.ts index 6c6357b..c6aef4c 100644 --- a/src/renderer-globals.d.ts +++ b/src/renderer-globals.d.ts @@ -187,6 +187,7 @@ interface ApiBridge { reorderQueue(orderIds: string[]): Promise; clearCompleted(): Promise; retryFailedDownloads(): Promise; + retryQueueItem(id: string): Promise; createMergeGroup(itemIds: string[]): Promise; startDownload(): Promise; pauseDownload(): Promise; diff --git a/src/renderer-locale-de.ts b/src/renderer-locale-de.ts index 086c9f0..9df250a 100644 --- a/src/renderer-locale-de.ts +++ b/src/renderer-locale-de.ts @@ -160,7 +160,9 @@ const UI_TEXT_DE = { openFile: 'Datei oeffnen', showInFolder: 'Im Ordner zeigen', openFileFailed: 'Datei konnte nicht geoeffnet werden (evtl. verschoben oder geloescht).', - outputFilesLabel: '{count} Ausgabedateien' + outputFilesLabel: '{count} Ausgabedateien', + retryItem: 'Diesen Eintrag erneut versuchen', + statusBarSummary: '{downloading} aktiv, {pending} wartet' }, vods: { noneTitle: 'Keine VODs', @@ -227,7 +229,13 @@ const UI_TEXT_DE = { cutting: 'Schneidet...', cut: 'Schneiden', cutSuccess: 'Video erfolgreich geschnitten!', - cutFailed: 'Fehler beim Schneiden des Videos.' + cutFailed: 'Fehler beim Schneiden des Videos.', + infoDuration: 'Dauer', + infoResolution: 'Aufloesung', + infoFps: 'FPS', + infoSelection: 'Auswahl', + startLabel: 'Start:', + endLabel: 'Ende:' }, merge: { empty: 'Keine Videos ausgewahlt', diff --git a/src/renderer-locale-en.ts b/src/renderer-locale-en.ts index 3ceaf00..d39e0a6 100644 --- a/src/renderer-locale-en.ts +++ b/src/renderer-locale-en.ts @@ -160,7 +160,9 @@ const UI_TEXT_EN = { openFile: 'Open file', showInFolder: 'Show in folder', openFileFailed: 'Could not open the file (it may have been moved or deleted).', - outputFilesLabel: '{count} output files' + outputFilesLabel: '{count} output files', + retryItem: 'Retry this item', + statusBarSummary: '{downloading} dl, {pending} queued' }, vods: { noneTitle: 'No VODs', @@ -227,7 +229,13 @@ const UI_TEXT_EN = { cutting: 'Cutting...', cut: 'Cut', cutSuccess: 'Video cut successfully!', - cutFailed: 'Failed to cut video.' + cutFailed: 'Failed to cut video.', + infoDuration: 'Duration', + infoResolution: 'Resolution', + infoFps: 'FPS', + infoSelection: 'Selection', + startLabel: 'Start:', + endLabel: 'End:' }, merge: { empty: 'No videos selected', diff --git a/src/renderer-queue.ts b/src/renderer-queue.ts index 36a0805..d9681b6 100644 --- a/src/renderer-queue.ts +++ b/src/renderer-queue.ts @@ -127,6 +127,11 @@ async function retryFailedDownloads(): Promise { renderQueue(); } +async function retryQueueItem(id: string): Promise { + queue = await window.api.retryQueueItem(id); + renderQueue(); +} + function getQueueStatusLabel(item: QueueItem): string { if (item.status === 'completed') return UI_TEXT.queue.statusDone; if (item.status === 'error') return UI_TEXT.queue.statusFailed; @@ -382,6 +387,7 @@ function renderQueue(): void { ${renderQueueItemFileActions(item)} + ${item.status === 'error' ? `` : ''} x `; diff --git a/src/renderer-texts.ts b/src/renderer-texts.ts index 8b141d9..c5b31c4 100644 --- a/src/renderer-texts.ts +++ b/src/renderer-texts.ts @@ -72,9 +72,17 @@ function applyLanguageToStaticUI(): void { setText('clipDialogConfirmBtn', UI_TEXT.clips.dialogConfirm); setText('cutterSelectTitle', UI_TEXT.static.cutterSelectTitle); setText('cutterBrowseBtn', UI_TEXT.static.cutterBrowse); + setText('cutterInfoDurationLabel', UI_TEXT.cutter.infoDuration); + setText('cutterInfoResolutionLabel', UI_TEXT.cutter.infoResolution); + setText('cutterInfoFpsLabel', UI_TEXT.cutter.infoFps); + setText('cutterInfoSelectionLabel', UI_TEXT.cutter.infoSelection); + setText('cutterStartLabel', UI_TEXT.cutter.startLabel); + setText('cutterEndLabel', UI_TEXT.cutter.endLabel); + setText('btnCut', UI_TEXT.cutter.cut); setText('mergeTitle', UI_TEXT.static.mergeTitle); setText('mergeDesc', UI_TEXT.static.mergeDesc); setText('mergeAddBtn', UI_TEXT.static.mergeAdd); + setText('btnMerge', UI_TEXT.merge.merge); setText('designTitle', UI_TEXT.static.designTitle); setText('themeLabel', UI_TEXT.static.themeLabel); setText('themeLightOption', UI_TEXT.static.themeLight); diff --git a/src/renderer.ts b/src/renderer.ts index 8810bd2..242a7a6 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -44,6 +44,7 @@ async function init(): Promise { renderQueue(); initQueueDragDrop(); updateDownloadButtonState(); + updateStatusBarQueueSummary(); // Restore persisted VOD filter into the input — the filter itself only // takes effect once VODs load (renderVODs reads vodFilterQuery). @@ -65,6 +66,7 @@ async function init(): Promise { window.api.onQueueUpdated((q: QueueItem[]) => { queue = mergeQueueState(Array.isArray(q) ? q : []); renderQueue(); + updateStatusBarQueueSummary(); markQueueActivity(); }); @@ -89,6 +91,7 @@ async function init(): Promise { item.totalBytes = progress.totalBytes; item.progressStatus = progress.status; updateQueueItemProgress(progress); + updateStatusBarQueueSummary(); markQueueActivity(); }); @@ -248,6 +251,31 @@ function formatSpeedRenderer(bytesPerSec: number): string { return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`; } +function updateStatusBarQueueSummary(): void { + const node = document.getElementById('statusBarQueueSummary'); + if (!node) return; + if (!Array.isArray(queue) || queue.length === 0) { + node.textContent = ''; + return; + } + + let downloading = 0; + let pending = 0; + for (const item of queue) { + if (item.status === 'downloading') downloading++; + else if (item.status === 'pending') pending++; + } + + if (downloading === 0 && pending === 0) { + node.textContent = ''; + return; + } + + node.textContent = UI_TEXT.queue.statusBarSummary + .replace('{downloading}', String(downloading)) + .replace('{pending}', String(pending)); +} + async function updateStatsBar(): Promise { try { const metrics = await window.api.getRuntimeMetrics();