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 @@
- Dauer
+ Dauer
--:--:--
- Auflosung
+ Aufloesung
----x----
- FPS
+ FPS
--
- Auswahl
+ Auswahl
--:--:--
@@ -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();