diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd1ba6..3a22882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.16 - 2026-08-13 + +- Preserve completed System Check results when switching between German and English. +- Relocalize successful and failed diagnostics, health badges and running check controls without stale language state. +- Clear obsolete System Check results after download path, imported configuration or managed tool changes. + ## 1.0.15 - 2026-08-13 - Keep update download progress within its popover and make changelog expansion and collapse easier to follow. diff --git a/README.md b/README.md index c87af4c..b39fb17 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ The application works in public mode without a Twitch login. Connecting a Twitch ## Installation 1. Open the [latest GitHub release](https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/latest). -2. Download `Twitch-VOD-Manager-Setup-1.0.15.exe`. +2. Download `Twitch-VOD-Manager-Setup-1.0.16.exe`. 3. Run the installer and choose the installation directory. 4. Start Twitch VOD Manager and add a streamer. diff --git a/package-lock.json b/package-lock.json index 727c3ba..186dc51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "1.0.15", + "version": "1.0.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "1.0.15", + "version": "1.0.16", "license": "MIT", "dependencies": { "axios": "^1.16.1", diff --git a/package.json b/package.json index 5583abf..1140d6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "1.0.15", + "version": "1.0.16", "description": "Twitch VOD Manager - Download Twitch VODs easily", "main": "dist/main.js", "author": "Sucukdeluxe", diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 323499f..dd69700 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -93,7 +93,7 @@ if (process.platform === 'win32') { sourcePath: electronSourceExecutable, destinationPath: resolve(rootDirectory, 'node_modules', 'electron', 'dist', 'Twitch VOD Manager.exe'), iconPath: resolve(rootDirectory, 'build', 'icon.ico'), - version: '1.0.15', + version: '1.0.16', }); } diff --git a/scripts/e2e-test-environment.js b/scripts/e2e-test-environment.js index 2405025..0a3bf77 100644 --- a/scripts/e2e-test-environment.js +++ b/scripts/e2e-test-environment.js @@ -208,7 +208,7 @@ async function installOfflineFixtures(app) { ffprobe: true, downloadPathWritable: true }, - messages: ['Offline fixture'], + messages: [], timestamp: '2026-01-01T00:00:00Z' })); replaceHandler('check-update', async () => ({ checking: true, offlineFixture: true })); diff --git a/scripts/smoke-test-public-release-config.js b/scripts/smoke-test-public-release-config.js index 8c06fd7..3a7e61d 100644 --- a/scripts/smoke-test-public-release-config.js +++ b/scripts/smoke-test-public-release-config.js @@ -15,9 +15,9 @@ function check(condition, message) { if (!condition) failures.push(message); } -check(packageJson.version === '1.0.15', `package version is ${packageJson.version}`); -check(packageLock.version === '1.0.15', `lockfile version is ${packageLock.version}`); -check(packageLock.packages?.['']?.version === '1.0.15', `lockfile root package version is ${packageLock.packages?.['']?.version}`); +check(packageJson.version === '1.0.16', `package version is ${packageJson.version}`); +check(packageLock.version === '1.0.16', `lockfile version is ${packageLock.version}`); +check(packageLock.packages?.['']?.version === '1.0.16', `lockfile root package version is ${packageLock.packages?.['']?.version}`); check(packageJson.build?.appId === 'io.github.sucukdeluxe.twitch-vod-manager', `appId is ${packageJson.build?.appId}`); check(packageJson.build?.publish?.provider === 'generic', `publish provider is ${packageJson.build?.publish?.provider}`); check(packageJson.build?.publish?.url === 'https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/latest/download/', `publish URL is ${packageJson.build?.publish?.url}`); @@ -65,7 +65,7 @@ check(mainSource.includes('GITHUB_RELEASES_DOWNLOAD_BASE_URL'), 'GitHub releases check(mainSource.includes('https://api.github.com/repos/Sucukdeluxe/Twitch-VOD-Manager/releases/latest'), 'GitHub latest release API URL is missing'); check(mainSource.includes('https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/download'), 'GitHub release download URL is missing'); check(!/storyboards\/\d{8,12}(?:-|\/)/.test(mainSource), 'numeric Twitch VOD example remains in the public source'); -check(indexSource.includes('Version: v1.0.15'), 'initial version label is not 1.0.15'); +check(indexSource.includes('Version: v1.0.16'), 'initial version label is not 1.0.16'); check(!indexSource.includes('Version: v4.1.13'), 'legacy version label is still present'); check(fs.existsSync(manifestPath), 'public release manifest is missing'); diff --git a/scripts/smoke-test-workspace-ui.js b/scripts/smoke-test-workspace-ui.js index 7d7f77d..a529019 100644 --- a/scripts/smoke-test-workspace-ui.js +++ b/scripts/smoke-test-workspace-ui.js @@ -96,6 +96,139 @@ async function run() { check(shell.topNavigationItems === 7, `Expected 7 native primary navigation buttons, found ${shell.topNavigationItems}`); check(shell.nonButtonNavigationItems === 0, `Expected only native primary navigation buttons, found ${shell.nonButtonNavigationItems} non-buttons`); + const preflightLocaleRetention = await win.evaluate(async () => { + await window.runPreflight(false); + window.changeLanguage('de'); + const german = { + result: document.getElementById('preflightResult')?.textContent || '', + badge: document.getElementById('healthBadge')?.textContent || '', + badgeGood: document.getElementById('healthBadge')?.classList.contains('good') || false + }; + window.changeLanguage('en'); + const english = { + result: document.getElementById('preflightResult')?.textContent || '', + badge: document.getElementById('healthBadge')?.textContent || '', + badgeGood: document.getElementById('healthBadge')?.classList.contains('good') || false + }; + return { german, english }; + }); + checks.preflightLocaleRetention = preflightLocaleRetention; + check(preflightLocaleRetention.german.result.includes('OK Download-Pfad') && preflightLocaleRetention.german.result.includes('Alles bereit.'), `German language switch lost the completed system check: ${preflightLocaleRetention.german.result}`); + check(preflightLocaleRetention.english.result.includes('OK Download path') && preflightLocaleRetention.english.result.includes('Everything is ready.'), `English language switch lost the completed system check: ${preflightLocaleRetention.english.result}`); + check(preflightLocaleRetention.german.badgeGood && preflightLocaleRetention.german.badge === 'System: Stabil', `German language switch lost the healthy system badge: ${JSON.stringify(preflightLocaleRetention.german)}`); + check(preflightLocaleRetention.english.badgeGood && preflightLocaleRetention.english.badge === 'System: Stable', `English language switch lost the healthy system badge: ${JSON.stringify(preflightLocaleRetention.english)}`); + + await app.evaluate(({ ipcMain }) => { + ipcMain.removeHandler('run-preflight'); + ipcMain.handle('run-preflight', async () => ({ + ok: false, + autoFixApplied: false, + checks: { + internet: false, + streamlink: true, + ffmpeg: false, + ffprobe: true, + downloadPathWritable: true + }, + messages: [ + 'No internet connection detected.', + 'FFmpeg is missing or not runnable.' + ], + timestamp: '2026-01-01T00:00:00Z' + })); + }); + const failedPreflightLocaleRetention = await win.evaluate(async () => { + window.changeLanguage('en'); + await window.runPreflight(false); + window.changeLanguage('de'); + const german = { + result: document.getElementById('preflightResult')?.textContent || '', + badge: document.getElementById('healthBadge')?.textContent || '', + badgeWarn: document.getElementById('healthBadge')?.classList.contains('warn') || false + }; + window.changeLanguage('en'); + const english = { + result: document.getElementById('preflightResult')?.textContent || '', + badge: document.getElementById('healthBadge')?.textContent || '', + badgeWarn: document.getElementById('healthBadge')?.classList.contains('warn') || false + }; + return { german, english }; + }); + checks.failedPreflightLocaleRetention = failedPreflightLocaleRetention; + check(failedPreflightLocaleRetention.german.result.includes('Keine Internetverbindung erkannt.') && failedPreflightLocaleRetention.german.result.includes('FFmpeg fehlt oder ist nicht startbar.'), `German language switch did not relocalize failed system checks: ${failedPreflightLocaleRetention.german.result}`); + check(!failedPreflightLocaleRetention.german.result.includes('No internet connection detected.'), `German failed system check kept English diagnostics: ${failedPreflightLocaleRetention.german.result}`); + check(failedPreflightLocaleRetention.english.result.includes('No internet connection detected.') && failedPreflightLocaleRetention.english.result.includes('FFmpeg is missing or not runnable.'), `English language switch did not relocalize failed system checks: ${failedPreflightLocaleRetention.english.result}`); + check(failedPreflightLocaleRetention.german.badgeWarn && failedPreflightLocaleRetention.german.badge === 'System: Warnung', `German failed system badge was not retained: ${JSON.stringify(failedPreflightLocaleRetention.german)}`); + check(failedPreflightLocaleRetention.english.badgeWarn && failedPreflightLocaleRetention.english.badge === 'System: Warning', `English failed system badge was not retained: ${JSON.stringify(failedPreflightLocaleRetention.english)}`); + + await app.evaluate(({ ipcMain }) => { + ipcMain.removeHandler('run-preflight'); + ipcMain.handle('run-preflight', () => new Promise((resolve) => { + globalThis.__workspacePreflightLocaleResolve = () => resolve({ + ok: true, + autoFixApplied: false, + checks: { + internet: true, + streamlink: true, + ffmpeg: true, + ffprobe: true, + downloadPathWritable: true + }, + messages: [], + timestamp: '2026-01-01T00:00:00Z' + }); + })); + }); + await win.evaluate(() => { + window.changeLanguage('en'); + globalThis.__workspacePreflightLocalePending = window.runPreflight(false); + }); + await app.evaluate(async () => { + const deadline = Date.now() + 5000; + while (typeof globalThis.__workspacePreflightLocaleResolve !== 'function') { + if (Date.now() >= deadline) throw new Error('Timed out waiting for the gated system check'); + await new Promise((resolve) => setTimeout(resolve, 1)); + } + }); + const duringPreflightLocale = await win.evaluate(() => { + window.changeLanguage('de'); + return document.getElementById('btnPreflightRun')?.textContent || ''; + }); + const changedDownloadPath = path.join(environment.downloadsDir, 'preflight-path-change'); + fs.mkdirSync(changedDownloadPath, { recursive: true }); + await app.evaluate(({ dialog }, folderPath) => { + dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [folderPath] }); + }, changedDownloadPath); + await win.evaluate(() => window.selectFolder()); + await app.evaluate(() => globalThis.__workspacePreflightLocaleResolve()); + const afterPreflightLocale = await win.evaluate(async () => { + await globalThis.__workspacePreflightLocalePending; + return document.getElementById('btnPreflightRun')?.textContent || ''; + }); + const preflightInFlightLocale = { during: duringPreflightLocale, after: afterPreflightLocale }; + checks.preflightInFlightLocale = preflightInFlightLocale; + check(preflightInFlightLocale.during === 'Prüfe...', `Running system check did not adopt the German language: ${preflightInFlightLocale.during}`); + check(preflightInFlightLocale.after === 'Check ausführen', `Completed system check restored an outdated button label: ${preflightInFlightLocale.after}`); + + const preflightAfterPathChange = await win.evaluate(() => { + window.changeLanguage('en'); + const english = { + result: document.getElementById('preflightResult')?.textContent || '', + badge: document.getElementById('healthBadge')?.textContent || '', + badgeUnknown: document.getElementById('healthBadge')?.classList.contains('unknown') || false + }; + window.changeLanguage('de'); + const german = { + result: document.getElementById('preflightResult')?.textContent || '', + badge: document.getElementById('healthBadge')?.textContent || '', + badgeUnknown: document.getElementById('healthBadge')?.classList.contains('unknown') || false + }; + return { english, german }; + }); + checks.preflightAfterPathChange = preflightAfterPathChange; + check(preflightAfterPathChange.english.result === 'No checks run yet.' && preflightAfterPathChange.english.badgeUnknown && preflightAfterPathChange.english.badge === 'System: Unknown', `Download path change kept a stale English system check: ${JSON.stringify(preflightAfterPathChange.english)}`); + check(preflightAfterPathChange.german.result === 'Noch kein Check ausgeführt.' && preflightAfterPathChange.german.badgeUnknown && preflightAfterPathChange.german.badge === 'System: Unbekannt', `Download path change restored a stale German system check: ${JSON.stringify(preflightAfterPathChange.german)}`); + const mergeAddToolbarActions = await win.evaluate(() => { const capture = () => { const button = document.querySelector('[data-toolbar-for="merge"] button[onclick="addMergeFiles()"]'); diff --git a/src/index.html b/src/index.html index 545d1a0..869593a 100644 --- a/src/index.html +++ b/src/index.html @@ -942,7 +942,7 @@ @@ -952,10 +952,10 @@ System: Unbekannt
- +
-
Noch kein Check ausgefuhrt.
+
Noch kein Check ausgeführt.

