Twitch-VOD-Manager/src/renderer-texts.ts
xRangerDE 23d0dd5829 ui: VOD list filter with persistence + Ctrl+F focus + Esc clear
Filter row above the VOD grid lets the user search the loaded archive
by title. Concrete user pain: streamers commonly have hundreds of VODs
and the current UI only supported scrolling.

- vodFilterInput / vodFilterClearBtn / vodFilterCount in index.html
- localized placeholder + clear-button title (DE + EN)
- vodFilterQuery state persisted to localStorage as
  twitch-vod-manager:vod-filter so the search bar survives reloads
- renderVODs split: it now caches lastLoadedVods + lastLoadedStreamer
  and delegates to renderVodGridFromCurrentState which applies
  filterVodsByQuery on every input event (no re-fetch)
- empty-state DOM is now built with createElement + textContent (via
  setVodGridEmptyState) instead of an innerHTML template, even for
  locale-only strings — defence in depth
- keyboard: Ctrl/Cmd+F focuses the filter when the VODs tab is active
  (Electron has no native find bar, so the default is suppressed). Esc
  clears the filter when the input has focus and content. Esc still
  closes modals first if any are open.

docs/IMPROVEMENT_LOG.md: Cycle 3 dated section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:43:16 +02:00

180 lines
9.1 KiB
TypeScript

