This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# 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
|
## 1.0.15 - 2026-08-13
|
||||||
|
|
||||||
- Keep update download progress within its popover and make changelog expansion and collapse easier to follow.
|
- Keep update download progress within its popover and make changelog expansion and collapse easier to follow.
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ The application works in public mode without a Twitch login. Connecting a Twitch
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Open the [latest GitHub release](https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/latest).
|
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.
|
3. Run the installer and choose the installation directory.
|
||||||
4. Start Twitch VOD Manager and add a streamer.
|
4. Start Twitch VOD Manager and add a streamer.
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "1.0.15",
|
"version": "1.0.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "1.0.15",
|
"version": "1.0.16",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "1.0.15",
|
"version": "1.0.16",
|
||||||
"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": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ if (process.platform === 'win32') {
|
|||||||
sourcePath: electronSourceExecutable,
|
sourcePath: electronSourceExecutable,
|
||||||
destinationPath: resolve(rootDirectory, 'node_modules', 'electron', 'dist', 'Twitch VOD Manager.exe'),
|
destinationPath: resolve(rootDirectory, 'node_modules', 'electron', 'dist', 'Twitch VOD Manager.exe'),
|
||||||
iconPath: resolve(rootDirectory, 'build', 'icon.ico'),
|
iconPath: resolve(rootDirectory, 'build', 'icon.ico'),
|
||||||
version: '1.0.15',
|
version: '1.0.16',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ async function installOfflineFixtures(app) {
|
|||||||
ffprobe: true,
|
ffprobe: true,
|
||||||
downloadPathWritable: true
|
downloadPathWritable: true
|
||||||
},
|
},
|
||||||
messages: ['Offline fixture'],
|
messages: [],
|
||||||
timestamp: '2026-01-01T00:00:00Z'
|
timestamp: '2026-01-01T00:00:00Z'
|
||||||
}));
|
}));
|
||||||
replaceHandler('check-update', async () => ({ checking: true, offlineFixture: true }));
|
replaceHandler('check-update', async () => ({ checking: true, offlineFixture: true }));
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ function check(condition, message) {
|
|||||||
if (!condition) failures.push(message);
|
if (!condition) failures.push(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
check(packageJson.version === '1.0.15', `package version is ${packageJson.version}`);
|
check(packageJson.version === '1.0.16', `package version is ${packageJson.version}`);
|
||||||
check(packageLock.version === '1.0.15', `lockfile version is ${packageLock.version}`);
|
check(packageLock.version === '1.0.16', `lockfile version is ${packageLock.version}`);
|
||||||
check(packageLock.packages?.['']?.version === '1.0.15', `lockfile root package version is ${packageLock.packages?.['']?.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?.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?.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}`);
|
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://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(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(!/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(!indexSource.includes('Version: v4.1.13'), 'legacy version label is still present');
|
||||||
check(fs.existsSync(manifestPath), 'public release manifest is missing');
|
check(fs.existsSync(manifestPath), 'public release manifest is missing');
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,139 @@ async function run() {
|
|||||||
check(shell.topNavigationItems === 7, `Expected 7 native primary navigation buttons, found ${shell.topNavigationItems}`);
|
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`);
|
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 mergeAddToolbarActions = await win.evaluate(() => {
|
||||||
const capture = () => {
|
const capture = () => {
|
||||||
const button = document.querySelector('[data-toolbar-for="merge"] button[onclick="addMergeFiles()"]');
|
const button = document.querySelector('[data-toolbar-for="merge"] button[onclick="addMergeFiles()"]');
|
||||||
|
|||||||
+3
-3
@@ -942,7 +942,7 @@
|
|||||||
|
|
||||||
<div class="settings-card" data-settings-pane="updates" hidden>
|
<div class="settings-card" data-settings-pane="updates" hidden>
|
||||||
<h3 id="updateTitle">Updates</h3>
|
<h3 id="updateTitle">Updates</h3>
|
||||||
<p id="versionInfo" class="card-intro">Version: v1.0.15</p>
|
<p id="versionInfo" class="card-intro">Version: v1.0.16</p>
|
||||||
<button type="button" class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
<button type="button" class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -952,10 +952,10 @@
|
|||||||
<span class="health-badge unknown" id="healthBadge">System: Unbekannt</span>
|
<span class="health-badge unknown" id="healthBadge">System: Unbekannt</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-bottom: 10px;">
|
<div class="form-row" style="margin-bottom: 10px;">
|
||||||
<button type="button" class="btn-secondary" id="btnPreflightRun" onclick="runPreflight(false)">Check ausfuhren</button>
|
<button type="button" class="btn-secondary" id="btnPreflightRun" onclick="runPreflight(false)">Check ausführen</button>
|
||||||
<button type="button" class="btn-secondary" id="btnPreflightFix" onclick="runPreflight(true)">Auto-Fix Tools</button>
|
<button type="button" class="btn-secondary" id="btnPreflightFix" onclick="runPreflight(true)">Auto-Fix Tools</button>
|
||||||
</div>
|
</div>
|
||||||
<pre id="preflightResult" class="log-panel">Noch kein Check ausgefuhrt.</pre>
|
<pre id="preflightResult" class="log-panel">Noch kein Check ausgeführt.</pre>
|
||||||
<h4 id="managedToolsTitle">Verwaltete Tools</h4>
|
<h4 id="managedToolsTitle">Verwaltete Tools</h4>
|
||||||
<div class="form-row" style="margin-bottom: 10px;">
|
<div class="form-row" style="margin-bottom: 10px;">
|
||||||
<button type="button" class="btn-secondary" id="btnRefreshManagedTools" onclick="refreshManagedToolStatus()">Tool-Status aktualisieren</button>
|
<button type="button" class="btn-secondary" id="btnRefreshManagedTools" onclick="refreshManagedToolStatus()">Tool-Status aktualisieren</button>
|
||||||
|
|||||||
@@ -286,9 +286,9 @@ const UI_TEXT_DE = {
|
|||||||
updateTitle: 'Updates',
|
updateTitle: 'Updates',
|
||||||
checkUpdates: 'Nach Updates suchen',
|
checkUpdates: 'Nach Updates suchen',
|
||||||
preflightTitle: 'System-Check',
|
preflightTitle: 'System-Check',
|
||||||
preflightRun: 'Check ausfuhren',
|
preflightRun: 'Check ausführen',
|
||||||
preflightFix: 'Auto-Fix Tools',
|
preflightFix: 'Auto-Fix Tools',
|
||||||
preflightEmpty: 'Noch kein Check ausgefuhrt.',
|
preflightEmpty: 'Noch kein Check ausgeführt.',
|
||||||
preflightChecking: 'Prüfe...',
|
preflightChecking: 'Prüfe...',
|
||||||
preflightFixing: 'Fixe...',
|
preflightFixing: 'Fixe...',
|
||||||
preflightReady: 'Alles bereit.',
|
preflightReady: 'Alles bereit.',
|
||||||
@@ -297,6 +297,11 @@ const UI_TEXT_DE = {
|
|||||||
preflightFfmpeg: 'FFmpeg',
|
preflightFfmpeg: 'FFmpeg',
|
||||||
preflightFfprobe: 'FFprobe',
|
preflightFfprobe: 'FFprobe',
|
||||||
preflightPath: 'Download-Pfad',
|
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',
|
managedToolsTitle: 'Verwaltete Tools',
|
||||||
managedToolsRefresh: 'Tool-Status aktualisieren',
|
managedToolsRefresh: 'Tool-Status aktualisieren',
|
||||||
managedToolsRepair: 'Tools reparieren',
|
managedToolsRepair: 'Tools reparieren',
|
||||||
|
|||||||
@@ -297,6 +297,11 @@ const UI_TEXT_EN = {
|
|||||||
preflightFfmpeg: 'FFmpeg',
|
preflightFfmpeg: 'FFmpeg',
|
||||||
preflightFfprobe: 'FFprobe',
|
preflightFfprobe: 'FFprobe',
|
||||||
preflightPath: 'Download path',
|
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',
|
managedToolsTitle: 'Managed tools',
|
||||||
managedToolsRefresh: 'Refresh tool status',
|
managedToolsRefresh: 'Refresh tool status',
|
||||||
managedToolsRepair: 'Repair tools',
|
managedToolsRepair: 'Repair tools',
|
||||||
|
|||||||
+51
-17
@@ -7,6 +7,8 @@ let settingsAutoSaveTimer: number | null = null;
|
|||||||
let pendingCredentialsReconnect = false;
|
let pendingCredentialsReconnect = false;
|
||||||
let lastPersistedSettingsFingerprint = '';
|
let lastPersistedSettingsFingerprint = '';
|
||||||
let settingsInputGeneration = 0;
|
let settingsInputGeneration = 0;
|
||||||
|
let lastPreflightResult: PreflightResult | null = null;
|
||||||
|
let preflightGeneration = 0;
|
||||||
const SECRET_INPUT_MASK = '••••••••';
|
const SECRET_INPUT_MASK = '••••••••';
|
||||||
let secretStatus: SecretStatus = {
|
let secretStatus: SecretStatus = {
|
||||||
encryptionAvailable: false,
|
encryptionAvailable: false,
|
||||||
@@ -276,6 +278,7 @@ function changeLanguage(lang: string): void {
|
|||||||
|
|
||||||
void refreshRuntimeMetrics();
|
void refreshRuntimeMetrics();
|
||||||
void refreshAutomationStatusLine();
|
void refreshAutomationStatusLine();
|
||||||
|
refreshLocalizedPreflightUi();
|
||||||
validateFilenameTemplates();
|
validateFilenameTemplates();
|
||||||
filterSettings(byId<HTMLInputElement>('settingsSearchInput').value);
|
filterSettings(byId<HTMLInputElement>('settingsSearchInput').value);
|
||||||
}
|
}
|
||||||
@@ -296,30 +299,53 @@ function selectLanguageOption(lang: string): void {
|
|||||||
changeLanguage(lang);
|
changeLanguage(lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPreflightResult(result: PreflightResult): void {
|
function renderPreflightButtonLabels(): void {
|
||||||
const entries = [
|
const runButton = byId<HTMLButtonElement>('btnPreflightRun');
|
||||||
[UI_TEXT.static.preflightInternet, result.checks.internet],
|
const fixButton = byId<HTMLButtonElement>('btnPreflightFix');
|
||||||
[UI_TEXT.static.preflightStreamlink, result.checks.streamlink],
|
runButton.textContent = runButton.disabled ? UI_TEXT.static.preflightChecking : UI_TEXT.static.preflightRun;
|
||||||
[UI_TEXT.static.preflightFfmpeg, result.checks.ffmpeg],
|
fixButton.textContent = fixButton.disabled ? UI_TEXT.static.preflightFixing : UI_TEXT.static.preflightFix;
|
||||||
[UI_TEXT.static.preflightFfprobe, result.checks.ffprobe],
|
}
|
||||||
[UI_TEXT.static.preflightPath, result.checks.downloadPathWritable]
|
|
||||||
];
|
|
||||||
|
|
||||||
|
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 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}`;
|
byId('preflightResult').textContent = `${lines}${extra}`;
|
||||||
|
|
||||||
const badge = byId('healthBadge');
|
const badge = byId('healthBadge');
|
||||||
badge.classList.remove('good', 'warn', 'bad', 'unknown');
|
badge.classList.remove('good', 'warn', 'bad', 'unknown');
|
||||||
|
|
||||||
if (result.ok) {
|
const failCount = localizedIssues.length;
|
||||||
|
if (failCount === 0) {
|
||||||
badge.classList.add('good');
|
badge.classList.add('good');
|
||||||
badge.textContent = UI_TEXT.static.healthGood;
|
badge.textContent = UI_TEXT.static.healthGood;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const failCount = Object.values(result.checks).filter((ok) => !ok).length;
|
|
||||||
if (failCount <= 2) {
|
if (failCount <= 2) {
|
||||||
badge.classList.add('warn');
|
badge.classList.add('warn');
|
||||||
badge.textContent = UI_TEXT.static.healthWarn;
|
badge.textContent = UI_TEXT.static.healthWarn;
|
||||||
@@ -331,16 +357,16 @@ function renderPreflightResult(result: PreflightResult): void {
|
|||||||
|
|
||||||
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 generation = ++preflightGeneration;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = autoFix ? UI_TEXT.static.preflightFixing : UI_TEXT.static.preflightChecking;
|
renderPreflightButtonLabels();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.api.runPreflight(autoFix);
|
const result = await window.api.runPreflight(autoFix);
|
||||||
renderPreflightResult(result);
|
if (generation === preflightGeneration) renderPreflightResult(result);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = old;
|
renderPreflightButtonLabels();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +404,10 @@ async function repairManagedTools(): Promise<void> {
|
|||||||
byId('managedToolStatus').textContent = UI_TEXT.static.managedToolsRepairing;
|
byId('managedToolStatus').textContent = UI_TEXT.static.managedToolsRepairing;
|
||||||
try {
|
try {
|
||||||
const result = await window.api.repairManagedTools();
|
const result = await window.api.repairManagedTools();
|
||||||
if (result) renderManagedToolStatus(result.statuses);
|
if (result) {
|
||||||
|
renderManagedToolStatus(result.statuses);
|
||||||
|
invalidatePreflightResult();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
for (const button of buttons) button.disabled = false;
|
for (const button of buttons) button.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -387,7 +416,10 @@ async function repairManagedTools(): Promise<void> {
|
|||||||
async function resetManagedTools(): Promise<void> {
|
async function resetManagedTools(): Promise<void> {
|
||||||
if (!confirm(UI_TEXT.static.managedToolsResetConfirm)) return;
|
if (!confirm(UI_TEXT.static.managedToolsResetConfirm)) return;
|
||||||
const result = await window.api.resetManagedTools();
|
const result = await window.api.resetManagedTools();
|
||||||
if (result) renderManagedToolStatus(result.statuses);
|
if (result) {
|
||||||
|
renderManagedToolStatus(result.statuses);
|
||||||
|
invalidatePreflightResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runCleanupDryRun(): Promise<void> {
|
async function runCleanupDryRun(): Promise<void> {
|
||||||
@@ -564,6 +596,7 @@ async function importConfigFromFile(): Promise<void> {
|
|||||||
config = await window.api.getConfig();
|
config = await window.api.getConfig();
|
||||||
if (typeof setLanguage === 'function' && typeof config.language === 'string') {
|
if (typeof setLanguage === 'function' && typeof config.language === 'string') {
|
||||||
setLanguage(config.language);
|
setLanguage(config.language);
|
||||||
|
invalidatePreflightResult();
|
||||||
}
|
}
|
||||||
if (typeof renderStreamers === 'function') renderStreamers();
|
if (typeof renderStreamers === 'function') renderStreamers();
|
||||||
if (typeof syncSettingsFormFromConfig === 'function') syncSettingsFormFromConfig();
|
if (typeof syncSettingsFormFromConfig === 'function') syncSettingsFormFromConfig();
|
||||||
@@ -1141,6 +1174,7 @@ async function selectFolder(): Promise<void> {
|
|||||||
|
|
||||||
byId<HTMLInputElement>('downloadPath').value = folder.displayPath;
|
byId<HTMLInputElement>('downloadPath').value = folder.displayPath;
|
||||||
config = await window.api.saveConfig({ download_path: folder.displayPath }, folder.token);
|
config = await window.api.saveConfig({ download_path: folder.displayPath }, folder.token);
|
||||||
|
invalidatePreflightResult();
|
||||||
|
|
||||||
// Warn-only validation — the user explicitly chose this folder, so don't
|
// 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
|
// refuse to save (they might be picking a path on a USB stick that's
|
||||||
|
|||||||
Reference in New Issue
Block a user