Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e81a47e9e | ||
|
|
70643b4c08 | ||
|
|
86d68466f9 | ||
|
|
ae156ff395 | ||
|
|
2d109077a0 | ||
|
|
25be77b4ab | ||
|
|
29315091c6 | ||
|
|
84f576d131 | ||
|
|
fce353d529 | ||
|
|
7b0e511479 | ||
|
|
6c56c4e908 | ||
|
|
4472e3bf50 | ||
|
|
ce3b876006 | ||
|
|
801e02601f | ||
|
|
65c9d06dfa | ||
|
|
e8404b8802 | ||
|
|
3d40160b5c | ||
|
|
85d2bf5316 | ||
|
|
8f0f7d5d84 | ||
|
|
564d123431 |
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.6.145",
|
"version": "4.6.155",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.6.145",
|
"version": "4.6.155",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.6.145",
|
"version": "4.6.155",
|
||||||
"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",
|
||||||
|
|||||||
@ -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"></div>
|
<div id="eventsViewerStatus" class="viewer-modal-status" role="status" aria-live="polite"></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"></span>
|
<span id="chatViewerStatus" class="viewer-modal-status viewer-modal-status-inline" role="status" aria-live="polite"></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"></div>
|
<div class="clip-status" id="clipStatus" role="status" aria-live="polite"></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"></span>
|
<span id="statsLastScannedLabel" class="form-sublabel" role="status" aria-live="polite"></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"></div>
|
<div id="archiveSearchSummary" class="form-sublabel" role="status" aria-live="polite"></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;"></div>
|
<div id="storageSummary" class="form-sublabel" style="margin-bottom:8px;" role="status" aria-live="polite"></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"></div>
|
<div id="cleanupReport" class="form-note" role="status" aria-live="polite"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
|
|||||||
@ -100,11 +100,10 @@ const UI_TEXT_DE = {
|
|||||||
autoVodScanNow: 'Jetzt scannen',
|
autoVodScanNow: 'Jetzt scannen',
|
||||||
autoRecordScanNow: 'Live-Status pruefen',
|
autoRecordScanNow: 'Live-Status pruefen',
|
||||||
statsTitle: 'Archiv-Statistik',
|
statsTitle: 'Archiv-Statistik',
|
||||||
statsIntro: 'Aggregiert ueber den Download-Ordner. Live-Aufnahmen liegen unter {streamer}/live/, VOD-Downloads direkt unter {streamer}/. Lade-Zeit skaliert mit der Anzahl Dateien.',
|
statsIntro: 'Aggregiert ueber den Download-Ordner. Live-Aufnahmen liegen unter <code>{streamer}/live/</code>, VOD-Downloads direkt unter <code>{streamer}/</code>. Lade-Zeit skaliert mit der Anzahl Dateien.',
|
||||||
statsRefresh: 'Aktualisieren',
|
statsRefresh: 'Aktualisieren',
|
||||||
statsScanning: 'Scanne...',
|
statsScanning: 'Scanne...',
|
||||||
statsScannedAt: 'Letzter Scan',
|
statsScannedAt: 'Letzter Scan',
|
||||||
statsScannedAtNever: 'Noch nicht gescannt',
|
|
||||||
statsSummaryTitle: 'Uebersicht',
|
statsSummaryTitle: 'Uebersicht',
|
||||||
statsTopStreamersTitle: 'Top Streamer (nach Groesse)',
|
statsTopStreamersTitle: 'Top Streamer (nach Groesse)',
|
||||||
statsActivityTitle: 'Aktivitaet (letzte 30 Tage)',
|
statsActivityTitle: 'Aktivitaet (letzte 30 Tage)',
|
||||||
@ -140,6 +139,7 @@ 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,6 +178,7 @@ 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?',
|
||||||
@ -333,6 +334,7 @@ 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',
|
||||||
@ -378,6 +380,7 @@ 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.',
|
||||||
@ -444,6 +447,7 @@ 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!',
|
||||||
|
|||||||
@ -101,11 +101,10 @@ const UI_TEXT_EN = {
|
|||||||
autoVodScanNow: 'Scan now',
|
autoVodScanNow: 'Scan now',
|
||||||
autoRecordScanNow: 'Check live status',
|
autoRecordScanNow: 'Check live status',
|
||||||
statsTitle: 'Archive statistics',
|
statsTitle: 'Archive statistics',
|
||||||
statsIntro: 'Aggregated across the download folder. Live recordings live under {streamer}/live/, VOD downloads under {streamer}/. Scan time scales with file count.',
|
statsIntro: 'Aggregated across the download folder. Live recordings live under <code>{streamer}/live/</code>, VOD downloads under <code>{streamer}/</code>. Scan time scales with file count.',
|
||||||
statsRefresh: 'Refresh',
|
statsRefresh: 'Refresh',
|
||||||
statsScanning: 'Scanning...',
|
statsScanning: 'Scanning...',
|
||||||
statsScannedAt: 'Last scan',
|
statsScannedAt: 'Last scan',
|
||||||
statsScannedAtNever: 'Not yet scanned',
|
|
||||||
statsSummaryTitle: 'Overview',
|
statsSummaryTitle: 'Overview',
|
||||||
statsTopStreamersTitle: 'Top streamers (by size)',
|
statsTopStreamersTitle: 'Top streamers (by size)',
|
||||||
statsActivityTitle: 'Activity (last 30 days)',
|
statsActivityTitle: 'Activity (last 30 days)',
|
||||||
@ -141,6 +140,7 @@ 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,6 +178,7 @@ 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?',
|
||||||
@ -333,6 +334,7 @@ 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',
|
||||||
@ -378,6 +380,7 @@ 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.',
|
||||||
@ -444,6 +447,7 @@ 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!',
|
||||||
|
|||||||
@ -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="Live preview" onerror="onProfileLivePreviewError(this)">`
|
? `<img class="streamer-profile-live-thumb" src="${escapeHtml(p.currentStreamPreviewUrl)}" alt="${escapeHtml(UI_TEXT.profile.liveThumbAlt)}" 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">
|
||||||
|
|||||||
@ -88,6 +88,11 @@ 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> {
|
||||||
|
|||||||
@ -67,6 +67,7 @@ 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;
|
||||||
@ -85,6 +86,8 @@ function applyLanguageToStaticUI(): void {
|
|||||||
}
|
}
|
||||||
setText('navSettingsText', UI_TEXT.static.navSettings);
|
setText('navSettingsText', UI_TEXT.static.navSettings);
|
||||||
setText('statsTitle', UI_TEXT.static.statsTitle);
|
setText('statsTitle', UI_TEXT.static.statsTitle);
|
||||||
|
const statsIntroEl = document.getElementById('statsIntro');
|
||||||
|
if (statsIntroEl) applyHtml(statsIntroEl, UI_TEXT.static.statsIntro);
|
||||||
setText('statsSummaryTitle', UI_TEXT.static.statsSummaryTitle);
|
setText('statsSummaryTitle', UI_TEXT.static.statsSummaryTitle);
|
||||||
setText('statsTopStreamersTitle', UI_TEXT.static.statsTopStreamersTitle);
|
setText('statsTopStreamersTitle', UI_TEXT.static.statsTopStreamersTitle);
|
||||||
setText('statsActivityTitle', UI_TEXT.static.statsActivityTitle);
|
setText('statsActivityTitle', UI_TEXT.static.statsActivityTitle);
|
||||||
@ -183,6 +186,7 @@ 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);
|
||||||
@ -291,7 +295,9 @@ 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);
|
||||||
|
|||||||
@ -1557,15 +1557,15 @@ async function updatePreview(time: number): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preview = byId('cutterPreview');
|
const preview = byId('cutterPreview');
|
||||||
preview.innerHTML = `<div class="placeholder"><p>${UI_TEXT.cutter.previewLoading}</p></div>`;
|
applyHtml(preview, `<div class="placeholder"><p>${escapeHtml(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) {
|
||||||
preview.innerHTML = `<img src="${frame}" alt="Preview">`;
|
applyHtml(preview, `<img src="${escapeHtml(frame)}" alt="${escapeHtml(UI_TEXT.cutter.previewAlt)}">`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
preview.innerHTML = `<div class="placeholder"><p>${UI_TEXT.cutter.previewUnavailable}</p></div>`;
|
applyHtml(preview, `<div class="placeholder"><p>${escapeHtml(UI_TEXT.cutter.previewUnavailable)}</p></div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startCutting(): Promise<void> {
|
async function startCutting(): Promise<void> {
|
||||||
|
|||||||
@ -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
|
||||||
.filter-input:hover:not(:focus):not(:disabled) {
|
input[type="text"]:hover rule added in 4.6.142 (same effect: soft
|
||||||
border-color: rgba(145, 70, 255, 0.45);
|
purple border on hover). The class is always applied to <input
|
||||||
}
|
type="text"> elements, so the global rule already covers them. */
|
||||||
|
|
||||||
.filter-input.compact {
|
.filter-input.compact {
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 16px);
|
||||||
@ -3787,11 +3787,6 @@ input[type="number"]::-webkit-outer-spin-button {
|
|||||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 167, 38, 0.25);
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 167, 38, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-toast.error {
|
|
||||||
border-left-color: var(--error);
|
|
||||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 70, 70, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
STREAMER SECTION COUNTER
|
STREAMER SECTION COUNTER
|
||||||
============================================
|
============================================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user