Verwaltete Tools

diff --git a/src/renderer-locale-de.ts b/src/renderer-locale-de.ts index 2415461..dcdaa60 100644 --- a/src/renderer-locale-de.ts +++ b/src/renderer-locale-de.ts @@ -286,9 +286,9 @@ const UI_TEXT_DE = { updateTitle: 'Updates', checkUpdates: 'Nach Updates suchen', preflightTitle: 'System-Check', - preflightRun: 'Check ausfuhren', + preflightRun: 'Check ausführen', preflightFix: 'Auto-Fix Tools', - preflightEmpty: 'Noch kein Check ausgefuhrt.', + preflightEmpty: 'Noch kein Check ausgeführt.', preflightChecking: 'Prüfe...', preflightFixing: 'Fixe...', preflightReady: 'Alles bereit.', @@ -297,6 +297,11 @@ const UI_TEXT_DE = { preflightFfmpeg: 'FFmpeg', preflightFfprobe: 'FFprobe', preflightPath: 'Download-Pfad', + preflightNoInternet: 'Keine Internetverbindung erkannt.', + preflightStreamlinkMissing: 'Streamlink fehlt oder ist nicht startbar.', + preflightFfmpegMissing: 'FFmpeg fehlt oder ist nicht startbar.', + preflightFfprobeMissing: 'FFprobe fehlt oder ist nicht startbar.', + preflightDownloadPathNotWritable: 'Download-Ordner ist nicht beschreibbar.', managedToolsTitle: 'Verwaltete Tools', managedToolsRefresh: 'Tool-Status aktualisieren', managedToolsRepair: 'Tools reparieren', diff --git a/src/renderer-locale-en.ts b/src/renderer-locale-en.ts index b57be0e..83c90c3 100644 --- a/src/renderer-locale-en.ts +++ b/src/renderer-locale-en.ts @@ -297,6 +297,11 @@ const UI_TEXT_EN = { preflightFfmpeg: 'FFmpeg', preflightFfprobe: 'FFprobe', preflightPath: 'Download path', + preflightNoInternet: 'No internet connection detected.', + preflightStreamlinkMissing: 'Streamlink is missing or not runnable.', + preflightFfmpegMissing: 'FFmpeg is missing or not runnable.', + preflightFfprobeMissing: 'FFprobe is missing or not runnable.', + preflightDownloadPathNotWritable: 'Download folder is not writable.', managedToolsTitle: 'Managed tools', managedToolsRefresh: 'Refresh tool status', managedToolsRepair: 'Repair tools', diff --git a/src/renderer-settings.ts b/src/renderer-settings.ts index c8162c3..99b0fc3 100644 --- a/src/renderer-settings.ts +++ b/src/renderer-settings.ts @@ -7,6 +7,8 @@ let settingsAutoSaveTimer: number | null = null; let pendingCredentialsReconnect = false; let lastPersistedSettingsFingerprint = ''; let settingsInputGeneration = 0; +let lastPreflightResult: PreflightResult | null = null; +let preflightGeneration = 0; const SECRET_INPUT_MASK = '••••••••'; let secretStatus: SecretStatus = { encryptionAvailable: false, @@ -276,6 +278,7 @@ function changeLanguage(lang: string): void { void refreshRuntimeMetrics(); void refreshAutomationStatusLine(); + refreshLocalizedPreflightUi(); validateFilenameTemplates(); filterSettings(byId('settingsSearchInput').value); } @@ -296,30 +299,53 @@ function selectLanguageOption(lang: string): void { changeLanguage(lang); } -function renderPreflightResult(result: PreflightResult): void { - const entries = [ - [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] - ]; +function renderPreflightButtonLabels(): void { + const runButton = byId('btnPreflightRun'); + const fixButton = byId('btnPreflightFix'); + runButton.textContent = runButton.disabled ? UI_TEXT.static.preflightChecking : UI_TEXT.static.preflightRun; + fixButton.textContent = fixButton.disabled ? UI_TEXT.static.preflightFixing : UI_TEXT.static.preflightFix; +} +function refreshLocalizedPreflightUi(): void { + renderPreflightButtonLabels(); + if (lastPreflightResult) renderPreflightResult(lastPreflightResult); +} + +function invalidatePreflightResult(): void { + preflightGeneration += 1; + lastPreflightResult = null; + byId('preflightResult').textContent = UI_TEXT.static.preflightEmpty; + const badge = byId('healthBadge'); + badge.classList.remove('good', 'warn', 'bad', 'unknown'); + badge.classList.add('unknown'); + badge.textContent = UI_TEXT.static.healthUnknown; +} + +function renderPreflightResult(result: PreflightResult): void { + lastPreflightResult = result; + const entries: Array<[string, boolean, string]> = [ + [UI_TEXT.static.preflightInternet, result.checks.internet, UI_TEXT.static.preflightNoInternet], + [UI_TEXT.static.preflightStreamlink, result.checks.streamlink, UI_TEXT.static.preflightStreamlinkMissing], + [UI_TEXT.static.preflightFfmpeg, result.checks.ffmpeg, UI_TEXT.static.preflightFfmpegMissing], + [UI_TEXT.static.preflightFfprobe, result.checks.ffprobe, UI_TEXT.static.preflightFfprobeMissing], + [UI_TEXT.static.preflightPath, result.checks.downloadPathWritable, UI_TEXT.static.preflightDownloadPathNotWritable] + ]; + const localizedIssues = entries.filter(([, ok]) => !ok).map(([, , message]) => message); const lines = entries.map(([name, ok]) => `${ok ? 'OK' : 'FAIL'} ${name}`).join('\n'); - const extra = result.messages.length ? `\n\n${result.messages.join('\n')}` : `\n\n${UI_TEXT.static.preflightReady}`; + const extra = localizedIssues.length ? `\n\n${localizedIssues.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) { + const failCount = localizedIssues.length; + if (failCount === 0) { 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; @@ -331,16 +357,16 @@ function renderPreflightResult(result: PreflightResult): void { async function runPreflight(autoFix = false): Promise { const btn = byId(autoFix ? 'btnPreflightFix' : 'btnPreflightRun'); - const old = btn.textContent || ''; + const generation = ++preflightGeneration; btn.disabled = true; - btn.textContent = autoFix ? UI_TEXT.static.preflightFixing : UI_TEXT.static.preflightChecking; + renderPreflightButtonLabels(); try { const result = await window.api.runPreflight(autoFix); - renderPreflightResult(result); + if (generation === preflightGeneration) renderPreflightResult(result); } finally { btn.disabled = false; - btn.textContent = old; + renderPreflightButtonLabels(); } } @@ -378,7 +404,10 @@ async function repairManagedTools(): Promise { byId('managedToolStatus').textContent = UI_TEXT.static.managedToolsRepairing; try { const result = await window.api.repairManagedTools(); - if (result) renderManagedToolStatus(result.statuses); + if (result) { + renderManagedToolStatus(result.statuses); + invalidatePreflightResult(); + } } finally { for (const button of buttons) button.disabled = false; } @@ -387,7 +416,10 @@ async function repairManagedTools(): Promise { async function resetManagedTools(): Promise { if (!confirm(UI_TEXT.static.managedToolsResetConfirm)) return; const result = await window.api.resetManagedTools(); - if (result) renderManagedToolStatus(result.statuses); + if (result) { + renderManagedToolStatus(result.statuses); + invalidatePreflightResult(); + } } async function runCleanupDryRun(): Promise { @@ -564,6 +596,7 @@ async function importConfigFromFile(): Promise { config = await window.api.getConfig(); if (typeof setLanguage === 'function' && typeof config.language === 'string') { setLanguage(config.language); + invalidatePreflightResult(); } if (typeof renderStreamers === 'function') renderStreamers(); if (typeof syncSettingsFormFromConfig === 'function') syncSettingsFormFromConfig(); @@ -1141,6 +1174,7 @@ async function selectFolder(): Promise { byId('downloadPath').value = folder.displayPath; config = await window.api.saveConfig({ download_path: folder.displayPath }, folder.token); + invalidatePreflightResult(); // Warn-only validation — the user explicitly chose this folder, so don't // refuse to save (they might be picking a path on a USB stick that's