fix(queue): honor visible order and keep transfer metrics on one line
Windows CI / verify (push) Canceled after 0s

This commit is contained in:
Sucukdeluxe
2026-09-07 03:04:12 +02:00
parent 3ec56c45e4
commit 6d721d88be
17 changed files with 124 additions and 65 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## Unreleased
- Start pending downloads in the visible queue order, including manually reordered entries. Retire the automatic short-job prioritization, including for existing configurations.
- Keep transfer size, percentage, speed and remaining time on one compact line below 1 GB as well. Preserve wrapping for error messages and full values in tooltips.
## 1.0.22 - 2026-09-06
- Show small round profile pictures beside streamer names in the sidebar, with an initial fallback when an image is unavailable.
+11
View File
@@ -8,6 +8,8 @@ Am 6. September 2026 nach einem Festplatten-Reset aus den vorhandenen Remote-Rep
Aktueller veröffentlichter Stand: **1.0.22**, Tag `v1.0.22`, veröffentlicht am 6. September 2026 auf GitHub und Forgejo. Der Tag zeigt auf `a526bd3541fd1d909dae0060ba92d64347668099`; nachfolgende Memory-Commits gehören nicht zum Installer.
Unveröffentlichte Änderungen vom 7. September 2026: Queue startet wartende Downloads strikt in der sichtbaren Reihenfolge; kompakte, einzeilige Fortschrittsanzeige auch unter 1 GB.
## Git und maßgeblicher Stand
- Arbeitsbranch: `public-v1`, Tracking: `origin/public-v1`.
@@ -21,6 +23,8 @@ Aktueller veröffentlichter Stand: **1.0.22**, Tag `v1.0.22`, veröffentlicht am
## Letzte Änderungen
- Release 1.0.22 abgeschlossen: runde Profilbilder in der Streamerliste; auf GitHub und Forgejo mit identischen Assets sowie englischem/deutschem Changelog veröffentlicht und geprüft.
- 7. September 2026: Automatische Bevorzugung kurzer Downloads/Clips entfernt, damit die Queue von oben nach unten startet und manuelles Umsortieren respektiert. Die frühere Scheduler-Einstellung ist aus der Oberfläche entfernt; vorhandene Konfigurationen werden auf deaktiviert normalisiert, das alte Feld bleibt kompatibel lesbar. Parallele Downloads werden weiterhin in sichtbarer Reihenfolge gestartet.
- 7. September 2026: Transferzeile zeigt die Datenmenge ohne redundantes „heruntergeladen“, daneben Prozent, Tempo und Restzeit in 11 px ohne Zeilenumbruch. Vollständige Angaben bleiben im Tooltip, Fehlermeldungen weiterhin mehrzeilig in 12 px. 500 MB, 1003 MB und 1,35 GB in beiden Sprachen und Themes ohne abgeschnittene Werte gemessen.
- Linke Streamerliste zeigt runde Profilbilder in 24 × 24 px direkt vor dem Namen. Verwendet den bestehenden Profil-Cache und dessen Vorladen/Hintergrundaktualisierung ohne zusätzliche Profilanfragen. Bilder werden in der vorhandenen Zeile nachgeladen und erhalten den Tastaturfokus; fehlende oder defekte Bilder zeigen den Anfangsbuchstaben. Bildklick wählt den Streamer, lange Namen werden einzeilig gekürzt und bleiben im Tooltip lesbar. Build, 37 gezielte Tests und vollständige Workspace-UI-Prüfung inklusive Laden, Fallback, Auswahl und Hell/Dunkel erfolgreich; Lint ohne Fehler bei 15 bestehenden Warnungen. Screenshots unter `tmp_streamer-avatar-artifacts/`. Mit 1.0.22 veröffentlicht, auch im Hot-Dev aktiv.
- Release 1.0.21 abgeschlossen: alle Queue-Nachbesserungen seit 1.0.20 enthalten; auf GitHub und Forgejo mit identischen Assets sowie englischem/deutschem Changelog veröffentlicht und geprüft.
- Streamer in Queue-Details verwendet jetzt den aufgelösten Twitch-Anzeigenamen mit originaler Groß-/Kleinschreibung, etwa `xRohat` statt `xrohat`. Gespeicherter Login bleibt unverändert; noch unbekannte Anzeigenamen fallen auf den Login zurück. Nachgeladene Profilnamen aktualisieren vorhandene Queue-Cards sofort und werden bei der Render-Erkennung berücksichtigt. Build, 43 gezielte Tests, Queue-UI-Test einschließlich nachgeladener Groß-/Kleinschreibung und Lint erfolgreich (15 bestehende Warnungen). Mit 1.0.21 veröffentlicht.
@@ -145,3 +149,10 @@ Abschließende Verifikation von Release 1.0.22 am 6. September 2026:
- Alle Assets von beiden Plattformen heruntergeladen und bytegenau per SHA-256/SHA-512 gegen den lokalen Build geprüft. Nach Veröffentlichung öffentliche Asset-URLs, Latest-Release und Update-Metadaten beider Plattformen einschließlich GitHubs Update-Kanal erfolgreich verifiziert.
- Installer: 110.441.936 Bytes, SHA-256 `bbfc1ab5ac2e7e7d0d5b5e86c7c757ab2c3e03e854a4e0a830156283af2131ba`. Blockmap: 117.025 Bytes. Update-Metadaten: 368 Bytes mit passender Version, Dateigröße und SHA-512-Prüfsumme.
- Keine produktiven Datenbankänderungen und keine vollständige Live-Update-Installation einer alten Version auf diesem Rechner. Hot-Dev bleibt aktiv. Ignorierte Prüfdateien liegen in `tmp_release_1.0.22/`, Testprotokolle im Windows-Temp-Verzeichnis.
7. September 2026, Queue-Reihenfolge und Transferzeile (noch nicht veröffentlicht):
- Build und alle 635 Unit-Tests in 83 Testdateien erfolgreich. Neuer Produktionstest prüft die sichtbare Startreihenfolge trotz früher aktivierter Priorisierung, manuelles Umsortieren und das Überspringen nicht wartender Einträge.
- Queue-UI-Prüfung in Deutsch/Englisch und Hell/Dunkel einschließlich einzeiliger Mengen-/Tempo-/Restzeitanzeige erfolgreich; Screenshots visuell geprüft. Der Animationstest liest Transitions nun direkt nach dem Umschalten aus, damit langsame Frames die kurze Animation nicht vor der Messung beenden.
- Vollständiger Workspace-UI-Test und isolierter Einstellungen-Autosave-Test erfolgreich, keine gemeldeten Laufzeitfehler. Die Checkbox-Layoutprüfung verwendet nach Entfernen der Scheduler-Option den vorhandenen Duplikatschutz.
- Lint ohne Fehler bei 15 bestehenden Warnungen; Security-/Public-Manifest-Prüfung erfolgreich. Keine Live-Downloads oder produktiven Datenbankänderungen für die Tests. Release-Version bleibt 1.0.22; Änderungen auf public-v1 für die nächste Veröffentlichung vorbereitet.
+23 -2
View File
@@ -79,6 +79,29 @@ async function main() {
assert(card.overflow <= 1, `Card overflow: ${JSON.stringify(card)}`);
}
await win.locator('.queue-section').screenshot({ path: path.join(artifacts, `queue-${theme}-${language}.png`) });
for (const size of ['500.0 MB', '1003.0 MB', '1.35 GB']) {
const transfer = await win.evaluate(({ size, language }) => {
const item = queue.find((candidate) => candidate.id === 'running');
item.progress = 32.2;
item.progressStatus = `${size} ${language === 'de' ? 'heruntergeladen' : 'downloaded'}`;
item.speed = '23.1 MB/s';
item.eta = '12m 34s';
updateQueueItemProgress({ id: item.id });
const row = document.querySelector('[data-id="running"] .queue-progress-info');
const status = row.querySelector('.queue-progress-status');
const metrics = row.querySelector('.queue-progress-metrics');
return {
height: row.getBoundingClientRect().height,
sameLine: status.getBoundingClientRect().top === metrics.getBoundingClientRect().top,
statusVisible: status.scrollWidth <= status.clientWidth,
metricsVisible: metrics.scrollWidth <= metrics.clientWidth,
status: status.textContent,
tooltip: status.title,
};
}, { size, language });
assert(transfer.height <= 18 && transfer.sameLine && transfer.statusVisible && transfer.metricsVisible && transfer.status === size && transfer.tooltip.includes(size), `Transfer line (${theme}/${language}): ${JSON.stringify(transfer)}`);
}
await win.locator('[data-id="running"]').screenshot({ path: path.join(artifacts, `transfer-${theme}-${language}.png`) });
const expansionLayout = await win.locator('#queueList').evaluate(async (list) => {
const items = [...list.querySelectorAll('.queue-item')];
const originalStyles = [list, ...items].map((element) => element.getAttribute('style'));
@@ -139,8 +162,6 @@ async function main() {
const details = item.querySelector('.queue-details');
const frame = () => new Promise((resolve) => requestAnimationFrame(resolve));
const sample = async () => {
await frame();
await frame();
const animations = details.getAnimations();
const slide = animations.find((animation) => animation.transitionProperty === 'grid-template-rows');
if (!slide) throw new Error('Queue details have no height transition');
+2 -2
View File
@@ -979,14 +979,14 @@ async function run() {
window.setSettingsPane('downloads');
window.changeLanguage('de');
updateStatus(UI_TEXT.status.noLogin, false, 'public');
document.getElementById('smartSchedulerToggle').checked = true;
document.getElementById('duplicatePreventionToggle').checked = true;
});
await win.waitForTimeout(240);
const downloadSettingsWide = await win.evaluate(() => {
const tab = document.getElementById('settingsTab');
const card = tab?.querySelector('.settings-card[data-settings-pane="downloads"]');
const layout = card?.querySelector('.download-settings-layout');
const checkbox = card?.querySelector('#smartSchedulerToggle');
const checkbox = card?.querySelector('#duplicatePreventionToggle');
const label = checkbox?.closest('.toggle-row')?.querySelector('span');
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
-1
View File
@@ -900,7 +900,6 @@
<section class="download-settings-section">
<h4 id="queueAutomationTitle">Queue und Automatisierung</h4>
<div class="download-settings-toggles">
<label class="toggle-row"><input type="checkbox" id="smartSchedulerToggle" checked><span id="smartSchedulerLabel">Smart Queue Scheduler aktivieren</span></label>
<label class="toggle-row"><input type="checkbox" id="duplicatePreventionToggle" checked><span id="duplicatePreventionLabel">Duplikate in Queue verhindern</span></label>
<label class="toggle-row"><input type="checkbox" id="persistQueueToggle" checked><span id="persistQueueLabel">Queue zwischen App-Starts speichern</span></label>
<label class="toggle-row"><input type="checkbox" id="autoResumeQueueToggle"><span id="autoResumeQueueLabel">Queue beim Start automatisch fortsetzen</span></label>
+6 -38
View File
@@ -447,7 +447,7 @@ const defaultConfig: Config = {
filename_template_vod: DEFAULT_FILENAME_TEMPLATE_VOD,
filename_template_parts: DEFAULT_FILENAME_TEMPLATE_PARTS,
filename_template_clip: DEFAULT_FILENAME_TEMPLATE_CLIP,
smart_queue_scheduler: true,
smart_queue_scheduler: false,
performance_mode: DEFAULT_PERFORMANCE_MODE,
prevent_duplicate_downloads: true,
persist_queue_on_restart: true,
@@ -521,7 +521,7 @@ function normalizeConfigTemplates(input: Config): Config {
filename_template_parts: normalizeFilenameTemplate(input.filename_template_parts, DEFAULT_FILENAME_TEMPLATE_PARTS),
filename_template_clip: normalizeFilenameTemplate(input.filename_template_clip, DEFAULT_FILENAME_TEMPLATE_CLIP),
sidebar_split_view: input.sidebar_split_view !== false,
smart_queue_scheduler: input.smart_queue_scheduler !== false,
smart_queue_scheduler: false,
performance_mode: normalizePerformanceMode(input.performance_mode),
prevent_duplicate_downloads: input.prevent_duplicate_downloads !== false,
persist_queue_on_restart: input.persist_queue_on_restart !== false,
@@ -1786,7 +1786,7 @@ function getRuntimeMetricsSnapshot(): RuntimeMetricsSnapshot {
},
config: {
performanceMode: normalizePerformanceMode(config.performance_mode),
smartScheduler: config.smart_queue_scheduler !== false,
smartScheduler: false,
metadataCacheMinutes: normalizeMetadataCacheMinutes(config.metadata_cache_minutes),
duplicatePrevention: config.prevent_duplicate_downloads !== false
}
@@ -1810,39 +1810,8 @@ function hasActiveDuplicate(candidate: Pick<QueueItem, 'url' | 'streamer' | 'dat
});
}
function getQueuePriorityScore(item: QueueItem): number {
const now = Date.now();
const createdMs = getQueueCreatedAtMs(item, now);
const waitSeconds = Math.max(0, Math.floor((now - createdMs) / 1000));
const durationSeconds = Math.max(0, parseDuration(item.duration_str || '0s'));
const clipBoost = item.customClip ? 1500 : 0;
const shortJobBoost = Math.max(0, 7200 - Math.min(7200, durationSeconds)) / 5;
const ageBoost = Math.min(waitSeconds, 1800) / 2;
return clipBoost + shortJobBoost + ageBoost;
}
function pickNextPendingQueueItem(): QueueItem | null {
const pendingItems = downloadQueue.filter((item) => item.status === 'pending');
if (!pendingItems.length) return null;
if (!config.smart_queue_scheduler) {
return pendingItems[0];
}
let best = pendingItems[0];
let bestScore = getQueuePriorityScore(best);
for (let i = 1; i < pendingItems.length; i += 1) {
const candidate = pendingItems[i];
const score = getQueuePriorityScore(candidate);
if (score > bestScore) {
best = candidate;
bestScore = score;
}
}
return best;
return downloadQueue.find((item) => item.status === 'pending') || null;
}
function parseClockDurationSeconds(duration: string | null): number | null {
@@ -6824,8 +6793,7 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
appendDebugLog('queue-item-start', {
itemId: item.id,
title: item.title,
url: item.url,
smartScore: config.smart_queue_scheduler ? getQueuePriorityScore(item) : 0
url: item.url
});
runtimeMetrics.downloadsStarted += 1;
@@ -7107,7 +7075,7 @@ async function processQueue(manualOverride = false): Promise<void> {
appendDebugLog('queue-start', {
items: downloadQueue.length,
smartScheduler: config.smart_queue_scheduler,
scheduling: 'queue-order',
performanceMode: config.performance_mode,
parallelDownloads: config.parallel_downloads || 1
});
@@ -1,8 +1,36 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { runInNewContext } from 'node:vm';
import { ModuleKind, ScriptTarget, transpileModule } from 'typescript';
describe('queue addition IPC contract', () => {
it('starts pending downloads in visible order even with a legacy smart scheduler setting', () => {
const source = readFileSync(join(process.cwd(), 'src', 'main.ts'), 'utf8');
const start = source.indexOf('function pickNextPendingQueueItem');
const end = source.indexOf('function parseClockDurationSeconds', start);
const context = {
config: { smart_queue_scheduler: true },
downloadQueue: [
{ id: 'finished', status: 'completed' },
{ id: 'long-first', status: 'pending', duration_str: '10h', createdAt: '2026-09-07T10:00:00Z' },
{ id: 'short-second', status: 'pending', duration_str: '1m', customClip: { durationSec: 60 }, createdAt: '2026-09-07T10:01:00Z' },
],
};
const code = transpileModule(`${source.slice(start, end)}\nglobalThis.pickNext = pickNextPendingQueueItem;`, {
compilerOptions: { module: ModuleKind.None, target: ScriptTarget.ES2022 },
}).outputText;
const runtime = runInNewContext(`${code}\nglobalThis`, context);
expect(runtime.pickNext().id).toBe('long-first');
context.downloadQueue[1].status = 'downloading';
expect(runtime.pickNext().id).toBe('short-second');
context.downloadQueue[1].status = 'pending';
context.downloadQueue.reverse();
expect(runtime.pickNext().id).toBe('short-second');
context.downloadQueue.forEach((item) => { item.status = 'paused'; });
expect(runtime.pickNext()).toBeNull();
});
it('keeps the legacy queue result and exposes the atomic accepted result separately', () => {
const source = readFileSync(join(process.cwd(), 'src', 'main.ts'), 'utf8');
-2
View File
@@ -88,8 +88,6 @@ const UI_TEXT_DE = {
performanceModeStability: 'Max Stabilität',
performanceModeBalanced: 'Ausgewogen',
performanceModeSpeed: 'Max Geschwindigkeit',
smartSchedulerLabel: 'Smart Queue Scheduler aktivieren',
smartSchedulerHint: 'Bevorzugt kürzere VODs und ältere Queue-Einträge zuerst, damit der Durchsatz gleichmäßig bleibt. Deaktivieren = strikte Einfügereihenfolge.',
sidebarSplitViewLabel: 'Streamer und Queue gleichzeitig anzeigen',
sidebarSplitViewHint: 'Teilt die linke Seitenleiste gleichmäßig auf. Deaktiviert erscheinen wieder die Umschaltflächen.',
streamerInvalid: 'Twitch-Username ungültig (4-25 Zeichen, Buchstaben/Zahlen/Unterstrich).',
-2
View File
@@ -88,8 +88,6 @@ const UI_TEXT_EN = {
performanceModeStability: 'Max Stability',
performanceModeBalanced: 'Balanced',
performanceModeSpeed: 'Max Speed',
smartSchedulerLabel: 'Enable smart queue scheduler',
smartSchedulerHint: 'Prefers shorter VODs and older queue entries first so the queue throughput stays steady. Disable to drain in strict insertion order.',
sidebarSplitViewLabel: 'Show streamers and queue together',
sidebarSplitViewHint: 'Splits the left sidebar evenly. Disable it to restore the Streamer and Queue switches.',
streamerInvalid: 'Invalid Twitch username (4-25 chars, letters/digits/underscore).',
@@ -413,6 +413,17 @@ describe('renderer queue production paths', () => {
expect(metrics({ status: 'completed', progress: 100, speed: '4 MB/s', eta: '2m' })).toBe('100%');
});
it('keeps transfer sizes compact while preserving non-transfer status messages', () => {
const runtime = evaluate(fragment('function getQueueProgressStatusText', 'function getQueueProgressMetricsText'), 'getQueueProgressStatusText', { UI_TEXT: { queue: queueText } });
for (const size of ['500.0 MB', '1003.0 MB', '1.35 GB']) {
for (const suffix of ['downloaded', 'heruntergeladen']) {
expect(runtime.exposed.getQueueProgressStatusText({ status: 'downloading', progressStatus: `${size} ${suffix}` })).toBe(size);
}
}
expect(runtime.exposed.getQueueProgressStatusText({ status: 'downloading', progressStatus: 'Preparing download tools' })).toBe('Preparing download tools');
expect(runtime.exposed.getQueueProgressStatusText({ status: 'error', last_error: 'Connection failed. Please retry.' })).toBe('Connection failed. Please retry.');
});
it('keeps monotonic progress while treating explicit empty telemetry as an authoritative reset', () => {
const mergePath = rendererSource.slice(
rendererSource.indexOf('function mergeQueueState'),
+14 -5
View File
@@ -458,7 +458,10 @@ function getQueueProgressStatusText(item: QueueItem): string {
if (item.status === 'paused') return UI_TEXT.queue.statusPaused;
if (item.status === 'completed') return UI_TEXT.queue.done;
if (item.status === 'error') return UI_TEXT.queue.failed;
if (item.status === 'downloading' && item.progressStatus) return item.progressStatus;
if (item.status === 'downloading' && item.progressStatus) {
const downloadedSize = item.progressStatus.match(/^([\d.,]+\s(?:B|KB|MB|GB|TB)) (?:downloaded|heruntergeladen)$/);
return downloadedSize ? downloadedSize[1] : item.progressStatus;
}
if (item.currentPart && item.totalParts) {
return `${UI_TEXT.queue.part} ${item.currentPart}/${item.totalParts}`;
}
@@ -565,8 +568,14 @@ function updateQueueItemProgress(progress: DownloadProgress): void {
bar.className = 'queue-progress-bar';
if (wrap) wrap.setAttribute('aria-valuenow', String(Math.round(pct)));
}
if (status) status.textContent = getQueueProgressStatusText(item);
if (metrics) metrics.textContent = getQueueProgressMetricsText(item);
if (status) {
status.textContent = getQueueProgressStatusText(item);
status.title = item.progressStatus || status.textContent;
}
if (metrics) {
metrics.textContent = getQueueProgressMetricsText(item);
metrics.title = metrics.textContent;
}
syncQueueRecordingHealth(el, item);
}
@@ -727,8 +736,8 @@ function renderQueue(): void {
<span class="queue-status-badge" title="${safeProgressStatus}"><span class="status ${item.status}" aria-hidden="true"></span><span class="queue-status-label">${safeStatusLabel}</span></span>
</div>
<div class="queue-progress-info${item.status === 'pending' || item.status === 'completed' ? ' is-hidden' : ''}">
<span class="queue-progress-status${progressStatusClass}${item.status === 'paused' ? ' is-hidden' : ''}">${safeProgressStatus}</span>
<span class="queue-progress-metrics">${safeProgressMetrics}</span>
<span class="queue-progress-status${progressStatusClass}${item.status === 'paused' ? ' is-hidden' : ''}" title="${escapeHtml(item.progressStatus || getQueueProgressStatusText(item))}">${safeProgressStatus}</span>
<span class="queue-progress-metrics" title="${safeProgressMetrics}">${safeProgressMetrics}</span>
</div>
<div class="queue-details${expandedQueueIds.has(item.id) ? ' expanded' : ''}" id="${detailsId}"${expandedQueueIds.has(item.id) ? '' : ' inert'}>
<div class="queue-details-clip"><div class="queue-details-content">
+1 -1
View File
@@ -11,7 +11,7 @@ type Input = {
const inputIds = [
'clientId', 'clientSecret', 'sidebarSplitViewToggle', 'downloadMode', 'partMinutes', 'parallelDownloads',
'performanceMode', 'smartSchedulerToggle', 'duplicatePreventionToggle', 'persistQueueToggle',
'performanceMode', 'duplicatePreventionToggle', 'persistQueueToggle',
'autoResumeQueueToggle', 'notifyEachCompletionToggle', 'streamlinkDisableAdsToggle', 'downloadChatReplayToggle',
'captureLiveChatToggle', 'logStreamEventsToggle', 'autoResumeLiveRecordingToggle', 'autoMergeResumedPartsToggle',
'deletePartsAfterMergeToggle', 'discordWebhookUrl', 'discordNotifyLiveStartToggle', 'discordNotifyLiveEndToggle',
-4
View File
@@ -900,7 +900,6 @@ function collectDownloadSettingsPayload(): Partial<AppConfig> {
part_minutes: parseInt(byId<HTMLInputElement>('partMinutes').value, 10) || 120,
parallel_downloads: parseInt(byId<HTMLSelectElement>('parallelDownloads').value, 10) || 1,
performance_mode: byId<HTMLSelectElement>('performanceMode').value as 'stability' | 'balanced' | 'speed',
smart_queue_scheduler: byId<HTMLInputElement>('smartSchedulerToggle').checked,
prevent_duplicate_downloads: byId<HTMLInputElement>('duplicatePreventionToggle').checked,
persist_queue_on_restart: byId<HTMLInputElement>('persistQueueToggle').checked,
auto_resume_queue_on_startup: byId<HTMLInputElement>('autoResumeQueueToggle').checked,
@@ -965,7 +964,6 @@ function getSettingsFingerprint(payload: Partial<AppConfig>): string {
effective.part_minutes ?? 120,
effective.parallel_downloads ?? 1,
effective.performance_mode ?? 'balanced',
effective.smart_queue_scheduler !== false,
effective.prevent_duplicate_downloads !== false,
effective.persist_queue_on_restart !== false,
effective.auto_resume_queue_on_startup === true,
@@ -1007,7 +1005,6 @@ function syncSettingsFormFromConfig(syncSecrets = true): void {
byId<HTMLInputElement>('partMinutes').value = String((config.part_minutes as number) || 120);
byId<HTMLSelectElement>('parallelDownloads').value = String((config.parallel_downloads as number) || 1);
byId<HTMLSelectElement>('performanceMode').value = (config.performance_mode as string) || 'balanced';
byId<HTMLInputElement>('smartSchedulerToggle').checked = (config.smart_queue_scheduler as boolean) !== false;
byId<HTMLInputElement>('duplicatePreventionToggle').checked = (config.prevent_duplicate_downloads as boolean) !== false;
byId<HTMLInputElement>('persistQueueToggle').checked = (config.persist_queue_on_restart as boolean) !== false;
byId<HTMLInputElement>('autoResumeQueueToggle').checked = (config.auto_resume_queue_on_startup as boolean) === true;
@@ -1151,7 +1148,6 @@ function initSettingsAutoSave(): void {
'sidebarSplitViewToggle',
'parallelDownloads',
'performanceMode',
'smartSchedulerToggle',
'duplicatePreventionToggle',
'persistQueueToggle',
'autoResumeQueueToggle',
-3
View File
@@ -269,9 +269,6 @@ function applyLanguageToStaticUI(): void {
setText('performanceModeStability', UI_TEXT.static.performanceModeStability);
setText('performanceModeBalanced', UI_TEXT.static.performanceModeBalanced);
setText('performanceModeSpeed', UI_TEXT.static.performanceModeSpeed);
setText('smartSchedulerLabel', UI_TEXT.static.smartSchedulerLabel);
setTitle('smartSchedulerLabel', UI_TEXT.static.smartSchedulerHint);
setTitle('smartSchedulerToggle', UI_TEXT.static.smartSchedulerHint);
setText('duplicatePreventionLabel', UI_TEXT.static.duplicatePreventionLabel);
setText('persistQueueLabel', UI_TEXT.static.persistQueueLabel);
setText('autoResumeQueueLabel', UI_TEXT.static.autoResumeQueueLabel);
-1
View File
@@ -35,7 +35,6 @@ async function init(): Promise<void> {
byId<HTMLSelectElement>('downloadMode').value = config.download_mode ?? 'full';
byId<HTMLInputElement>('partMinutes').value = String(config.part_minutes ?? 120);
byId<HTMLSelectElement>('performanceMode').value = (config.performance_mode as string) || 'balanced';
byId<HTMLInputElement>('smartSchedulerToggle').checked = (config.smart_queue_scheduler as boolean) !== false;
byId<HTMLInputElement>('duplicatePreventionToggle').checked = (config.prevent_duplicate_downloads as boolean) !== false;
byId<HTMLInputElement>('metadataCacheMinutes').value = String((config.metadata_cache_minutes as number) || 10);
byId<HTMLInputElement>('vodFilenameTemplate').value = (config.filename_template_vod as string) || DEFAULT_VOD_TEMPLATE;
+1 -1
View File
@@ -20,7 +20,7 @@ describe('production style modules', () => {
.replace(/\r\n/g, '\n'));
const digest = createHash('sha256').update(content).digest('hex');
expect(digest).toBe('ece1a9e9f064bc42b0797cd15fcdcdc6367530a67f7c8f6b72c9e10253d73f9a');
expect(digest).toBe('674178288cf7695d7981ebc6f9360a4b7043c9eb66a44b363c830dd8b413114c');
});
test('derives the Windows hot-development executable version from package metadata', () => {
+22 -3
View File
@@ -280,10 +280,10 @@
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
gap: 4px;
margin-top: 3px;
flex-wrap: wrap;
font-size: 12px;
flex-wrap: nowrap;
font-size: 11px;
color: var(--text-secondary);
line-height: 18px;
}
@@ -291,7 +291,24 @@
.queue-progress-status,
.queue-progress-metrics {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.queue-progress-status {
flex: 1;
}
.queue-item:has(.status.error) .queue-progress-info {
flex-wrap: wrap;
}
.queue-item:has(.status.error) .queue-progress-status {
flex-basis: 100%;
white-space: normal;
overflow-wrap: anywhere;
font-size: 12px;
}
.queue-progress-status.is-starting {
@@ -304,6 +321,8 @@
}
.queue-progress-metrics {
flex-shrink: 0;
max-width: 100%;
margin-left: auto;
text-align: right;
font-variant-numeric: tabular-nums;