Compare commits

..

No commits in common. "main" and "v4.6.147" have entirely different histories.

10 changed files with 18 additions and 37 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.155", "version": "4.6.147",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.155", "version": "4.6.147",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.155", "version": "4.6.147",
"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": "xRangerDE", "author": "xRangerDE",

View File

@ -118,7 +118,7 @@
<div class="modal viewer-modal viewer-modal-events"> <div class="modal viewer-modal viewer-modal-events">
<button type="button" class="modal-close modal-close-localizable" aria-label="Close" onclick="closeEventsViewer()">x</button> <button type="button" class="modal-close modal-close-localizable" aria-label="Close" onclick="closeEventsViewer()">x</button>
<h2 id="eventsViewerTitle" class="viewer-modal-title"></h2> <h2 id="eventsViewerTitle" class="viewer-modal-title"></h2>
<div id="eventsViewerStatus" class="viewer-modal-status" role="status" aria-live="polite"></div> <div id="eventsViewerStatus" class="viewer-modal-status"></div>
<div id="eventsViewerList" class="viewer-modal-list"></div> <div id="eventsViewerList" class="viewer-modal-list"></div>
</div> </div>
</div> </div>
@ -130,7 +130,7 @@
<h2 id="chatViewerTitle" class="viewer-modal-title"></h2> <h2 id="chatViewerTitle" class="viewer-modal-title"></h2>
<div class="viewer-modal-filter-row"> <div class="viewer-modal-filter-row">
<input type="text" id="chatViewerFilter" class="viewer-modal-filter-input" placeholder="Filter..." oninput="onChatViewerFilterChange()"> <input type="text" id="chatViewerFilter" class="viewer-modal-filter-input" placeholder="Filter..." oninput="onChatViewerFilterChange()">
<span id="chatViewerStatus" class="viewer-modal-status viewer-modal-status-inline" role="status" aria-live="polite"></span> <span id="chatViewerStatus" class="viewer-modal-status viewer-modal-status-inline"></span>
</div> </div>
<div id="chatViewerList" class="viewer-modal-list viewer-modal-list-chat"></div> <div id="chatViewerList" class="viewer-modal-list viewer-modal-list-chat"></div>
</div> </div>
@ -301,7 +301,7 @@
<h2 id="clipsHeading">Twitch Clip-Download</h2> <h2 id="clipsHeading">Twitch Clip-Download</h2>
<input type="text" id="clipUrl" placeholder="https://clips.twitch.tv/... oder https://www.twitch.tv/.../clip/..."> <input type="text" id="clipUrl" placeholder="https://clips.twitch.tv/... oder https://www.twitch.tv/.../clip/...">
<button type="button" class="btn-primary" onclick="downloadClip()" id="btnClip">Clip herunterladen</button> <button type="button" class="btn-primary" onclick="downloadClip()" id="btnClip">Clip herunterladen</button>
<div class="clip-status" id="clipStatus" role="status" aria-live="polite"></div> <div class="clip-status" id="clipStatus"></div>
</div> </div>
<div class="settings-card centered"> <div class="settings-card centered">
@ -422,7 +422,7 @@
<div class="form-row section-header"> <div class="form-row section-header">
<h3 id="statsTitle">Archiv-Statistik</h3> <h3 id="statsTitle">Archiv-Statistik</h3>
<div class="section-header-actions"> <div class="section-header-actions">
<span id="statsLastScannedLabel" class="form-sublabel" role="status" aria-live="polite"></span> <span id="statsLastScannedLabel" class="form-sublabel"></span>
<button type="button" class="btn-secondary" id="btnStatsRefresh" onclick="refreshArchiveStats()">Aktualisieren</button> <button type="button" class="btn-secondary" id="btnStatsRefresh" onclick="refreshArchiveStats()">Aktualisieren</button>
</div> </div>
</div> </div>
@ -474,7 +474,7 @@
</select> </select>
<button type="button" class="btn-secondary" id="btnArchiveSearch" onclick="performArchiveSearch()">Suchen</button> <button type="button" class="btn-secondary" id="btnArchiveSearch" onclick="performArchiveSearch()">Suchen</button>
</div> </div>
<div id="archiveSearchSummary" class="form-sublabel" role="status" aria-live="polite"></div> <div id="archiveSearchSummary" class="form-sublabel"></div>
</div> </div>
<div class="settings-card"> <div class="settings-card">
<div id="archiveSearchResults"></div> <div id="archiveSearchResults"></div>
@ -695,7 +695,7 @@
<button type="button" class="btn-secondary" id="btnRefreshStorage" onclick="refreshStorageStats()">Aktualisieren</button> <button type="button" class="btn-secondary" id="btnRefreshStorage" onclick="refreshStorageStats()">Aktualisieren</button>
</div> </div>
<p id="storageCardIntro" class="card-intro">Disk-Verbrauch pro Streamer im aktuellen Download-Ordner. Live-Aufnahmen werden separat ausgewiesen.</p> <p id="storageCardIntro" class="card-intro">Disk-Verbrauch pro Streamer im aktuellen Download-Ordner. Live-Aufnahmen werden separat ausgewiesen.</p>
<div id="storageSummary" class="form-sublabel" style="margin-bottom:8px;" role="status" aria-live="polite"></div> <div id="storageSummary" class="form-sublabel" style="margin-bottom:8px;"></div>
<div id="storageList"></div> <div id="storageList"></div>
<hr> <hr>
@ -729,7 +729,7 @@
<button type="button" class="btn-secondary" id="btnCleanupDryRun" onclick="runCleanupDryRun()">Vorschau</button> <button type="button" class="btn-secondary" id="btnCleanupDryRun" onclick="runCleanupDryRun()">Vorschau</button>
<button type="button" class="btn-secondary" id="btnCleanupRunNow" onclick="runCleanupNow()">Jetzt ausfuehren</button> <button type="button" class="btn-secondary" id="btnCleanupRunNow" onclick="runCleanupNow()">Jetzt ausfuehren</button>
</div> </div>
<div id="cleanupReport" class="form-note" role="status" aria-live="polite"></div> <div id="cleanupReport" class="form-note"></div>
</div> </div>
<div class="settings-card"> <div class="settings-card">

