Launch v4.0.0 with reliability center and advanced queue controls
Ship a major update that adds in-app preflight diagnostics with health badge, live debug log tooling, retry management, pause/resume downloads, queue reordering support, locale polish (flags + clearer retry wording), and extensive backend hardening for day-to-day stability.
This commit is contained in:
parent
551690d09c
commit
8f44211115
4
typescript-version/package-lock.json
generated
4
typescript-version/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "3.9.0",
|
"version": "4.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "3.9.0",
|
"version": "4.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "3.9.0",
|
"version": "4.0.0",
|
||||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"author": "xRangerDE",
|
"author": "xRangerDE",
|
||||||
|
|||||||
@ -125,12 +125,13 @@
|
|||||||
<div class="queue-section">
|
<div class="queue-section">
|
||||||
<div class="queue-header">
|
<div class="queue-header">
|
||||||
<span class="queue-title" id="queueTitleText">Warteschlange</span>
|
<span class="queue-title" id="queueTitleText">Warteschlange</span>
|
||||||
|
<span class="health-badge unknown" id="healthBadge">System: Unbekannt</span>
|
||||||
<span class="queue-count" id="queueCount">0</span>
|
<span class="queue-count" id="queueCount">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="queue-list" id="queueList"></div>
|
<div class="queue-list" id="queueList"></div>
|
||||||
<div class="queue-actions">
|
<div class="queue-actions">
|
||||||
<button class="btn btn-start" id="btnStart" onclick="toggleDownload()">Start</button>
|
<button class="btn btn-start" id="btnStart" onclick="toggleDownload()">Start</button>
|
||||||
<button class="btn btn-retry" id="btnRetryFailed" onclick="retryFailedDownloads()">Fehler neu</button>
|
<button class="btn btn-retry" id="btnRetryFailed" onclick="retryFailedDownloads()" title="Nur fehlgeschlagene Downloads erneut starten">Wiederholen</button>
|
||||||
<button class="btn btn-clear" id="btnClear" onclick="clearCompleted()">Leeren</button>
|
<button class="btn btn-clear" id="btnClear" onclick="clearCompleted()">Leeren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -300,8 +301,8 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label id="languageLabel">Sprache</label>
|
<label id="languageLabel">Sprache</label>
|
||||||
<select id="languageSelect" onchange="changeLanguage(this.value)">
|
<select id="languageSelect" onchange="changeLanguage(this.value)">
|
||||||
<option value="de" id="languageDeText">Deutsch</option>
|
<option value="de" id="languageDeText">🇩🇪 Deutsch</option>
|
||||||
<option value="en" id="languageEnText">Englisch</option>
|
<option value="en" id="languageEnText">🇺🇸 English</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -344,7 +345,7 @@
|
|||||||
|
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 id="updateTitle">Updates</h3>
|
<h3 id="updateTitle">Updates</h3>
|
||||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.9.0</p>
|
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.0.0</p>
|
||||||
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -376,7 +377,7 @@
|
|||||||
<div class="status-dot" id="statusDot"></div>
|
<div class="status-dot" id="statusDot"></div>
|
||||||
<span id="statusText">Nicht verbunden</span>
|
<span id="statusText">Nicht verbunden</span>
|
||||||
</div>
|
</div>
|
||||||
<span id="versionText">v3.9.0</span>
|
<span id="versionText">v4.0.0</span>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// CONFIG & CONSTANTS
|
// CONFIG & CONSTANTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const APP_VERSION = '3.9.0';
|
const APP_VERSION = '4.0.0';
|
||||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
@ -71,7 +71,7 @@ interface QueueItem {
|
|||||||
date: string;
|
date: string;
|
||||||
streamer: string;
|
streamer: string;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
status: 'pending' | 'downloading' | 'completed' | 'error';
|
status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
|
||||||
progress: number;
|
progress: number;
|
||||||
currentPart?: number;
|
currentPart?: number;
|
||||||
totalParts?: number;
|
totalParts?: number;
|
||||||
@ -190,6 +190,7 @@ let downloadQueue: QueueItem[] = loadQueue();
|
|||||||
let isDownloading = false;
|
let isDownloading = false;
|
||||||
let currentProcess: ChildProcess | null = null;
|
let currentProcess: ChildProcess | null = null;
|
||||||
let currentDownloadCancelled = false;
|
let currentDownloadCancelled = false;
|
||||||
|
let pauseRequested = false;
|
||||||
let downloadStartTime = 0;
|
let downloadStartTime = 0;
|
||||||
let downloadedBytes = 0;
|
let downloadedBytes = 0;
|
||||||
const userIdLoginCache = new Map<string, string>();
|
const userIdLoginCache = new Map<string, string>();
|
||||||
@ -1440,12 +1441,13 @@ async function processQueue(): Promise<void> {
|
|||||||
|
|
||||||
appendDebugLog('queue-start', { items: downloadQueue.length });
|
appendDebugLog('queue-start', { items: downloadQueue.length });
|
||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
|
pauseRequested = false;
|
||||||
mainWindow?.webContents.send('download-started');
|
mainWindow?.webContents.send('download-started');
|
||||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
|
|
||||||
for (const item of downloadQueue) {
|
for (const item of downloadQueue) {
|
||||||
if (!isDownloading) break;
|
if (!isDownloading || pauseRequested) break;
|
||||||
if (item.status === 'completed') continue;
|
if (item.status === 'completed' || item.status === 'error' || item.status === 'paused') continue;
|
||||||
|
|
||||||
appendDebugLog('queue-item-start', { itemId: item.id, title: item.title, url: item.url });
|
appendDebugLog('queue-item-start', { itemId: item.id, title: item.title, url: item.url });
|
||||||
|
|
||||||
@ -1472,8 +1474,8 @@ async function processQueue(): Promise<void> {
|
|||||||
|
|
||||||
finalResult = result;
|
finalResult = result;
|
||||||
|
|
||||||
if (!isDownloading || currentDownloadCancelled) {
|
if (!isDownloading || currentDownloadCancelled || pauseRequested) {
|
||||||
finalResult = { success: false, error: 'Download wurde abgebrochen.' };
|
finalResult = { success: false, error: pauseRequested ? 'Download wurde pausiert.' : 'Download wurde abgebrochen.' };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1494,9 +1496,10 @@ async function processQueue(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item.status = finalResult.success ? 'completed' : 'error';
|
const wasPaused = pauseRequested || (finalResult.error || '').includes('pausiert');
|
||||||
item.progress = finalResult.success ? 100 : 0;
|
item.status = finalResult.success ? 'completed' : (wasPaused ? 'paused' : 'error');
|
||||||
item.last_error = finalResult.success ? '' : (finalResult.error || 'Unbekannter Fehler beim Download');
|
item.progress = finalResult.success ? 100 : item.progress;
|
||||||
|
item.last_error = finalResult.success || wasPaused ? '' : (finalResult.error || 'Unbekannter Fehler beim Download');
|
||||||
appendDebugLog('queue-item-finished', {
|
appendDebugLog('queue-item-finished', {
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
@ -1507,6 +1510,7 @@ async function processQueue(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
|
pauseRequested = false;
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
mainWindow?.webContents.send('download-finished');
|
mainWindow?.webContents.send('download-finished');
|
||||||
@ -1659,6 +1663,20 @@ ipcMain.handle('clear-completed', () => {
|
|||||||
return downloadQueue;
|
return downloadQueue;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('reorder-queue', (_, orderIds: string[]) => {
|
||||||
|
const order = new Map(orderIds.map((id, idx) => [id, idx]));
|
||||||
|
const withOrder = [...downloadQueue].sort((a, b) => {
|
||||||
|
const ai = order.has(a.id) ? (order.get(a.id) as number) : Number.MAX_SAFE_INTEGER;
|
||||||
|
const bi = order.has(b.id) ? (order.get(b.id) as number) : Number.MAX_SAFE_INTEGER;
|
||||||
|
return ai - bi;
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadQueue = withOrder;
|
||||||
|
saveQueue(downloadQueue);
|
||||||
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
|
return downloadQueue;
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('retry-failed-downloads', () => {
|
ipcMain.handle('retry-failed-downloads', () => {
|
||||||
downloadQueue = downloadQueue.map((item) => {
|
downloadQueue = downloadQueue.map((item) => {
|
||||||
if (item.status !== 'error') return item;
|
if (item.status !== 'error') return item;
|
||||||
@ -1682,18 +1700,35 @@ ipcMain.handle('retry-failed-downloads', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('start-download', async () => {
|
ipcMain.handle('start-download', async () => {
|
||||||
const hasPendingItems = downloadQueue.some(item => item.status !== 'completed');
|
downloadQueue = downloadQueue.map((item) => item.status === 'paused' ? { ...item, status: 'pending' } : item);
|
||||||
|
|
||||||
|
const hasPendingItems = downloadQueue.some(item => item.status === 'pending');
|
||||||
if (!hasPendingItems) {
|
if (!hasPendingItems) {
|
||||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveQueue(downloadQueue);
|
||||||
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
|
|
||||||
processQueue();
|
processQueue();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('pause-download', () => {
|
||||||
|
if (!isDownloading) return false;
|
||||||
|
|
||||||
|
pauseRequested = true;
|
||||||
|
currentDownloadCancelled = true;
|
||||||
|
if (currentProcess) {
|
||||||
|
currentProcess.kill();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('cancel-download', () => {
|
ipcMain.handle('cancel-download', () => {
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
|
pauseRequested = false;
|
||||||
currentDownloadCancelled = true;
|
currentDownloadCancelled = true;
|
||||||
if (currentProcess) {
|
if (currentProcess) {
|
||||||
currentProcess.kill();
|
currentProcess.kill();
|
||||||
|
|||||||
@ -15,7 +15,7 @@ interface QueueItem {
|
|||||||
date: string;
|
date: string;
|
||||||
streamer: string;
|
streamer: string;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
status: 'pending' | 'downloading' | 'completed' | 'error';
|
status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
|
||||||
progress: number;
|
progress: number;
|
||||||
currentPart?: number;
|
currentPart?: number;
|
||||||
totalParts?: number;
|
totalParts?: number;
|
||||||
@ -60,11 +60,13 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
getQueue: () => ipcRenderer.invoke('get-queue'),
|
getQueue: () => ipcRenderer.invoke('get-queue'),
|
||||||
addToQueue: (item: Omit<QueueItem, 'id' | 'status' | 'progress'>) => ipcRenderer.invoke('add-to-queue', item),
|
addToQueue: (item: Omit<QueueItem, 'id' | 'status' | 'progress'>) => ipcRenderer.invoke('add-to-queue', item),
|
||||||
removeFromQueue: (id: string) => ipcRenderer.invoke('remove-from-queue', id),
|
removeFromQueue: (id: string) => ipcRenderer.invoke('remove-from-queue', id),
|
||||||
|
reorderQueue: (orderIds: string[]) => ipcRenderer.invoke('reorder-queue', orderIds),
|
||||||
clearCompleted: () => ipcRenderer.invoke('clear-completed'),
|
clearCompleted: () => ipcRenderer.invoke('clear-completed'),
|
||||||
retryFailedDownloads: () => ipcRenderer.invoke('retry-failed-downloads'),
|
retryFailedDownloads: () => ipcRenderer.invoke('retry-failed-downloads'),
|
||||||
|
|
||||||
// Download
|
// Download
|
||||||
startDownload: () => ipcRenderer.invoke('start-download'),
|
startDownload: () => ipcRenderer.invoke('start-download'),
|
||||||
|
pauseDownload: () => ipcRenderer.invoke('pause-download'),
|
||||||
cancelDownload: () => ipcRenderer.invoke('cancel-download'),
|
cancelDownload: () => ipcRenderer.invoke('cancel-download'),
|
||||||
isDownloading: () => ipcRenderer.invoke('is-downloading'),
|
isDownloading: () => ipcRenderer.invoke('is-downloading'),
|
||||||
downloadClip: (url: string) => ipcRenderer.invoke('download-clip', url),
|
downloadClip: (url: string) => ipcRenderer.invoke('download-clip', url),
|
||||||
|
|||||||
4
typescript-version/src/renderer-globals.d.ts
vendored
4
typescript-version/src/renderer-globals.d.ts
vendored
@ -35,7 +35,7 @@ interface QueueItem {
|
|||||||
date: string;
|
date: string;
|
||||||
streamer: string;
|
streamer: string;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
status: 'pending' | 'downloading' | 'completed' | 'error';
|
status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
|
||||||
progress: number;
|
progress: number;
|
||||||
currentPart?: number;
|
currentPart?: number;
|
||||||
totalParts?: number;
|
totalParts?: number;
|
||||||
@ -112,9 +112,11 @@ interface ApiBridge {
|
|||||||
getQueue(): Promise<QueueItem[]>;
|
getQueue(): Promise<QueueItem[]>;
|
||||||
addToQueue(item: Omit<QueueItem, 'id' | 'status' | 'progress'>): Promise<QueueItem[]>;
|
addToQueue(item: Omit<QueueItem, 'id' | 'status' | 'progress'>): Promise<QueueItem[]>;
|
||||||
removeFromQueue(id: string): Promise<QueueItem[]>;
|
removeFromQueue(id: string): Promise<QueueItem[]>;
|
||||||
|
reorderQueue(orderIds: string[]): Promise<QueueItem[]>;
|
||||||
clearCompleted(): Promise<QueueItem[]>;
|
clearCompleted(): Promise<QueueItem[]>;
|
||||||
retryFailedDownloads(): Promise<QueueItem[]>;
|
retryFailedDownloads(): Promise<QueueItem[]>;
|
||||||
startDownload(): Promise<boolean>;
|
startDownload(): Promise<boolean>;
|
||||||
|
pauseDownload(): Promise<boolean>;
|
||||||
cancelDownload(): Promise<boolean>;
|
cancelDownload(): Promise<boolean>;
|
||||||
isDownloading(): Promise<boolean>;
|
isDownloading(): Promise<boolean>;
|
||||||
downloadClip(url: string): Promise<{ success: boolean; error?: string }>;
|
downloadClip(url: string): Promise<{ success: boolean; error?: string }>;
|
||||||
|
|||||||
@ -7,7 +7,12 @@ const UI_TEXT_DE = {
|
|||||||
navMerge: 'Videos zusammenfugen',
|
navMerge: 'Videos zusammenfugen',
|
||||||
navSettings: 'Einstellungen',
|
navSettings: 'Einstellungen',
|
||||||
queueTitle: 'Warteschlange',
|
queueTitle: 'Warteschlange',
|
||||||
retryFailed: 'Fehler neu',
|
retryFailed: 'Wiederholen',
|
||||||
|
retryFailedHint: 'Nur fehlgeschlagene Downloads erneut starten',
|
||||||
|
healthUnknown: 'System: Unbekannt',
|
||||||
|
healthGood: 'System: Stabil',
|
||||||
|
healthWarn: 'System: Warnung',
|
||||||
|
healthBad: 'System: Problem',
|
||||||
clearQueue: 'Leeren',
|
clearQueue: 'Leeren',
|
||||||
refresh: 'Aktualisieren',
|
refresh: 'Aktualisieren',
|
||||||
streamerPlaceholder: 'Streamer hinzufugen...',
|
streamerPlaceholder: 'Streamer hinzufugen...',
|
||||||
@ -22,8 +27,8 @@ const UI_TEXT_DE = {
|
|||||||
designTitle: 'Design',
|
designTitle: 'Design',
|
||||||
themeLabel: 'Theme',
|
themeLabel: 'Theme',
|
||||||
languageLabel: 'Sprache',
|
languageLabel: 'Sprache',
|
||||||
languageDe: 'DE - Deutsch',
|
languageDe: '🇩🇪 Deutsch',
|
||||||
languageEn: 'EN - Englisch',
|
languageEn: '🇺🇸 Englisch',
|
||||||
apiTitle: 'Twitch API',
|
apiTitle: 'Twitch API',
|
||||||
clientIdLabel: 'Client ID',
|
clientIdLabel: 'Client ID',
|
||||||
clientSecretLabel: 'Client Secret',
|
clientSecretLabel: 'Client Secret',
|
||||||
@ -41,6 +46,14 @@ const UI_TEXT_DE = {
|
|||||||
preflightRun: 'Check ausfuhren',
|
preflightRun: 'Check ausfuhren',
|
||||||
preflightFix: 'Auto-Fix Tools',
|
preflightFix: 'Auto-Fix Tools',
|
||||||
preflightEmpty: 'Noch kein Check ausgefuhrt.',
|
preflightEmpty: 'Noch kein Check ausgefuhrt.',
|
||||||
|
preflightChecking: 'Prufe...',
|
||||||
|
preflightFixing: 'Fixe...',
|
||||||
|
preflightReady: 'Alles bereit.',
|
||||||
|
preflightInternet: 'Internet',
|
||||||
|
preflightStreamlink: 'Streamlink',
|
||||||
|
preflightFfmpeg: 'FFmpeg',
|
||||||
|
preflightFfprobe: 'FFprobe',
|
||||||
|
preflightPath: 'Download-Pfad',
|
||||||
debugLogTitle: 'Live Debug-Log',
|
debugLogTitle: 'Live Debug-Log',
|
||||||
refreshLog: 'Aktualisieren',
|
refreshLog: 'Aktualisieren',
|
||||||
autoRefresh: 'Auto-Refresh',
|
autoRefresh: 'Auto-Refresh',
|
||||||
@ -62,10 +75,12 @@ const UI_TEXT_DE = {
|
|||||||
queue: {
|
queue: {
|
||||||
empty: 'Keine Downloads in der Warteschlange',
|
empty: 'Keine Downloads in der Warteschlange',
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
stop: 'Stoppen',
|
stop: 'Pausieren',
|
||||||
|
resume: 'Fortsetzen',
|
||||||
statusDone: 'Abgeschlossen',
|
statusDone: 'Abgeschlossen',
|
||||||
statusFailed: 'Fehlgeschlagen',
|
statusFailed: 'Fehlgeschlagen',
|
||||||
statusRunning: 'Laeuft',
|
statusRunning: 'Laeuft',
|
||||||
|
statusPaused: 'Pausiert',
|
||||||
statusWaiting: 'Wartet',
|
statusWaiting: 'Wartet',
|
||||||
progressError: 'Fehler',
|
progressError: 'Fehler',
|
||||||
progressReady: 'Bereit',
|
progressReady: 'Bereit',
|
||||||
|
|||||||
@ -7,7 +7,12 @@ const UI_TEXT_EN = {
|
|||||||
navMerge: 'Merge Videos',
|
navMerge: 'Merge Videos',
|
||||||
navSettings: 'Settings',
|
navSettings: 'Settings',
|
||||||
queueTitle: 'Queue',
|
queueTitle: 'Queue',
|
||||||
retryFailed: 'Retry failed',
|
retryFailed: 'Retry',
|
||||||
|
retryFailedHint: 'Retry failed downloads only',
|
||||||
|
healthUnknown: 'System: Unknown',
|
||||||
|
healthGood: 'System: Stable',
|
||||||
|
healthWarn: 'System: Warning',
|
||||||
|
healthBad: 'System: Problem',
|
||||||
clearQueue: 'Clear',
|
clearQueue: 'Clear',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
streamerPlaceholder: 'Add streamer...',
|
streamerPlaceholder: 'Add streamer...',
|
||||||
@ -22,8 +27,8 @@ const UI_TEXT_EN = {
|
|||||||
designTitle: 'Design',
|
designTitle: 'Design',
|
||||||
themeLabel: 'Theme',
|
themeLabel: 'Theme',
|
||||||
languageLabel: 'Language',
|
languageLabel: 'Language',
|
||||||
languageDe: 'DE - German',
|
languageDe: '🇩🇪 German',
|
||||||
languageEn: 'EN - English',
|
languageEn: '🇺🇸 English',
|
||||||
apiTitle: 'Twitch API',
|
apiTitle: 'Twitch API',
|
||||||
clientIdLabel: 'Client ID',
|
clientIdLabel: 'Client ID',
|
||||||
clientSecretLabel: 'Client Secret',
|
clientSecretLabel: 'Client Secret',
|
||||||
@ -41,6 +46,14 @@ const UI_TEXT_EN = {
|
|||||||
preflightRun: 'Run check',
|
preflightRun: 'Run check',
|
||||||
preflightFix: 'Auto-fix tools',
|
preflightFix: 'Auto-fix tools',
|
||||||
preflightEmpty: 'No checks run yet.',
|
preflightEmpty: 'No checks run yet.',
|
||||||
|
preflightChecking: 'Checking...',
|
||||||
|
preflightFixing: 'Fixing...',
|
||||||
|
preflightReady: 'Everything is ready.',
|
||||||
|
preflightInternet: 'Internet',
|
||||||
|
preflightStreamlink: 'Streamlink',
|
||||||
|
preflightFfmpeg: 'FFmpeg',
|
||||||
|
preflightFfprobe: 'FFprobe',
|
||||||
|
preflightPath: 'Download path',
|
||||||
debugLogTitle: 'Live Debug Log',
|
debugLogTitle: 'Live Debug Log',
|
||||||
refreshLog: 'Refresh',
|
refreshLog: 'Refresh',
|
||||||
autoRefresh: 'Auto refresh',
|
autoRefresh: 'Auto refresh',
|
||||||
@ -62,10 +75,12 @@ const UI_TEXT_EN = {
|
|||||||
queue: {
|
queue: {
|
||||||
empty: 'No downloads in queue',
|
empty: 'No downloads in queue',
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
stop: 'Stop',
|
stop: 'Pause',
|
||||||
|
resume: 'Resume',
|
||||||
statusDone: 'Completed',
|
statusDone: 'Completed',
|
||||||
statusFailed: 'Failed',
|
statusFailed: 'Failed',
|
||||||
statusRunning: 'Running',
|
statusRunning: 'Running',
|
||||||
|
statusPaused: 'Paused',
|
||||||
statusWaiting: 'Waiting',
|
statusWaiting: 'Waiting',
|
||||||
progressError: 'Error',
|
progressError: 'Error',
|
||||||
progressReady: 'Ready',
|
progressReady: 'Ready',
|
||||||
|
|||||||
@ -27,6 +27,7 @@ async function retryFailedDownloads(): Promise<void> {
|
|||||||
function getQueueStatusLabel(item: QueueItem): string {
|
function getQueueStatusLabel(item: QueueItem): string {
|
||||||
if (item.status === 'completed') return UI_TEXT.queue.statusDone;
|
if (item.status === 'completed') return UI_TEXT.queue.statusDone;
|
||||||
if (item.status === 'error') return UI_TEXT.queue.statusFailed;
|
if (item.status === 'error') return UI_TEXT.queue.statusFailed;
|
||||||
|
if (item.status === 'paused') return UI_TEXT.queue.statusPaused;
|
||||||
if (item.status === 'downloading') return UI_TEXT.queue.statusRunning;
|
if (item.status === 'downloading') return UI_TEXT.queue.statusRunning;
|
||||||
return UI_TEXT.queue.statusWaiting;
|
return UI_TEXT.queue.statusWaiting;
|
||||||
}
|
}
|
||||||
@ -34,6 +35,7 @@ function getQueueStatusLabel(item: QueueItem): string {
|
|||||||
function getQueueProgressText(item: QueueItem): string {
|
function getQueueProgressText(item: QueueItem): string {
|
||||||
if (item.status === 'completed') return '100%';
|
if (item.status === 'completed') return '100%';
|
||||||
if (item.status === 'error') return UI_TEXT.queue.progressError;
|
if (item.status === 'error') return UI_TEXT.queue.progressError;
|
||||||
|
if (item.status === 'paused') return UI_TEXT.queue.progressReady;
|
||||||
if (item.status === 'pending') return UI_TEXT.queue.progressReady;
|
if (item.status === 'pending') return UI_TEXT.queue.progressReady;
|
||||||
if (item.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`;
|
if (item.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`;
|
||||||
return item.progressStatus || UI_TEXT.queue.progressLoading;
|
return item.progressStatus || UI_TEXT.queue.progressLoading;
|
||||||
@ -62,6 +64,10 @@ function getQueueMetaText(item: QueueItem): string {
|
|||||||
parts.push(UI_TEXT.queue.readyToDownload);
|
parts.push(UI_TEXT.queue.readyToDownload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!parts.length && item.status === 'paused') {
|
||||||
|
parts.push(UI_TEXT.queue.statusPaused);
|
||||||
|
}
|
||||||
|
|
||||||
if (!parts.length && item.status === 'downloading') {
|
if (!parts.length && item.status === 'downloading') {
|
||||||
parts.push(item.progressStatus || UI_TEXT.queue.started);
|
parts.push(item.progressStatus || UI_TEXT.queue.started);
|
||||||
}
|
}
|
||||||
@ -84,6 +90,9 @@ function renderQueue(): void {
|
|||||||
|
|
||||||
const list = byId('queueList');
|
const list = byId('queueList');
|
||||||
byId('queueCount').textContent = String(queue.length);
|
byId('queueCount').textContent = String(queue.length);
|
||||||
|
const retryBtn = byId<HTMLButtonElement>('btnRetryFailed');
|
||||||
|
const hasFailed = queue.some((item) => item.status === 'error');
|
||||||
|
retryBtn.disabled = !hasFailed;
|
||||||
|
|
||||||
if (queue.length === 0) {
|
if (queue.length === 0) {
|
||||||
list.innerHTML = `<div style="color: var(--text-secondary); font-size: 12px; text-align: center; padding: 15px;">${UI_TEXT.queue.empty}</div>`;
|
list.innerHTML = `<div style="color: var(--text-secondary); font-size: 12px; text-align: center; padding: 15px;">${UI_TEXT.queue.empty}</div>`;
|
||||||
@ -124,7 +133,7 @@ function renderQueue(): void {
|
|||||||
|
|
||||||
async function toggleDownload(): Promise<void> {
|
async function toggleDownload(): Promise<void> {
|
||||||
if (downloading) {
|
if (downloading) {
|
||||||
await window.api.cancelDownload();
|
await window.api.pauseDownload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,24 +42,42 @@ function changeLanguage(lang: string): void {
|
|||||||
|
|
||||||
function renderPreflightResult(result: PreflightResult): void {
|
function renderPreflightResult(result: PreflightResult): void {
|
||||||
const entries = [
|
const entries = [
|
||||||
['Internet', result.checks.internet],
|
[UI_TEXT.static.preflightInternet, result.checks.internet],
|
||||||
['Streamlink', result.checks.streamlink],
|
[UI_TEXT.static.preflightStreamlink, result.checks.streamlink],
|
||||||
['FFmpeg', result.checks.ffmpeg],
|
[UI_TEXT.static.preflightFfmpeg, result.checks.ffmpeg],
|
||||||
['FFprobe', result.checks.ffprobe],
|
[UI_TEXT.static.preflightFfprobe, result.checks.ffprobe],
|
||||||
['Download-Pfad', result.checks.downloadPathWritable]
|
[UI_TEXT.static.preflightPath, result.checks.downloadPathWritable]
|
||||||
];
|
];
|
||||||
|
|
||||||
const lines = entries.map(([name, ok]) => `${ok ? 'OK' : 'FAIL'} ${name}`).join('\n');
|
const lines = entries.map(([name, ok]) => `${ok ? 'OK' : 'FAIL'} ${name}`).join('\n');
|
||||||
const extra = result.messages.length ? `\n\n${result.messages.join('\n')}` : '\n\nAlles bereit.';
|
const extra = result.messages.length ? `\n\n${result.messages.join('\n')}` : `\n\n${UI_TEXT.static.preflightReady}`;
|
||||||
|
|
||||||
byId('preflightResult').textContent = `${lines}${extra}`;
|
byId('preflightResult').textContent = `${lines}${extra}`;
|
||||||
|
|
||||||
|
const badge = byId('healthBadge');
|
||||||
|
badge.classList.remove('good', 'warn', 'bad', 'unknown');
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
badge.classList.add('good');
|
||||||
|
badge.textContent = UI_TEXT.static.healthGood;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const failCount = Object.values(result.checks).filter((ok) => !ok).length;
|
||||||
|
if (failCount <= 2) {
|
||||||
|
badge.classList.add('warn');
|
||||||
|
badge.textContent = UI_TEXT.static.healthWarn;
|
||||||
|
} else {
|
||||||
|
badge.classList.add('bad');
|
||||||
|
badge.textContent = UI_TEXT.static.healthBad;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runPreflight(autoFix = false): Promise<void> {
|
async function runPreflight(autoFix = false): Promise<void> {
|
||||||
const btn = byId<HTMLButtonElement>(autoFix ? 'btnPreflightFix' : 'btnPreflightRun');
|
const btn = byId<HTMLButtonElement>(autoFix ? 'btnPreflightFix' : 'btnPreflightRun');
|
||||||
const old = btn.textContent || '';
|
const old = btn.textContent || '';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = autoFix ? 'Fixe...' : 'Prufe...';
|
btn.textContent = autoFix ? UI_TEXT.static.preflightFixing : UI_TEXT.static.preflightChecking;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.api.runPreflight(autoFix);
|
const result = await window.api.runPreflight(autoFix);
|
||||||
|
|||||||
@ -39,3 +39,4 @@ let clipTotalSeconds = 0;
|
|||||||
|
|
||||||
let updateReady = false;
|
let updateReady = false;
|
||||||
let debugLogAutoRefreshTimer: number | null = null;
|
let debugLogAutoRefreshTimer: number | null = null;
|
||||||
|
let draggedQueueItemId: string | null = null;
|
||||||
|
|||||||
@ -31,6 +31,11 @@ function setPlaceholder(id: string, value: string): void {
|
|||||||
if (node) node.placeholder = value;
|
if (node) node.placeholder = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTitle(id: string, value: string): void {
|
||||||
|
const node = document.getElementById(id);
|
||||||
|
if (node) node.setAttribute('title', value);
|
||||||
|
}
|
||||||
|
|
||||||
function setLanguage(lang: string): LanguageCode {
|
function setLanguage(lang: string): LanguageCode {
|
||||||
currentLanguage = lang === 'en' ? 'en' : 'de';
|
currentLanguage = lang === 'en' ? 'en' : 'de';
|
||||||
UI_TEXT = UI_TEXTS[currentLanguage];
|
UI_TEXT = UI_TEXTS[currentLanguage];
|
||||||
@ -46,7 +51,9 @@ function applyLanguageToStaticUI(): void {
|
|||||||
setText('navMergeText', UI_TEXT.static.navMerge);
|
setText('navMergeText', UI_TEXT.static.navMerge);
|
||||||
setText('navSettingsText', UI_TEXT.static.navSettings);
|
setText('navSettingsText', UI_TEXT.static.navSettings);
|
||||||
setText('queueTitleText', UI_TEXT.static.queueTitle);
|
setText('queueTitleText', UI_TEXT.static.queueTitle);
|
||||||
|
setText('healthBadge', UI_TEXT.static.healthUnknown);
|
||||||
setText('btnRetryFailed', UI_TEXT.static.retryFailed);
|
setText('btnRetryFailed', UI_TEXT.static.retryFailed);
|
||||||
|
setTitle('btnRetryFailed', UI_TEXT.static.retryFailedHint);
|
||||||
setText('btnClear', UI_TEXT.static.clearQueue);
|
setText('btnClear', UI_TEXT.static.clearQueue);
|
||||||
setText('refreshText', UI_TEXT.static.refresh);
|
setText('refreshText', UI_TEXT.static.refresh);
|
||||||
setText('clipsHeading', UI_TEXT.static.clipsHeading);
|
setText('clipsHeading', UI_TEXT.static.clipsHeading);
|
||||||
|
|||||||
@ -117,7 +117,8 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] {
|
|||||||
|
|
||||||
function updateDownloadButtonState(): void {
|
function updateDownloadButtonState(): void {
|
||||||
const btn = byId('btnStart');
|
const btn = byId('btnStart');
|
||||||
btn.textContent = downloading ? UI_TEXT.queue.stop : UI_TEXT.queue.start;
|
const hasPaused = queue.some((item) => item.status === 'paused');
|
||||||
|
btn.textContent = downloading ? UI_TEXT.queue.stop : (hasPaused ? UI_TEXT.queue.resume : UI_TEXT.queue.start);
|
||||||
btn.classList.toggle('downloading', downloading);
|
btn.classList.toggle('downloading', downloading);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -179,6 +179,7 @@ body {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-title {
|
.queue-title {
|
||||||
@ -199,6 +200,33 @@ body {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.health-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-badge.good {
|
||||||
|
background: rgba(0, 200, 83, 0.2);
|
||||||
|
border-color: rgba(0, 200, 83, 0.45);
|
||||||
|
color: #93efb9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-badge.warn {
|
||||||
|
background: rgba(255, 171, 0, 0.2);
|
||||||
|
border-color: rgba(255, 171, 0, 0.45);
|
||||||
|
color: #ffd98e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-badge.bad,
|
||||||
|
.health-badge.unknown {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
border-color: rgba(255, 68, 68, 0.45);
|
||||||
|
color: #ffaaaa;
|
||||||
|
}
|
||||||
|
|
||||||
.queue-item {
|
.queue-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user