type LanguageCode = 'de' | 'en';
const UI_TEXTS = {
de: UI_TEXT_DE,
en: UI_TEXT_EN
} as const;
let currentLanguage: LanguageCode = 'en';
let UI_TEXT: (typeof UI_TEXTS)[LanguageCode] = UI_TEXTS[currentLanguage];
function getIntlLocale(): string {
return currentLanguage === 'en' ? 'en-US' : 'de-DE';
}
function formatUiDate(input: string | Date): string {
const date = input instanceof Date ? input : new Date(input);
return date.toLocaleDateString(getIntlLocale());
}
function formatUiNumber(value: number): string {
return value.toLocaleString(getIntlLocale());
}
function setText(id: string, value: string): void {
const node = document.getElementById(id);
if (node) node.textContent = value;
}
function setPlaceholder(id: string, value: string): void {
const node = document.getElementById(id) as HTMLInputElement | null;
if (node) node.placeholder = value;
}
function setTitle(id: string, value: string): void {
const node = document.getElementById(id);
if (node) node.setAttribute('title', value);
}
function setLanguage(lang: string): LanguageCode {
currentLanguage = lang === 'en' ? 'en' : 'de';
UI_TEXT = UI_TEXTS[currentLanguage];
applyLanguageToStaticUI();
return currentLanguage;
}
function applyLanguageToStaticUI(): void {
setText('logoText', UI_TEXT.appName);
setText('navVodsText', UI_TEXT.static.navVods);
setText('navClipsText', UI_TEXT.static.navClips);
setText('navCutterText', UI_TEXT.static.navCutter);
setText('navMergeText', UI_TEXT.static.navMerge);
setText('navSettingsText', UI_TEXT.static.navSettings);
setText('queueTitleText', UI_TEXT.static.queueTitle);
setText('healthBadge', UI_TEXT.static.healthUnknown);
setText('btnRetryFailed', UI_TEXT.static.retryFailed);
setTitle('btnRetryFailed', UI_TEXT.static.retryFailedHint);
setText('btnClear', UI_TEXT.static.clearQueue);
setText('refreshText', UI_TEXT.static.refresh);
setText('clipsHeading', UI_TEXT.static.clipsHeading);
setText('clipsInfoTitle', UI_TEXT.static.clipsInfoTitle);
setText('clipsInfoText', UI_TEXT.static.clipsInfoText);
setText('clipTemplateHelp', UI_TEXT.clips.templateHelp);
setPlaceholder('clipFilenameTemplate', UI_TEXT.clips.templatePlaceholder);
setText('cutterSelectTitle', UI_TEXT.static.cutterSelectTitle);
setText('cutterBrowseBtn', UI_TEXT.static.cutterBrowse);
setText('mergeTitle', UI_TEXT.static.mergeTitle);
setText('mergeDesc', UI_TEXT.static.mergeDesc);
setText('mergeAddBtn', UI_TEXT.static.mergeAdd);
setText('designTitle', UI_TEXT.static.designTitle);
setText('themeLabel', UI_TEXT.static.themeLabel);
setText('themeLightOption', UI_TEXT.static.themeLight);
setText('languageLabel', UI_TEXT.static.languageLabel);
setText('languageDeText', UI_TEXT.static.languageDe);
setText('languageEnText', UI_TEXT.static.languageEn);
setText('apiTitle', UI_TEXT.static.apiTitle);
setText('clientIdLabel', UI_TEXT.static.clientIdLabel);
setText('clientSecretLabel', UI_TEXT.static.clientSecretLabel);
setText('saveSettingsBtn', UI_TEXT.static.saveSettings);
setText('downloadSettingsTitle', UI_TEXT.static.downloadSettingsTitle);
setText('storageLabel', UI_TEXT.static.storageLabel);
setText('openFolderBtn', UI_TEXT.static.openFolder);
setText('modeLabel', UI_TEXT.static.modeLabel);
setText('modeFullText', UI_TEXT.static.modeFull);
setText('modePartsText', UI_TEXT.static.modeParts);
setText('partMinutesLabel', UI_TEXT.static.partMinutesLabel);
setText('parallelDownloadsLabel', UI_TEXT.static.parallelDownloadsLabel);
setText('parallelDownloads1', UI_TEXT.static.parallelDownloads1);
setText('parallelDownloads2', UI_TEXT.static.parallelDownloads2);
setText('performanceModeLabel', UI_TEXT.static.performanceModeLabel);
setText('performanceModeStability', UI_TEXT.static.performanceModeStability);
setText('performanceModeBalanced', UI_TEXT.static.performanceModeBalanced);
setText('performanceModeSpeed', UI_TEXT.static.performanceModeSpeed);
setText('smartSchedulerLabel', UI_TEXT.static.smartSchedulerLabel);
setText('duplicatePreventionLabel', UI_TEXT.static.duplicatePreventionLabel);
setText('persistQueueLabel', UI_TEXT.static.persistQueueLabel);
setText('metadataCacheMinutesLabel', UI_TEXT.static.metadataCacheMinutesLabel);
setText('filenameTemplatesTitle', UI_TEXT.static.filenameTemplatesTitle);
setText('vodTemplateLabel', UI_TEXT.static.vodTemplateLabel);
setText('partsTemplateLabel', UI_TEXT.static.partsTemplateLabel);
setText('defaultClipTemplateLabel', UI_TEXT.static.defaultClipTemplateLabel);
setText('filenameTemplateHint', UI_TEXT.static.filenameTemplateHint);
setText('filenameTemplateLint', UI_TEXT.static.templateLintOk);
setText('settingsTemplateGuideBtn', UI_TEXT.static.templateGuideButton);
setText('clipTemplateGuideBtn', UI_TEXT.static.templateGuideButton);
setText('clipTemplateLint', UI_TEXT.static.templateLintOk);
setText('templateGuideTitle', UI_TEXT.static.templateGuideTitle);
setText('templateGuideIntro', UI_TEXT.static.templateGuideIntro);
setText('templateGuideTemplateLabel', UI_TEXT.static.templateGuideTemplateLabel);
setText('templateGuideOutputLabel', UI_TEXT.static.templateGuideOutputLabel);
setText('templateGuideVarsTitle', UI_TEXT.static.templateGuideVarsTitle);
setText('templateGuideVarCol', UI_TEXT.static.templateGuideVarCol);
setText('templateGuideDescCol', UI_TEXT.static.templateGuideDescCol);
setText('templateGuideExampleCol', UI_TEXT.static.templateGuideExampleCol);
setText('templateGuideUseVod', UI_TEXT.static.templateGuideUseVod);
setText('templateGuideUseParts', UI_TEXT.static.templateGuideUseParts);
setText('templateGuideUseClip', UI_TEXT.static.templateGuideUseClip);
setText('templateGuideCloseBtn', UI_TEXT.static.templateGuideClose);
setPlaceholder('templateGuideInput', UI_TEXT.static.vodTemplatePlaceholder);
setPlaceholder('vodFilenameTemplate', UI_TEXT.static.vodTemplatePlaceholder);
setPlaceholder('partsFilenameTemplate', UI_TEXT.static.partsTemplatePlaceholder);
setPlaceholder('defaultClipFilenameTemplate', UI_TEXT.static.defaultClipTemplatePlaceholder);
setText('updateTitle', UI_TEXT.static.updateTitle);
setText('checkUpdateBtn', UI_TEXT.static.checkUpdates);
setText('preflightTitle', UI_TEXT.static.preflightTitle);
setText('btnPreflightRun', UI_TEXT.static.preflightRun);
setText('btnPreflightFix', UI_TEXT.static.preflightFix);
setText('preflightResult', UI_TEXT.static.preflightEmpty);
setText('debugLogTitle', UI_TEXT.static.debugLogTitle);
setText('btnRefreshLog', UI_TEXT.static.refreshLog);
setText('autoRefreshText', UI_TEXT.static.autoRefresh);
setText('runtimeMetricsTitle', UI_TEXT.static.runtimeMetricsTitle);
setText('btnRefreshMetrics', UI_TEXT.static.runtimeMetricsRefresh);
setText('btnExportMetrics', UI_TEXT.static.runtimeMetricsExport);
setText('runtimeMetricsAutoRefreshText', UI_TEXT.static.runtimeMetricsAutoRefresh);
setText('runtimeMetricsOutput', UI_TEXT.static.runtimeMetricsLoading);
setText('updateText', UI_TEXT.updates.bannerDefault);
setText('updateButton', UI_TEXT.updates.downloadNow);
setText('updateModalEyebrow', UI_TEXT.static.updateTitle);
setText('updateModalTitle', UI_TEXT.updates.modalAvailableTitle);
setText('updateModalDismissBtn', UI_TEXT.updates.modalDismiss);
setText('updateModalConfirmBtn', UI_TEXT.updates.modalDownloadConfirm);
setText('updateChangelogLabel', UI_TEXT.updates.changelogLabel);
setText('updateChangelogToggle', UI_TEXT.updates.showChangelog);
setText('updateChangelogEmpty', UI_TEXT.updates.noChangelog);
setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder);
setPlaceholder('vodFilterInput', UI_TEXT.vods.filterPlaceholder);
setTitle('vodFilterClearBtn', UI_TEXT.vods.filterClearTitle);
const status = document.getElementById('statusText')?.textContent?.trim() || '';
if (status === UI_TEXTS.de.static.notConnected || status === UI_TEXTS.en.static.notConnected) {
setText('statusText', UI_TEXT.static.notConnected);
}
const guideRefresh = (window as unknown as { refreshTemplateGuideTexts?: () => void }).refreshTemplateGuideTexts;
if (typeof guideRefresh === 'function') {
guideRefresh();
}
const updateRefresh = (window as unknown as { refreshUpdateUiTexts?: () => void }).refreshUpdateUiTexts;
if (typeof updateRefresh === 'function') {
updateRefresh();
}
}
function localizeCurrentStatusText(current: string): string {
const map: Record<string, keyof typeof UI_TEXT.status> = {
[UI_TEXTS.de.status.noLogin]: 'noLogin',
[UI_TEXTS.en.status.noLogin]: 'noLogin',
[UI_TEXTS.de.status.connecting]: 'connecting',
[UI_TEXTS.en.status.connecting]: 'connecting',
[UI_TEXTS.de.status.connected]: 'connected',
[UI_TEXTS.en.status.connected]: 'connected',
[UI_TEXTS.de.status.connectFailedPublic]: 'connectFailedPublic',
[UI_TEXTS.en.status.connectFailedPublic]: 'connectFailedPublic'
};
const key = map[current];
return key ? UI_TEXT.status[key] : current;
}