View File

@ -139,7 +139,6 @@ const UI_TEXT_DE = {
archiveNoMatches: 'Keine Treffer.', archiveNoMatches: 'Keine Treffer.',
archiveNoRoot: 'Download-Ordner nicht gefunden. Setze zuerst einen Download-Pfad in den Einstellungen.', archiveNoRoot: 'Download-Ordner nicht gefunden. Setze zuerst einen Download-Pfad in den Einstellungen.',
archiveSearchPlaceholder: 'Suche...', archiveSearchPlaceholder: 'Suche...',
archiveSearchAria: 'Archiv durchsuchen',
archiveOpen: 'Oeffnen', archiveOpen: 'Oeffnen',
archiveShowInFolder: 'Ordner', archiveShowInFolder: 'Ordner',
archiveViewChat: 'Chat', archiveViewChat: 'Chat',
@ -178,7 +177,6 @@ const UI_TEXT_DE = {
downloadPathNotWritable: 'Download-Ordner ist nicht beschreibbar. Waehle einen anderen Ordner oder pruefe die Schreibrechte.', downloadPathNotWritable: 'Download-Ordner ist nicht beschreibbar. Waehle einen anderen Ordner oder pruefe die Schreibrechte.',
streamerSectionTitle: 'Streamer', streamerSectionTitle: 'Streamer',
streamerListFilterPlaceholder: 'Filtern...', streamerListFilterPlaceholder: 'Filtern...',
streamerListFilterAria: 'Streamer-Liste filtern',
streamerAddAriaLabel: 'Streamer hinzufuegen', streamerAddAriaLabel: 'Streamer hinzufuegen',
streamerBulkRemoveTitle: 'Alle entfernen (oder gefilterte)', streamerBulkRemoveTitle: 'Alle entfernen (oder gefilterte)',
streamerBulkRemoveAll: 'Alle {count} Streamer aus der Liste entfernen?', streamerBulkRemoveAll: 'Alle {count} Streamer aus der Liste entfernen?',
@ -334,7 +332,6 @@ const UI_TEXT_DE = {
openTwitch: 'Auf Twitch oeffnen', openTwitch: 'Auf Twitch oeffnen',
openTwitchTooltip: 'Diesen Kanal auf twitch.tv oeffnen', openTwitchTooltip: 'Diesen Kanal auf twitch.tv oeffnen',
liveCardTooltip: 'Klick um sofort eine Live-Aufnahme zu starten', liveCardTooltip: 'Klick um sofort eine Live-Aufnahme zu starten',
liveThumbAlt: 'Live-Vorschau',
recordNow: 'Jetzt aufnehmen', recordNow: 'Jetzt aufnehmen',
refresh: 'Aktualisieren', refresh: 'Aktualisieren',
agoMinutes: 'vor {n} Min', agoMinutes: 'vor {n} Min',
@ -380,7 +377,6 @@ const UI_TEXT_DE = {
addQueue: '+ Warteschlange', addQueue: '+ Warteschlange',
trimButton: 'VOD zuschneiden', trimButton: 'VOD zuschneiden',
filterPlaceholder: 'Nach Titel filtern... (Strg+F)', filterPlaceholder: 'Nach Titel filtern... (Strg+F)',
filterAria: 'VOD-Titel filtern',
filterClearTitle: 'Filter loeschen (Esc)', filterClearTitle: 'Filter loeschen (Esc)',
filterNoMatchTitle: 'Keine Treffer', filterNoMatchTitle: 'Keine Treffer',
filterNoMatchText: 'Keine VODs entsprechen dem aktuellen Filter.', filterNoMatchText: 'Keine VODs entsprechen dem aktuellen Filter.',
@ -447,7 +443,6 @@ const UI_TEXT_DE = {
videoInfoFailed: 'Konnte Video-Informationen nicht lesen. FFprobe installiert?', videoInfoFailed: 'Konnte Video-Informationen nicht lesen. FFprobe installiert?',
previewLoading: 'Lade Vorschau...', previewLoading: 'Lade Vorschau...',
previewUnavailable: 'Vorschau nicht verfugbar', previewUnavailable: 'Vorschau nicht verfugbar',
previewAlt: 'Vorschau',
cutting: 'Schneidet...', cutting: 'Schneidet...',
cut: 'Schneiden', cut: 'Schneiden',
cutSuccess: 'Video erfolgreich geschnitten!', cutSuccess: 'Video erfolgreich geschnitten!',

View File

@ -140,7 +140,6 @@ const UI_TEXT_EN = {
archiveNoMatches: 'No matches.', archiveNoMatches: 'No matches.',
archiveNoRoot: 'Download folder not found. Set a download path in Settings first.', archiveNoRoot: 'Download folder not found. Set a download path in Settings first.',
archiveSearchPlaceholder: 'Search...', archiveSearchPlaceholder: 'Search...',
archiveSearchAria: 'Search archive',
archiveOpen: 'Open', archiveOpen: 'Open',
archiveShowInFolder: 'Folder', archiveShowInFolder: 'Folder',
archiveViewChat: 'Chat', archiveViewChat: 'Chat',
@ -178,7 +177,6 @@ const UI_TEXT_EN = {
downloadPathNotWritable: 'Download folder is not writable. Pick another folder or grant write permission.', downloadPathNotWritable: 'Download folder is not writable. Pick another folder or grant write permission.',
streamerSectionTitle: 'Streamer', streamerSectionTitle: 'Streamer',
streamerListFilterPlaceholder: 'Filter...', streamerListFilterPlaceholder: 'Filter...',
streamerListFilterAria: 'Filter streamer list',
streamerAddAriaLabel: 'Add streamer', streamerAddAriaLabel: 'Add streamer',
streamerBulkRemoveTitle: 'Remove all (or filtered)', streamerBulkRemoveTitle: 'Remove all (or filtered)',
streamerBulkRemoveAll: 'Remove all {count} streamers from the list?', streamerBulkRemoveAll: 'Remove all {count} streamers from the list?',
@ -334,7 +332,6 @@ const UI_TEXT_EN = {
openTwitch: 'Open on Twitch', openTwitch: 'Open on Twitch',
openTwitchTooltip: 'Open this channel on twitch.tv', openTwitchTooltip: 'Open this channel on twitch.tv',
liveCardTooltip: 'Click to start a live recording right now', liveCardTooltip: 'Click to start a live recording right now',
liveThumbAlt: 'Live preview',
recordNow: 'Record now', recordNow: 'Record now',
refresh: 'Refresh', refresh: 'Refresh',
agoMinutes: '{n} min ago', agoMinutes: '{n} min ago',
@ -380,7 +377,6 @@ const UI_TEXT_EN = {
addQueue: '+ Queue', addQueue: '+ Queue',
trimButton: 'Trim VOD', trimButton: 'Trim VOD',
filterPlaceholder: 'Filter by title... (Ctrl+F)', filterPlaceholder: 'Filter by title... (Ctrl+F)',
filterAria: 'Filter VOD titles',
filterClearTitle: 'Clear filter (Esc)', filterClearTitle: 'Clear filter (Esc)',
filterNoMatchTitle: 'No matches', filterNoMatchTitle: 'No matches',
filterNoMatchText: 'No VODs match the current filter.', filterNoMatchText: 'No VODs match the current filter.',
@ -447,7 +443,6 @@ const UI_TEXT_EN = {
videoInfoFailed: 'Could not read video info. Is FFprobe installed?', videoInfoFailed: 'Could not read video info. Is FFprobe installed?',
previewLoading: 'Loading preview...', previewLoading: 'Loading preview...',
previewUnavailable: 'Preview unavailable', previewUnavailable: 'Preview unavailable',
previewAlt: 'Preview',
cutting: 'Cutting...', cutting: 'Cutting...',
cut: 'Cut', cut: 'Cut',
cutSuccess: 'Video cut successfully!', cutSuccess: 'Video cut successfully!',

View File

@ -106,7 +106,7 @@ function renderStreamerProfileCard(p: StreamerProfile): void {
? ` ? `
<div class="streamer-profile-live-card" role="button" tabindex="0" aria-label="${escapeHtml(UI_TEXT.profile.liveCardTooltip)}" onclick="triggerLiveRecordingFromProfile('${safeLogin}')" onkeydown="if((event.key==='Enter'||event.key===' ')&&event.target===event.currentTarget){event.preventDefault();triggerLiveRecordingFromProfile('${safeLogin}');}" title="${escapeHtml(UI_TEXT.profile.liveCardTooltip)}"> <div class="streamer-profile-live-card" role="button" tabindex="0" aria-label="${escapeHtml(UI_TEXT.profile.liveCardTooltip)}" onclick="triggerLiveRecordingFromProfile('${safeLogin}')" onkeydown="if((event.key==='Enter'||event.key===' ')&&event.target===event.currentTarget){event.preventDefault();triggerLiveRecordingFromProfile('${safeLogin}');}" title="${escapeHtml(UI_TEXT.profile.liveCardTooltip)}">
${p.currentStreamPreviewUrl ${p.currentStreamPreviewUrl
? `<img class="streamer-profile-live-thumb" src="${escapeHtml(p.currentStreamPreviewUrl)}" alt="${escapeHtml(UI_TEXT.profile.liveThumbAlt)}" onerror="onProfileLivePreviewError(this)">` ? `<img class="streamer-profile-live-thumb" src="${escapeHtml(p.currentStreamPreviewUrl)}" alt="Live preview" onerror="onProfileLivePreviewError(this)">`
: `<div class="streamer-profile-live-thumb-fallback"></div>`} : `<div class="streamer-profile-live-thumb-fallback"></div>`}
<div class="streamer-profile-live-body"> <div class="streamer-profile-live-body">
<div class="streamer-profile-live-badge-row"> <div class="streamer-profile-live-badge-row">

View File

@ -88,11 +88,6 @@ function applyTemplatePreset(preset: string): void {
byId<HTMLInputElement>('partsFilenameTemplate').value = selected.parts; byId<HTMLInputElement>('partsFilenameTemplate').value = selected.parts;
byId<HTMLInputElement>('defaultClipFilenameTemplate').value = selected.clip; byId<HTMLInputElement>('defaultClipFilenameTemplate').value = selected.clip;
validateFilenameTemplates(); validateFilenameTemplates();
// Programmatic .value = ... does not trigger the 'input' event the
// template inputs listen on for debounced save, so the preset click
// would otherwise look applied but never persist until the user
// types into one of the inputs. Schedule the save explicitly.
scheduleSettingsAutoSave();
} }
async function refreshRuntimeMetrics(showLoading = true): Promise<void> { async function refreshRuntimeMetrics(showLoading = true): Promise<void> {

View File

@ -67,7 +67,6 @@ function applyLanguageToStaticUI(): void {
setText('btnArchiveSearch', UI_TEXT.static.archiveSearchBtn); setText('btnArchiveSearch', UI_TEXT.static.archiveSearchBtn);
const archiveQueryInput = document.getElementById('archiveSearchQuery') as HTMLInputElement | null; const archiveQueryInput = document.getElementById('archiveSearchQuery') as HTMLInputElement | null;
if (archiveQueryInput) archiveQueryInput.placeholder = UI_TEXT.static.archiveSearchPlaceholder; if (archiveQueryInput) archiveQueryInput.placeholder = UI_TEXT.static.archiveSearchPlaceholder;
setAriaLabel('archiveSearchQuery', UI_TEXT.static.archiveSearchAria);
const archiveTypeSelect = document.getElementById('archiveSearchType') as HTMLSelectElement | null; const archiveTypeSelect = document.getElementById('archiveSearchType') as HTMLSelectElement | null;
if (archiveTypeSelect) { if (archiveTypeSelect) {
const opts = archiveTypeSelect.options; const opts = archiveTypeSelect.options;
@ -186,7 +185,6 @@ function applyLanguageToStaticUI(): void {
setText('streamlinkQualityAudio', UI_TEXT.static.streamlinkQualityAudio); setText('streamlinkQualityAudio', UI_TEXT.static.streamlinkQualityAudio);
setText('streamerSectionTitleText', UI_TEXT.static.streamerSectionTitle); setText('streamerSectionTitleText', UI_TEXT.static.streamerSectionTitle);
setPlaceholder('streamerListFilter', UI_TEXT.static.streamerListFilterPlaceholder); setPlaceholder('streamerListFilter', UI_TEXT.static.streamerListFilterPlaceholder);
setAriaLabel('streamerListFilter', UI_TEXT.static.streamerListFilterAria);
setTitle('btnStreamerBulkRemove', UI_TEXT.static.streamerBulkRemoveTitle); setTitle('btnStreamerBulkRemove', UI_TEXT.static.streamerBulkRemoveTitle);
setAriaLabel('btnStreamerBulkRemove', UI_TEXT.static.streamerBulkRemoveTitle); setAriaLabel('btnStreamerBulkRemove', UI_TEXT.static.streamerBulkRemoveTitle);
setAriaLabel('btnAddStreamer', UI_TEXT.static.streamerAddAriaLabel); setAriaLabel('btnAddStreamer', UI_TEXT.static.streamerAddAriaLabel);
@ -295,9 +293,7 @@ function applyLanguageToStaticUI(): void {
setText('updateChangelogToggle', UI_TEXT.updates.showChangelog); setText('updateChangelogToggle', UI_TEXT.updates.showChangelog);
setText('updateChangelogEmpty', UI_TEXT.updates.noChangelog); setText('updateChangelogEmpty', UI_TEXT.updates.noChangelog);
setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder); setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder);
setAriaLabel('newStreamer', UI_TEXT.static.streamerAddAriaLabel);
setPlaceholder('vodFilterInput', UI_TEXT.vods.filterPlaceholder); setPlaceholder('vodFilterInput', UI_TEXT.vods.filterPlaceholder);
setAriaLabel('vodFilterInput', UI_TEXT.vods.filterAria);
setTitle('vodFilterClearBtn', UI_TEXT.vods.filterClearTitle); setTitle('vodFilterClearBtn', UI_TEXT.vods.filterClearTitle);
setAriaLabel('vodFilterClearBtn', UI_TEXT.vods.filterClearTitle); setAriaLabel('vodFilterClearBtn', UI_TEXT.vods.filterClearTitle);
setPlaceholder('chatViewerFilter', UI_TEXT.queue.chatViewerFilterPlaceholder); setPlaceholder('chatViewerFilter', UI_TEXT.queue.chatViewerFilterPlaceholder);

View File

@ -1557,15 +1557,15 @@ async function updatePreview(time: number): Promise<void> {
} }
const preview = byId('cutterPreview'); const preview = byId('cutterPreview');
applyHtml(preview, `<div class="placeholder"><p>${escapeHtml(UI_TEXT.cutter.previewLoading)}</p></div>`); preview.innerHTML = `<div class="placeholder"><p>${UI_TEXT.cutter.previewLoading}</p></div>`;
const frame = await window.api.extractFrame(cutterFile, time); const frame = await window.api.extractFrame(cutterFile, time);
if (frame) { if (frame) {
applyHtml(preview, `<img src="${escapeHtml(frame)}" alt="${escapeHtml(UI_TEXT.cutter.previewAlt)}">`); preview.innerHTML = `<img src="${frame}" alt="Preview">`;
return; return;
} }
applyHtml(preview, `<div class="placeholder"><p>${escapeHtml(UI_TEXT.cutter.previewUnavailable)}</p></div>`); preview.innerHTML = `<div class="placeholder"><p>${UI_TEXT.cutter.previewUnavailable}</p></div>`;
} }
async function startCutting(): Promise<void> { async function startCutting(): Promise<void> {

View File

@ -471,10 +471,10 @@ body {
color: var(--text); color: var(--text);
font-size: 13px; font-size: 13px;
} }
/* No .filter-input:hover here it's redundant with the global
input[type="text"]:hover rule added in 4.6.142 (same effect: soft .filter-input:hover:not(:focus):not(:disabled) {
purple border on hover). The class is always applied to <input border-color: rgba(145, 70, 255, 0.45);
type="text"> elements, so the global rule already covers them. */ }
.filter-input.compact { .filter-input.compact {
width: calc(100% - 16px); width: calc(100% - 16px);