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",
|
||||
"version": "3.9.0",
|
||||
"version": "4.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "3.9.0",
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "3.9.0",
|
||||
"version": "4.0.0",
|
||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||
"main": "dist/main.js",
|
||||
"author": "xRangerDE",
|
||||
|
||||
@ -125,12 +125,13 @@
|
||||
<div class="queue-section">
|
||||
<div class="queue-header">
|
||||
<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>
|
||||
</div>
|
||||
<div class="queue-list" id="queueList"></div>
|
||||
<div class="queue-actions">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -300,8 +301,8 @@
|
||||
<div class="form-group">
|
||||
<label id="languageLabel">Sprache</label>
|
||||
<select id="languageSelect" onchange="changeLanguage(this.value)">
|
||||
<option value="de" id="languageDeText">Deutsch</option>
|
||||
<option value="en" id="languageEnText">Englisch</option>
|
||||
<option value="de" id="languageDeText">🇩🇪 Deutsch</option>
|
||||
<option value="en" id="languageEnText">🇺🇸 English</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -344,7 +345,7 @@
|
||||
|
||||
<div class="settings-card">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -376,7 +377,7 @@
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Nicht verbunden</span>
|
||||
</div>
|
||||
<span id="versionText">v3.9.0</span>
|
||||
<span id="versionText">v4.0.0</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
|
||||
// ==========================================
|
||||
// CONFIG & CONSTANTS
|
||||
// ==========================================
|
||||
const APP_VERSION = '3.9.0';
|
||||
const APP_VERSION = '4.0.0';
|
||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||
|
||||
// Paths
|
||||
@ -71,7 +71,7 @@ interface QueueItem {
|
||||
date: string;
|
||||
streamer: string;
|
||||
duration_str: string;
|
||||
status: 'pending' | 'downloading' | 'completed' | 'error';
|
||||
status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
|
||||
progress: number;
|
||||
currentPart?: number;
|
||||
totalParts?: number;
|
||||
@ -190,6 +190,7 @@ let downloadQueue: QueueItem[] = loadQueue();
|
||||
let isDownloading = false;
|
||||
let currentProcess: ChildProcess | null = null;
|
||||
let currentDownloadCancelled = false;
|
||||
let pauseRequested = false;
|
||||
let downloadStartTime = 0;
|
||||
let downloadedBytes = 0;
|
||||
const userIdLoginCache = new Map<string, string>();
|
||||
@ -1440,12 +1441,13 @@ async function processQueue(): Promise<void> {
|
||||
|
||||
appendDebugLog('queue-start', { items: downloadQueue.length });
|
||||
isDownloading = true;
|
||||
pauseRequested = false;
|
||||
mainWindow?.webContents.send('download-started');
|
||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||
|
||||
for (const item of downloadQueue) {
|
||||
if (!isDownloading) break;
|
||||
if (item.status === 'completed') continue;
|
||||
if (!isDownloading || pauseRequested) break;
|
||||
if (item.status === 'completed' || item.status === 'error' || item.status === 'paused') continue;
|
||||
|
||||
appendDebugLog('queue-item-start', { itemId: item.id, title: item.title, url: item.url });
|
||||
|
||||
@ -1472,8 +1474,8 @@ async function processQueue(): Promise<void> {
|
||||
|
||||
finalResult = result;
|
||||
|
||||
if (!isDownloading || currentDownloadCancelled) {
|
||||
finalResult = { success: false, error: 'Download wurde abgebrochen.' };
|
||||
if (!isDownloading || currentDownloadCancelled || pauseRequested) {
|
||||
finalResult = { success: false, error: pauseRequested ? 'Download wurde pausiert.' : 'Download wurde abgebrochen.' };
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1494,9 +1496,10 @@ async function processQueue(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
item.status = finalResult.success ? 'completed' : 'error';
|
||||
item.progress = finalResult.success ? 100 : 0;
|
||||
item.last_error = finalResult.success ? '' : (finalResult.error || 'Unbekannter Fehler beim Download');
|
||||
const wasPaused = pauseRequested || (finalResult.error || '').includes('pausiert');
|
||||
item.status = finalResult.success ? 'completed' : (wasPaused ? 'paused' : 'error');
|
||||
item.progress = finalResult.success ? 100 : item.progress;
|
||||
item.last_error = finalResult.success || wasPaused ? '' : (finalResult.error || 'Unbekannter Fehler beim Download');
|
||||
appendDebugLog('queue-item-finished', {
|
||||
itemId: item.id,
|
||||
status: item.status,
|
||||
@ -1507,6 +1510,7 @@ async function processQueue(): Promise<void> {
|
||||
}
|
||||
|
||||
isDownloading = false;
|
||||
pauseRequested = false;
|
||||
saveQueue(downloadQueue);
|
||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||
mainWindow?.webContents.send('download-finished');
|
||||
@ -1659,6 +1663,20 @@ ipcMain.handle('clear-completed', () => {
|
||||
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', () => {
|
||||
downloadQueue = downloadQueue.map((item) => {
|
||||
if (item.status !== 'error') return item;
|
||||
@ -1682,18 +1700,35 @@ ipcMain.handle('retry-failed-downloads', () => {
|
||||
});
|
||||
|
||||
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) {
|
||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||
return false;
|
||||
}
|
||||
|
||||
saveQueue(downloadQueue);
|
||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||
|
||||
processQueue();
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('pause-download', () => {
|
||||
if (!isDownloading) return false;
|
||||
|
||||
pauseRequested = true;
|
||||
currentDownloadCancelled = true;
|
||||
if (currentProcess) {
|
||||
currentProcess.kill();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('cancel-download', () => {
|
||||
isDownloading = false;
|
||||
pauseRequested = false;
|
||||
currentDownloadCancelled = true;
|
||||
if (currentProcess) {
|
||||
currentProcess.kill();
|
||||
|
||||
@ -15,7 +15,7 @@ interface QueueItem {
|
||||
date: string;
|
||||
streamer: string;
|
||||
duration_str: string;
|
||||
status: 'pending' | 'downloading' | 'completed' | 'error';
|
||||
status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
|
||||
progress: number;
|
||||
currentPart?: number;
|
||||
totalParts?: number;
|
||||
@ -60,11 +60,13 @@ contextBridge.exposeInMainWorld('api', {
|
||||
getQueue: () => ipcRenderer.invoke('get-queue'),
|
||||
addToQueue: (item: Omit<QueueItem, 'id' | 'status' | 'progress'>) => ipcRenderer.invoke('add-to-queue', item),
|
||||
removeFromQueue: (id: string) => ipcRenderer.invoke('remove-from-queue', id),
|
||||
reorderQueue: (orderIds: string[]) => ipcRenderer.invoke('reorder-queue', orderIds),
|
||||
clearCompleted: () => ipcRenderer.invoke('clear-completed'),
|
||||
retryFailedDownloads: () => ipcRenderer.invoke('retry-failed-downloads'),
|
||||
|
||||
// Download
|
||||
startDownload: () => ipcRenderer.invoke('start-download'),
|
||||
pauseDownload: () => ipcRenderer.invoke('pause-download'),
|
||||
cancelDownload: () => ipcRenderer.invoke('cancel-download'),
|
||||
isDownloading: () => ipcRenderer.invoke('is-downloading'),
|
||||
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;
|
||||
streamer: string;
|
||||
duration_str: string;
|
||||
status: 'pending' | 'downloading' | 'completed' | 'error';
|
||||
status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
|
||||
progress: number;
|
||||
currentPart?: number;
|
||||
totalParts?: number;
|
||||
@ -112,9 +112,11 @@ interface ApiBridge {
|
||||
getQueue(): Promise<QueueItem[]>;
|
||||
addToQueue(item: Omit<QueueItem, 'id' | 'status' | 'progress'>): Promise<QueueItem[]>;
|
||||
removeFromQueue(id: string): Promise<QueueItem[]>;
|
||||
reorderQueue(orderIds: string[]): Promise<QueueItem[]>;
|
||||
clearCompleted(): Promise<QueueItem[]>;
|
||||
retryFailedDownloads(): Promise<QueueItem[]>;
|
||||
startDownload(): Promise<boolean>;
|
||||
pauseDownload(): Promise<boolean>;
|
||||
cancelDownload(): Promise<boolean>;
|
||||
isDownloading(): Promise<boolean>;
|
||||
downloadClip(url: string): Promise<{ success: boolean; error?: string }>;
|
||||
|
||||
@ -7,7 +7,12 @@ const UI_TEXT_DE = {
|
||||
navMerge: 'Videos zusammenfugen',
|
||||
navSettings: 'Einstellungen',
|
||||
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',
|
||||
refresh: 'Aktualisieren',
|
||||
streamerPlaceholder: 'Streamer hinzufugen...',
|
||||
@ -22,8 +27,8 @@ const UI_TEXT_DE = {
|
||||
designTitle: 'Design',
|
||||
themeLabel: 'Theme',
|
||||
languageLabel: 'Sprache',
|
||||
languageDe: 'DE - Deutsch',
|
||||
languageEn: 'EN - Englisch',
|
||||
languageDe: '🇩🇪 Deutsch',
|
||||
languageEn: '🇺🇸 Englisch',
|
||||
apiTitle: 'Twitch API',
|
||||
clientIdLabel: 'Client ID',
|
||||
clientSecretLabel: 'Client Secret',
|
||||
@ -41,6 +46,14 @@ const UI_TEXT_DE = {
|
||||
preflightRun: 'Check ausfuhren',
|
||||
preflightFix: 'Auto-Fix Tools',
|
||||
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',
|
||||
refreshLog: 'Aktualisieren',
|
||||
autoRefresh: 'Auto-Refresh',
|
||||
@ -62,10 +75,12 @@ const UI_TEXT_DE = {
|
||||
queue: {
|
||||
empty: 'Keine Downloads in der Warteschlange',
|
||||
start: 'Start',
|
||||
stop: 'Stoppen',
|
||||
stop: 'Pausieren',
|
||||
resume: 'Fortsetzen',
|
||||
statusDone: 'Abgeschlossen',
|
||||
statusFailed: 'Fehlgeschlagen',
|
||||
statusRunning: 'Laeuft',
|
||||
statusPaused: 'Pausiert',
|
||||
statusWaiting: 'Wartet',
|
||||
progressError: 'Fehler',
|
||||
progressReady: 'Bereit',
|
||||
|
||||
@ -7,7 +7,12 @@ const UI_TEXT_EN = {
|
||||
navMerge: 'Merge Videos',
|
||||
navSettings: 'Settings',
|
||||
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',
|
||||
refresh: 'Refresh',
|
||||
streamerPlaceholder: 'Add streamer...',
|
||||
@ -22,8 +27,8 @@ const UI_TEXT_EN = {
|
||||
designTitle: 'Design',
|
||||
themeLabel: 'Theme',
|
||||
languageLabel: 'Language',
|
||||
languageDe: 'DE - German',
|
||||
languageEn: 'EN - English',
|
||||
languageDe: '🇩🇪 German',
|
||||
languageEn: '🇺🇸 English',
|
||||
apiTitle: 'Twitch API',
|
||||
clientIdLabel: 'Client ID',
|
||||
clientSecretLabel: 'Client Secret',
|
||||
@ -41,6 +46,14 @@ const UI_TEXT_EN = {
|
||||
preflightRun: 'Run check',
|
||||
preflightFix: 'Auto-fix tools',
|
||||
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',
|
||||
refreshLog: 'Refresh',
|
||||
autoRefresh: 'Auto refresh',
|
||||
@ -62,10 +75,12 @@ const UI_TEXT_EN = {
|
||||
queue: {
|
||||
empty: 'No downloads in queue',
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
stop: 'Pause',
|
||||
resume: 'Resume',
|
||||
statusDone: 'Completed',
|
||||
statusFailed: 'Failed',
|
||||
statusRunning: 'Running',
|
||||
statusPaused: 'Paused',
|
||||
statusWaiting: 'Waiting',
|
||||
progressError: 'Error',
|
||||
progressReady: 'Ready',
|
||||
|
||||
@ -27,6 +27,7 @@ async function retryFailedDownloads(): Promise<void> {
|
||||
function getQueueStatusLabel(item: QueueItem): string {
|
||||
if (item.status === 'completed') return UI_TEXT.queue.statusDone;
|
||||
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;
|
||||
return UI_TEXT.queue.statusWaiting;
|
||||
}
|
||||
@ -34,6 +35,7 @@ function getQueueStatusLabel(item: QueueItem): string {
|
||||
function getQueueProgressText(item: QueueItem): string {
|
||||
if (item.status === 'completed') return '100%';
|
||||
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.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`;
|
||||
return item.progressStatus || UI_TEXT.queue.progressLoading;
|
||||
@ -62,6 +64,10 @@ function getQueueMetaText(item: QueueItem): string {
|
||||
parts.push(UI_TEXT.queue.readyToDownload);
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'paused') {
|
||||
parts.push(UI_TEXT.queue.statusPaused);
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'downloading') {
|
||||
parts.push(item.progressStatus || UI_TEXT.queue.started);
|
||||
}
|
||||
@ -84,6 +90,9 @@ function renderQueue(): void {
|
||||
|
||||
const list = byId('queueList');
|
||||
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) {
|
||||
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> {
|
||||
if (downloading) {
|
||||
await window.api.cancelDownload();
|
||||
await window.api.pauseDownload();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -42,24 +42,42 @@ function changeLanguage(lang: string): void {
|
||||
|
||||
function renderPreflightResult(result: PreflightResult): void {
|
||||
const entries = [
|
||||
['Internet', result.checks.internet],
|
||||
['Streamlink', result.checks.streamlink],
|
||||
['FFmpeg', result.checks.ffmpeg],
|
||||
['FFprobe', result.checks.ffprobe],
|
||||
['Download-Pfad', result.checks.downloadPathWritable]
|
||||
[UI_TEXT.static.preflightInternet, result.checks.internet],
|
||||
[UI_TEXT.static.preflightStreamlink, result.checks.streamlink],
|
||||
[UI_TEXT.static.preflightFfmpeg, result.checks.ffmpeg],
|
||||
[UI_TEXT.static.preflightFfprobe, result.checks.ffprobe],
|
||||
[UI_TEXT.static.preflightPath, result.checks.downloadPathWritable]
|
||||
];
|
||||
|
||||
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}`;
|
||||
|
||||
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> {
|
||||
const btn = byId<HTMLButtonElement>(autoFix ? 'btnPreflightFix' : 'btnPreflightRun');
|
||||
const old = btn.textContent || '';
|
||||
btn.disabled = true;
|
||||
btn.textContent = autoFix ? 'Fixe...' : 'Prufe...';
|
||||
btn.textContent = autoFix ? UI_TEXT.static.preflightFixing : UI_TEXT.static.preflightChecking;
|
||||
|
||||
try {
|
||||
const result = await window.api.runPreflight(autoFix);
|
||||
|
||||
@ -39,3 +39,4 @@ let clipTotalSeconds = 0;
|
||||
|
||||
let updateReady = false;
|
||||
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;
|
||||
}
|
||||
|
||||
function setTitle(id: string, value: string): void {
|
||||
const node = document.getElementById(id);
|
||||
if (node) node.setAttribute('title', value);
|
||||
}
|
||||
|
||||
function setLanguage(lang: string): LanguageCode {
|
||||
currentLanguage = lang === 'en' ? 'en' : 'de';
|
||||
UI_TEXT = UI_TEXTS[currentLanguage];
|
||||
@ -46,7 +51,9 @@ function applyLanguageToStaticUI(): void {
|
||||
setText('navMergeText', UI_TEXT.static.navMerge);
|
||||
setText('navSettingsText', UI_TEXT.static.navSettings);
|
||||
setText('queueTitleText', UI_TEXT.static.queueTitle);
|
||||
setText('healthBadge', UI_TEXT.static.healthUnknown);
|
||||
setText('btnRetryFailed', UI_TEXT.static.retryFailed);
|
||||
setTitle('btnRetryFailed', UI_TEXT.static.retryFailedHint);
|
||||
setText('btnClear', UI_TEXT.static.clearQueue);
|
||||
setText('refreshText', UI_TEXT.static.refresh);
|
||||
setText('clipsHeading', UI_TEXT.static.clipsHeading);
|
||||
|
||||
@ -117,7 +117,8 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] {
|
||||
|
||||
function updateDownloadButtonState(): void {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -179,6 +179,7 @@ body {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.queue-title {
|
||||
@ -199,6 +200,33 @@ body {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user