feat: skip-version + addStreamer validation + smart-scheduler tooltip
Three small UX wins.
1. Auto-update: "Skip this version" button on the update modal.
Stores the dismissed version in localStorage; subsequent automatic
update-available events for the same version are silenced (banner
hidden, modal not opened). Manual "Check for updates" overrides the
skip so the user can change their mind. The flag is cleared once
the version is actually downloaded so a stale entry never masks a
future update. Skip button is hidden in the "ready to install"
state where it would not make sense.
2. addStreamer now validates against Twitch username rules
(4-25 chars, [a-zA-Z0-9_]). Previously bad input fell through to
the API and the user saw a silent "streamer not found" message
instead of being told the input was invalid.
3. Smart Queue Scheduler checkbox got a hover tooltip that explains
what enabling it actually does ("prefers shorter VODs and older
queue entries first"). Users were disabling it without knowing
what they were turning off.
DE + EN locale strings added for all three.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
83647c264b
commit
d6e513d70d
@ -39,6 +39,7 @@
|
|||||||
|
|
||||||
<div class="modal-actions update-modal-actions">
|
<div class="modal-actions update-modal-actions">
|
||||||
<button class="btn-secondary" id="updateModalDismissBtn" type="button" onclick="dismissUpdateModal()">Nein</button>
|
<button class="btn-secondary" id="updateModalDismissBtn" type="button" onclick="dismissUpdateModal()">Nein</button>
|
||||||
|
<button class="btn-secondary" id="updateModalSkipBtn" type="button" onclick="skipUpdateVersion()">Diese Version ueberspringen</button>
|
||||||
<button class="btn-primary" id="updateModalConfirmBtn" type="button" onclick="confirmUpdateModal()">Ja, herunterladen</button>
|
<button class="btn-primary" id="updateModalConfirmBtn" type="button" onclick="confirmUpdateModal()">Ja, herunterladen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -49,6 +49,8 @@ const UI_TEXT_DE = {
|
|||||||
performanceModeBalanced: 'Ausgewogen',
|
performanceModeBalanced: 'Ausgewogen',
|
||||||
performanceModeSpeed: 'Max Geschwindigkeit',
|
performanceModeSpeed: 'Max Geschwindigkeit',
|
||||||
smartSchedulerLabel: 'Smart Queue Scheduler aktivieren',
|
smartSchedulerLabel: 'Smart Queue Scheduler aktivieren',
|
||||||
|
smartSchedulerHint: 'Bevorzugt kuerzere VODs und aeltere Queue-Eintraege zuerst, damit der Durchsatz gleichmaessig bleibt. Deaktivieren = strikte Einfuegereihenfolge.',
|
||||||
|
streamerInvalid: 'Twitch-Username ungueltig (4-25 Zeichen, Buchstaben/Zahlen/Unterstrich).',
|
||||||
duplicatePreventionLabel: 'Duplikate in Queue verhindern',
|
duplicatePreventionLabel: 'Duplikate in Queue verhindern',
|
||||||
persistQueueLabel: 'Queue zwischen App-Starts speichern',
|
persistQueueLabel: 'Queue zwischen App-Starts speichern',
|
||||||
metadataCacheMinutesLabel: 'Metadata-Cache (Minuten)',
|
metadataCacheMinutesLabel: 'Metadata-Cache (Minuten)',
|
||||||
@ -245,6 +247,7 @@ const UI_TEXT_DE = {
|
|||||||
modalDismiss: 'Nein',
|
modalDismiss: 'Nein',
|
||||||
modalDownloadConfirm: 'Ja, herunterladen',
|
modalDownloadConfirm: 'Ja, herunterladen',
|
||||||
modalInstallConfirm: 'Ja, installieren',
|
modalInstallConfirm: 'Ja, installieren',
|
||||||
|
modalSkipVersion: 'Diese Version ueberspringen',
|
||||||
changelogLabel: 'Changelog',
|
changelogLabel: 'Changelog',
|
||||||
showChangelog: 'Changelog anzeigen',
|
showChangelog: 'Changelog anzeigen',
|
||||||
hideChangelog: 'Changelog ausblenden',
|
hideChangelog: 'Changelog ausblenden',
|
||||||
|
|||||||
@ -49,6 +49,8 @@ const UI_TEXT_EN = {
|
|||||||
performanceModeBalanced: 'Balanced',
|
performanceModeBalanced: 'Balanced',
|
||||||
performanceModeSpeed: 'Max Speed',
|
performanceModeSpeed: 'Max Speed',
|
||||||
smartSchedulerLabel: 'Enable smart queue scheduler',
|
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.',
|
||||||
|
streamerInvalid: 'Invalid Twitch username (4-25 chars, letters/digits/underscore).',
|
||||||
duplicatePreventionLabel: 'Prevent duplicate queue entries',
|
duplicatePreventionLabel: 'Prevent duplicate queue entries',
|
||||||
persistQueueLabel: 'Keep queue between app restarts',
|
persistQueueLabel: 'Keep queue between app restarts',
|
||||||
metadataCacheMinutesLabel: 'Metadata Cache (Minutes)',
|
metadataCacheMinutesLabel: 'Metadata Cache (Minutes)',
|
||||||
@ -245,6 +247,7 @@ const UI_TEXT_EN = {
|
|||||||
modalDismiss: 'No',
|
modalDismiss: 'No',
|
||||||
modalDownloadConfirm: 'Yes, download',
|
modalDownloadConfirm: 'Yes, download',
|
||||||
modalInstallConfirm: 'Yes, install',
|
modalInstallConfirm: 'Yes, install',
|
||||||
|
modalSkipVersion: 'Skip this version',
|
||||||
changelogLabel: 'Changelog',
|
changelogLabel: 'Changelog',
|
||||||
showChangelog: 'Show changelog',
|
showChangelog: 'Show changelog',
|
||||||
hideChangelog: 'Hide changelog',
|
hideChangelog: 'Hide changelog',
|
||||||
|
|||||||
@ -210,7 +210,19 @@ function renderStreamers(): void {
|
|||||||
async function addStreamer(): Promise<void> {
|
async function addStreamer(): Promise<void> {
|
||||||
const input = byId<HTMLInputElement>('newStreamer');
|
const input = byId<HTMLInputElement>('newStreamer');
|
||||||
const name = input.value.trim().toLowerCase();
|
const name = input.value.trim().toLowerCase();
|
||||||
if (!name || (config.streamers ?? []).includes(name)) {
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitch usernames: 4-25 characters, alphanumeric + underscore.
|
||||||
|
// Catch typos / invalid input before it hits the API and silently
|
||||||
|
// returns "streamer not found".
|
||||||
|
if (!/^[a-zA-Z0-9_]{4,25}$/.test(name)) {
|
||||||
|
showAppToast(UI_TEXT.static.streamerInvalid, 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((config.streamers ?? []).includes(name)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -91,6 +91,8 @@ function applyLanguageToStaticUI(): void {
|
|||||||
setText('performanceModeBalanced', UI_TEXT.static.performanceModeBalanced);
|
setText('performanceModeBalanced', UI_TEXT.static.performanceModeBalanced);
|
||||||
setText('performanceModeSpeed', UI_TEXT.static.performanceModeSpeed);
|
setText('performanceModeSpeed', UI_TEXT.static.performanceModeSpeed);
|
||||||
setText('smartSchedulerLabel', UI_TEXT.static.smartSchedulerLabel);
|
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('duplicatePreventionLabel', UI_TEXT.static.duplicatePreventionLabel);
|
||||||
setText('persistQueueLabel', UI_TEXT.static.persistQueueLabel);
|
setText('persistQueueLabel', UI_TEXT.static.persistQueueLabel);
|
||||||
setText('metadataCacheMinutesLabel', UI_TEXT.static.metadataCacheMinutesLabel);
|
setText('metadataCacheMinutesLabel', UI_TEXT.static.metadataCacheMinutesLabel);
|
||||||
@ -139,6 +141,7 @@ function applyLanguageToStaticUI(): void {
|
|||||||
setText('updateModalTitle', UI_TEXT.updates.modalAvailableTitle);
|
setText('updateModalTitle', UI_TEXT.updates.modalAvailableTitle);
|
||||||
setText('updateModalDismissBtn', UI_TEXT.updates.modalDismiss);
|
setText('updateModalDismissBtn', UI_TEXT.updates.modalDismiss);
|
||||||
setText('updateModalConfirmBtn', UI_TEXT.updates.modalDownloadConfirm);
|
setText('updateModalConfirmBtn', UI_TEXT.updates.modalDownloadConfirm);
|
||||||
|
setText('updateModalSkipBtn', UI_TEXT.updates.modalSkipVersion);
|
||||||
setText('updateChangelogLabel', UI_TEXT.updates.changelogLabel);
|
setText('updateChangelogLabel', UI_TEXT.updates.changelogLabel);
|
||||||
setText('updateChangelogToggle', UI_TEXT.updates.showChangelog);
|
setText('updateChangelogToggle', UI_TEXT.updates.showChangelog);
|
||||||
setText('updateChangelogEmpty', UI_TEXT.updates.noChangelog);
|
setText('updateChangelogEmpty', UI_TEXT.updates.noChangelog);
|
||||||
|
|||||||
@ -9,6 +9,20 @@ let updateBannerState: 'idle' | 'available' | 'downloading' | 'ready' = 'idle';
|
|||||||
let updateChangelogExpanded = false;
|
let updateChangelogExpanded = false;
|
||||||
let shouldOpenUpdateModalOnAvailable = false;
|
let shouldOpenUpdateModalOnAvailable = false;
|
||||||
|
|
||||||
|
const SKIPPED_UPDATE_VERSION_KEY = 'twitch-vod-manager:skipped-update-version';
|
||||||
|
|
||||||
|
function getSkippedUpdateVersion(): string {
|
||||||
|
try { return localStorage.getItem(SKIPPED_UPDATE_VERSION_KEY) || ''; } catch { return ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistSkippedUpdateVersion(version: string): void {
|
||||||
|
try { localStorage.setItem(SKIPPED_UPDATE_VERSION_KEY, version); } catch { /* localStorage may be unavailable */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSkippedUpdateVersion(): void {
|
||||||
|
try { localStorage.removeItem(SKIPPED_UPDATE_VERSION_KEY); } catch { /* localStorage may be unavailable */ }
|
||||||
|
}
|
||||||
|
|
||||||
function notifyUpdate(message: string, type: 'info' | 'warn' = 'info'): void {
|
function notifyUpdate(message: string, type: 'info' | 'warn' = 'info'): void {
|
||||||
const toastFn = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
const toastFn = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
||||||
if (typeof toastFn === 'function') {
|
if (typeof toastFn === 'function') {
|
||||||
@ -278,6 +292,11 @@ function refreshUpdateModalTexts(): void {
|
|||||||
byId('updateModalConfirmBtn').textContent = isReady
|
byId('updateModalConfirmBtn').textContent = isReady
|
||||||
? UI_TEXT.updates.modalInstallConfirm
|
? UI_TEXT.updates.modalInstallConfirm
|
||||||
: UI_TEXT.updates.modalDownloadConfirm;
|
: UI_TEXT.updates.modalDownloadConfirm;
|
||||||
|
// Skip-version only makes sense before the download. Once the .exe is
|
||||||
|
// already on disk and ready to install, hide the button.
|
||||||
|
const skipBtn = byId<HTMLButtonElement>('updateModalSkipBtn');
|
||||||
|
skipBtn.textContent = UI_TEXT.updates.modalSkipVersion;
|
||||||
|
skipBtn.style.display = isReady ? 'none' : '';
|
||||||
byId('updateChangelogLabel').textContent = UI_TEXT.updates.changelogLabel;
|
byId('updateChangelogLabel').textContent = UI_TEXT.updates.changelogLabel;
|
||||||
byId('updateChangelogEmpty').textContent = UI_TEXT.updates.noChangelog;
|
byId('updateChangelogEmpty').textContent = UI_TEXT.updates.noChangelog;
|
||||||
|
|
||||||
@ -301,6 +320,19 @@ function dismissUpdateModal(): void {
|
|||||||
byId('updateModal').classList.remove('show');
|
byId('updateModal').classList.remove('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function skipUpdateVersion(): void {
|
||||||
|
const v = (latestUpdateInfo?.version || latestUpdateVersion || '').trim();
|
||||||
|
if (v) {
|
||||||
|
persistSkippedUpdateVersion(v);
|
||||||
|
}
|
||||||
|
dismissUpdateModal();
|
||||||
|
hideUpdateBanner();
|
||||||
|
updateBannerState = 'idle';
|
||||||
|
// Note: latestUpdateInfo is intentionally kept so a manual "Check for
|
||||||
|
// updates" can still re-surface the same version if the user changes
|
||||||
|
// their mind (manual checks bypass the skip-version filter).
|
||||||
|
}
|
||||||
|
|
||||||
function confirmUpdateModal(): void {
|
function confirmUpdateModal(): void {
|
||||||
dismissUpdateModal();
|
dismissUpdateModal();
|
||||||
|
|
||||||
@ -495,11 +527,22 @@ window.api.onUpdateAvailable((info: UpdateInfo) => {
|
|||||||
updateCheckInProgress = false;
|
updateCheckInProgress = false;
|
||||||
updateReady = false;
|
updateReady = false;
|
||||||
updateDownloadInProgress = false;
|
updateDownloadInProgress = false;
|
||||||
|
const wasManual = manualUpdateCheckPending;
|
||||||
manualUpdateCheckPending = false;
|
manualUpdateCheckPending = false;
|
||||||
manualUpdateOutcomeHandled = true;
|
manualUpdateOutcomeHandled = true;
|
||||||
latestDownloadProgress = null;
|
latestDownloadProgress = null;
|
||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
|
|
||||||
|
// If the user explicitly skipped this exact version, suppress the auto
|
||||||
|
// notification entirely — banner stays hidden, no modal popup. A manual
|
||||||
|
// "Check for updates" click overrides the skip so the user can change
|
||||||
|
// their mind.
|
||||||
|
const isSkipped = getSkippedUpdateVersion() === activeInfo.version;
|
||||||
|
if (isSkipped && !wasManual) {
|
||||||
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setUpdateBannerAvailableUi(activeInfo);
|
setUpdateBannerAvailableUi(activeInfo);
|
||||||
|
|
||||||
if (shouldOpenUpdateModalOnAvailable) {
|
if (shouldOpenUpdateModalOnAvailable) {
|
||||||
@ -509,6 +552,7 @@ window.api.onUpdateAvailable((info: UpdateInfo) => {
|
|||||||
shouldOpenUpdateModalOnAvailable = false;
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
window.api.onUpdateNotAvailable(() => {
|
window.api.onUpdateNotAvailable(() => {
|
||||||
updateCheckInProgress = false;
|
updateCheckInProgress = false;
|
||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
@ -540,6 +584,10 @@ window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
||||||
|
// Once a version is actually downloaded the user clearly stopped
|
||||||
|
// skipping it — clear the skip flag so future updates aren't masked
|
||||||
|
// by a stale entry.
|
||||||
|
clearSkippedUpdateVersion();
|
||||||
const activeInfo = rememberUpdateInfo(info);
|
const activeInfo = rememberUpdateInfo(info);
|
||||||
setDownloadReadyUi(activeInfo);
|
setDownloadReadyUi(activeInfo);
|
||||||
openUpdateModal(activeInfo);
|
openUpdateModal(activeInfo);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user