Add live DE/EN language switching without restart (v3.8.6)
Introduce a centralized renderer text dictionary with German and English locales, add a language selector in settings, and apply translations instantly across UI sections and dynamic status labels without requiring app restart.
This commit is contained in:
parent
59b507115c
commit
7fb7e4b03b
4
typescript-version/package-lock.json
generated
4
typescript-version/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "3.8.5",
|
||||
"version": "3.8.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "3.8.5",
|
||||
"version": "3.8.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "3.8.5",
|
||||
"version": "3.8.6",
|
||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||
"main": "dist/main.js",
|
||||
"author": "xRangerDE",
|
||||
|
||||
@ -93,29 +93,29 @@
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 24 24"><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/></svg>
|
||||
Twitch VOD Manager
|
||||
<span id="logoText">Twitch VOD Manager</span>
|
||||
</div>
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-item active" data-tab="vods" onclick="showTab('vods')">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 8l7 4-7 4V8z"/></svg>
|
||||
Twitch VODs
|
||||
<span id="navVodsText">Twitch VODs</span>
|
||||
</div>
|
||||
<div class="nav-item" data-tab="clips" onclick="showTab('clips')">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
||||
Twitch Clips
|
||||
<span id="navClipsText">Twitch Clips</span>
|
||||
</div>
|
||||
<div class="nav-item" data-tab="cutter" onclick="showTab('cutter')">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3h-3z"/></svg>
|
||||
Video schneiden
|
||||
<span id="navCutterText">Video schneiden</span>
|
||||
</div>
|
||||
<div class="nav-item" data-tab="merge" onclick="showTab('merge')">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 20.41L18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z"/></svg>
|
||||
Videos Zusammenfugen
|
||||
<span id="navMergeText">Videos zusammenfugen</span>
|
||||
</div>
|
||||
<div class="nav-item" data-tab="settings" onclick="showTab('settings')">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
||||
Einstellungen
|
||||
<span id="navSettingsText">Einstellungen</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@ -124,13 +124,13 @@
|
||||
|
||||
<div class="queue-section">
|
||||
<div class="queue-header">
|
||||
<span class="queue-title">Warteschlange</span>
|
||||
<span class="queue-title" id="queueTitleText">Warteschlange</span>
|
||||
<span class="queue-count" id="queueCount">0</span>
|
||||
</div>
|
||||
<div class="queue-list" id="queueList"></div>
|
||||
<div class="queue-actions">
|
||||
<button class="btn btn-start" id="btnStart" onclick="toggleDownload()">Start</button>
|
||||
<button class="btn btn-clear" onclick="clearCompleted()">Leeren</button>
|
||||
<button class="btn btn-clear" id="btnClear" onclick="clearCompleted()">Leeren</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@ -145,7 +145,7 @@
|
||||
</div>
|
||||
<button class="btn-icon" onclick="refreshVODs()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||||
Aktualisieren
|
||||
<span id="refreshText">Aktualisieren</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@ -165,18 +165,19 @@
|
||||
<!-- Clips Tab -->
|
||||
<div class="tab-content" id="clipsTab">
|
||||
<div class="clip-input">
|
||||
<h2>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/...">
|
||||
<button class="btn-primary" onclick="downloadClip()" id="btnClip">Clip herunterladen</button>
|
||||
<div class="clip-status" id="clipStatus"></div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card" style="max-width: 600px; margin: 20px auto;">
|
||||
<h3>Info</h3>
|
||||
<p style="color: var(--text-secondary); line-height: 1.6;">
|
||||
Unterstutzte Formate:<br>
|
||||
- https://clips.twitch.tv/ClipName<br>
|
||||
- https://www.twitch.tv/streamer/clip/ClipName<br><br>
|
||||
<h3 id="clipsInfoTitle">Info</h3>
|
||||
<p style="color: var(--text-secondary); line-height: 1.6; white-space: pre-line;" id="clipsInfoText">
|
||||
Unterstutzte Formate:
|
||||
- https://clips.twitch.tv/ClipName
|
||||
- https://www.twitch.tv/streamer/clip/ClipName
|
||||
|
||||
Clips werden im Download-Ordner unter "Clips/StreamerName/" gespeichert.
|
||||
</p>
|
||||
</div>
|
||||
@ -186,10 +187,10 @@
|
||||
<div class="tab-content" id="cutterTab">
|
||||
<div class="cutter-container">
|
||||
<div class="settings-card">
|
||||
<h3>Video auswahlen</h3>
|
||||
<h3 id="cutterSelectTitle">Video auswahlen</h3>
|
||||
<div class="form-row">
|
||||
<input type="text" id="cutterFilePath" readonly placeholder="Keine Datei ausgewahlt...">
|
||||
<button class="btn-secondary" onclick="selectCutterVideo()">Durchsuchen</button>
|
||||
<button class="btn-secondary" id="cutterBrowseBtn" onclick="selectCutterVideo()">Durchsuchen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -254,12 +255,12 @@
|
||||
<div class="tab-content" id="mergeTab">
|
||||
<div class="merge-container">
|
||||
<div class="settings-card">
|
||||
<h3>Videos zusammenfugen</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 15px;">
|
||||
<h3 id="mergeTitle">Videos zusammenfugen</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 15px;" id="mergeDesc">
|
||||
Wahle mehrere Videos aus um sie zu einem Video zusammenzufugen.
|
||||
Die Reihenfolge kann per Drag & Drop geandert werden.
|
||||
</p>
|
||||
<button class="btn-secondary" onclick="addMergeFiles()">+ Videos hinzufugen</button>
|
||||
<button class="btn-secondary" id="mergeAddBtn" onclick="addMergeFiles()">+ Videos hinzufugen</button>
|
||||
</div>
|
||||
|
||||
<div class="file-list" id="mergeFileList">
|
||||
@ -285,9 +286,9 @@
|
||||
<!-- Settings Tab -->
|
||||
<div class="tab-content" id="settingsTab">
|
||||
<div class="settings-card">
|
||||
<h3>Design</h3>
|
||||
<h3 id="designTitle">Design</h3>
|
||||
<div class="form-group">
|
||||
<label>Theme</label>
|
||||
<label id="themeLabel">Theme</label>
|
||||
<select id="themeSelect" onchange="changeTheme(this.value)">
|
||||
<option value="twitch">Twitch</option>
|
||||
<option value="discord">Discord</option>
|
||||
@ -295,48 +296,55 @@
|
||||
<option value="apple">Apple</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label id="languageLabel">Sprache</label>
|
||||
<select id="languageSelect" onchange="changeLanguage(this.value)">
|
||||
<option value="de" id="languageDeText">Deutsch</option>
|
||||
<option value="en" id="languageEnText">Englisch</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Twitch API</h3>
|
||||
<h3 id="apiTitle">Twitch API</h3>
|
||||
<div class="form-group">
|
||||
<label>Client ID</label>
|
||||
<label id="clientIdLabel">Client ID</label>
|
||||
<input type="text" id="clientId" placeholder="Twitch Client ID">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Client Secret</label>
|
||||
<label id="clientSecretLabel">Client Secret</label>
|
||||
<input type="password" id="clientSecret" placeholder="Twitch Client Secret">
|
||||
</div>
|
||||
<button class="btn-primary" onclick="saveSettings()">Speichern & Verbinden</button>
|
||||
<button class="btn-primary" id="saveSettingsBtn" onclick="saveSettings()">Speichern & Verbinden</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Download-Einstellungen</h3>
|
||||
<h3 id="downloadSettingsTitle">Download-Einstellungen</h3>
|
||||
<div class="form-group">
|
||||
<label>Speicherort</label>
|
||||
<label id="storageLabel">Speicherort</label>
|
||||
<div class="form-row">
|
||||
<input type="text" id="downloadPath" readonly>
|
||||
<button class="btn-secondary" onclick="selectFolder()">Ordner</button>
|
||||
<button class="btn-secondary" onclick="openFolder()">Offnen</button>
|
||||
<button class="btn-secondary" id="openFolderBtn" onclick="openFolder()">Offnen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Download-Modus</label>
|
||||
<label id="modeLabel">Download-Modus</label>
|
||||
<select id="downloadMode">
|
||||
<option value="full">Ganzes VOD</option>
|
||||
<option value="parts">In Teile splitten</option>
|
||||
<option value="full" id="modeFullText">Ganzes VOD</option>
|
||||
<option value="parts" id="modePartsText">In Teile splitten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Teil-Lange (Minuten)</label>
|
||||
<label id="partMinutesLabel">Teil-Lange (Minuten)</label>
|
||||
<input type="number" id="partMinutes" value="120" min="10" max="480">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Updates</h3>
|
||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.8.5</p>
|
||||
<button class="btn-secondary" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||
<h3 id="updateTitle">Updates</h3>
|
||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.8.6</p>
|
||||
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -346,11 +354,12 @@
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Nicht verbunden</span>
|
||||
</div>
|
||||
<span id="versionText">v3.8.5</span>
|
||||
<span id="versionText">v3.8.6</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="../dist/renderer-texts.js"></script>
|
||||
<script src="../dist/renderer-shared.js"></script>
|
||||
<script src="../dist/renderer-settings.js"></script>
|
||||
<script src="../dist/renderer-streamers.js"></script>
|
||||
|
||||
@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
|
||||
// ==========================================
|
||||
// CONFIG & CONSTANTS
|
||||
// ==========================================
|
||||
const APP_VERSION = '3.8.5';
|
||||
const APP_VERSION = '3.8.6';
|
||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||
|
||||
// Paths
|
||||
@ -43,6 +43,7 @@ interface Config {
|
||||
theme: string;
|
||||
download_mode: 'parts' | 'full';
|
||||
part_minutes: number;
|
||||
language: 'de' | 'en';
|
||||
}
|
||||
|
||||
interface VOD {
|
||||
@ -116,7 +117,8 @@ const defaultConfig: Config = {
|
||||
streamers: [],
|
||||
theme: 'twitch',
|
||||
download_mode: 'full',
|
||||
part_minutes: 120
|
||||
part_minutes: 120,
|
||||
language: 'de'
|
||||
};
|
||||
|
||||
function loadConfig(): Config {
|
||||
|
||||
1
typescript-version/src/renderer-globals.d.ts
vendored
1
typescript-version/src/renderer-globals.d.ts
vendored
@ -6,6 +6,7 @@ interface AppConfig {
|
||||
theme?: string;
|
||||
download_mode?: 'parts' | 'full';
|
||||
part_minutes?: number;
|
||||
language?: 'de' | 'en';
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
@ -20,18 +20,18 @@ async function clearCompleted(): Promise<void> {
|
||||
}
|
||||
|
||||
function getQueueStatusLabel(item: QueueItem): string {
|
||||
if (item.status === 'completed') return 'Abgeschlossen';
|
||||
if (item.status === 'error') return 'Fehlgeschlagen';
|
||||
if (item.status === 'downloading') return 'Laeuft';
|
||||
return 'Wartet';
|
||||
if (item.status === 'completed') return UI_TEXT.queue.statusDone;
|
||||
if (item.status === 'error') return UI_TEXT.queue.statusFailed;
|
||||
if (item.status === 'downloading') return UI_TEXT.queue.statusRunning;
|
||||
return UI_TEXT.queue.statusWaiting;
|
||||
}
|
||||
|
||||
function getQueueProgressText(item: QueueItem): string {
|
||||
if (item.status === 'completed') return '100%';
|
||||
if (item.status === 'error') return 'Fehler';
|
||||
if (item.status === 'pending') return 'Bereit';
|
||||
if (item.status === 'error') return UI_TEXT.queue.progressError;
|
||||
if (item.status === 'pending') return UI_TEXT.queue.progressReady;
|
||||
if (item.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`;
|
||||
return item.progressStatus || 'Lade...';
|
||||
return item.progressStatus || UI_TEXT.queue.progressLoading;
|
||||
}
|
||||
|
||||
function getQueueMetaText(item: QueueItem): string {
|
||||
@ -42,31 +42,31 @@ function getQueueMetaText(item: QueueItem): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (item.currentPart && item.totalParts) {
|
||||
parts.push(`Teil ${item.currentPart}/${item.totalParts}`);
|
||||
parts.push(`${UI_TEXT.queue.part} ${item.currentPart}/${item.totalParts}`);
|
||||
}
|
||||
|
||||
if (item.speed) {
|
||||
parts.push(`Geschwindigkeit: ${item.speed}`);
|
||||
parts.push(`${UI_TEXT.queue.speed}: ${item.speed}`);
|
||||
}
|
||||
|
||||
if (item.eta) {
|
||||
parts.push(`Restzeit: ${item.eta}`);
|
||||
parts.push(`${UI_TEXT.queue.eta}: ${item.eta}`);
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'pending') {
|
||||
parts.push('Bereit zum Download');
|
||||
parts.push(UI_TEXT.queue.readyToDownload);
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'downloading') {
|
||||
parts.push(item.progressStatus || 'Download gestartet');
|
||||
parts.push(item.progressStatus || UI_TEXT.queue.started);
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'completed') {
|
||||
parts.push('Fertig');
|
||||
parts.push(UI_TEXT.queue.done);
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'error') {
|
||||
parts.push('Download fehlgeschlagen');
|
||||
parts.push(UI_TEXT.queue.failed);
|
||||
}
|
||||
|
||||
return parts.join(' | ');
|
||||
@ -81,12 +81,12 @@ function renderQueue(): void {
|
||||
byId('queueCount').textContent = String(queue.length);
|
||||
|
||||
if (queue.length === 0) {
|
||||
list.innerHTML = '<div style="color: var(--text-secondary); font-size: 12px; text-align: center; padding: 15px;">Keine Downloads in der Warteschlange</div>';
|
||||
list.innerHTML = `<div style="color: var(--text-secondary); font-size: 12px; text-align: center; padding: 15px;">${UI_TEXT.queue.empty}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = queue.map((item: QueueItem) => {
|
||||
const safeTitle = escapeHtml(item.title || 'Untitled');
|
||||
const safeTitle = escapeHtml(item.title || UI_TEXT.vods.untitled);
|
||||
const safeStatusLabel = escapeHtml(getQueueStatusLabel(item));
|
||||
const safeProgressText = escapeHtml(getQueueProgressText(item));
|
||||
const safeMeta = escapeHtml(getQueueMetaText(item));
|
||||
@ -126,6 +126,6 @@ async function toggleDownload(): Promise<void> {
|
||||
const started = await window.api.startDownload();
|
||||
if (!started) {
|
||||
renderQueue();
|
||||
alert('Die Warteschlange ist leer. Fuge zuerst ein VOD oder einen Clip hinzu.');
|
||||
alert(UI_TEXT.queue.emptyAlert);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,14 +2,14 @@ async function connect(): Promise<void> {
|
||||
const hasCredentials = Boolean((config.client_id ?? '').toString().trim() && (config.client_secret ?? '').toString().trim());
|
||||
if (!hasCredentials) {
|
||||
isConnected = false;
|
||||
updateStatus('Ohne Login (Public Modus)', false);
|
||||
updateStatus(UI_TEXT.status.noLogin, false);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus('Verbinde...', false);
|
||||
updateStatus(UI_TEXT.status.connecting, false);
|
||||
const success = await window.api.login();
|
||||
isConnected = success;
|
||||
updateStatus(success ? 'Verbunden' : 'Verbindung fehlgeschlagen - Public Modus aktiv', success);
|
||||
updateStatus(success ? UI_TEXT.status.connected : UI_TEXT.status.connectFailedPublic, success);
|
||||
}
|
||||
|
||||
function updateStatus(text: string, connected: boolean): void {
|
||||
@ -19,6 +19,22 @@ function updateStatus(text: string, connected: boolean): void {
|
||||
dot.classList.add(connected ? 'connected' : 'error');
|
||||
}
|
||||
|
||||
function changeLanguage(lang: string): void {
|
||||
const normalized = setLanguage(lang);
|
||||
byId<HTMLSelectElement>('languageSelect').value = normalized;
|
||||
config.language = normalized;
|
||||
void window.api.saveConfig({ language: normalized });
|
||||
|
||||
const currentStatus = byId('statusText').textContent?.trim() || '';
|
||||
updateStatus(localizeCurrentStatusText(currentStatus), isConnected);
|
||||
|
||||
renderQueue();
|
||||
renderStreamers();
|
||||
if (!currentStreamer) {
|
||||
byId('pageTitle').textContent = UI_TEXT.tabs.vods;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(): Promise<void> {
|
||||
const clientId = byId<HTMLInputElement>('clientId').value.trim();
|
||||
const clientSecret = byId<HTMLInputElement>('clientSecret').value.trim();
|
||||
|
||||
@ -43,8 +43,8 @@ async function removeStreamer(name: string): Promise<void> {
|
||||
byId('vodGrid').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-9 14l-5-4 5-4v8zm2-8l5 4-5 4V9z"/></svg>
|
||||
<h3>Keine VODs</h3>
|
||||
<p>Wahle einen Streamer aus der Liste.</p>
|
||||
<h3>${UI_TEXT.vods.noneTitle}</h3>
|
||||
<p>${UI_TEXT.vods.noneText}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -59,14 +59,14 @@ async function selectStreamer(name: string): Promise<void> {
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
updateStatus('Ohne Login (Public Modus)', false);
|
||||
updateStatus(UI_TEXT.status.noLogin, false);
|
||||
}
|
||||
|
||||
byId('vodGrid').innerHTML = '<div class="empty-state"><p>Lade VODs...</p></div>';
|
||||
byId('vodGrid').innerHTML = `<div class="empty-state"><p>${UI_TEXT.vods.loading}</p></div>`;
|
||||
|
||||
const userId = await window.api.getUserId(name);
|
||||
if (!userId) {
|
||||
byId('vodGrid').innerHTML = '<div class="empty-state"><h3>Streamer nicht gefunden</h3></div>';
|
||||
byId('vodGrid').innerHTML = `<div class="empty-state"><h3>${UI_TEXT.vods.notFound}</h3></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ function renderVODs(vods: VOD[] | null | undefined, streamer: string): void {
|
||||
const grid = byId('vodGrid');
|
||||
|
||||
if (!vods || vods.length === 0) {
|
||||
grid.innerHTML = '<div class="empty-state"><h3>Keine VODs gefunden</h3><p>Dieser Streamer hat keine VODs.</p></div>';
|
||||
grid.innerHTML = `<div class="empty-state"><h3>${UI_TEXT.vods.noResultsTitle}</h3><p>${UI_TEXT.vods.noResultsText}</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ function renderVODs(vods: VOD[] | null | undefined, streamer: string): void {
|
||||
const thumb = vod.thumbnail_url.replace('%{width}', '320').replace('%{height}', '180');
|
||||
const date = new Date(vod.created_at).toLocaleDateString('de-DE');
|
||||
const escapedTitle = vod.title.replace(/'/g, "\\'").replace(/\"/g, '"');
|
||||
const safeDisplayTitle = escapeHtml(vod.title || 'Unbenanntes VOD');
|
||||
const safeDisplayTitle = escapeHtml(vod.title || UI_TEXT.vods.untitled);
|
||||
|
||||
return `
|
||||
<div class="vod-card">
|
||||
@ -96,12 +96,12 @@ function renderVODs(vods: VOD[] | null | undefined, streamer: string): void {
|
||||
<div class="vod-meta">
|
||||
<span>${date}</span>
|
||||
<span>${vod.duration}</span>
|
||||
<span>${vod.view_count.toLocaleString()} Aufrufe</span>
|
||||
<span>${vod.view_count.toLocaleString()} ${UI_TEXT.vods.views}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vod-actions">
|
||||
<button class="vod-btn secondary" onclick="openClipDialog('${vod.url}', '${escapedTitle}', '${vod.created_at}', '${streamer}', '${vod.duration}')">Clip</button>
|
||||
<button class="vod-btn primary" onclick="addToQueue('${vod.url}', '${escapedTitle}', '${vod.created_at}', '${streamer}', '${vod.duration}')">+ Warteschlange</button>
|
||||
<button class="vod-btn primary" onclick="addToQueue('${vod.url}', '${escapedTitle}', '${vod.created_at}', '${streamer}', '${vod.duration}')">${UI_TEXT.vods.addQueue}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
335
typescript-version/src/renderer-texts.ts
Normal file
335
typescript-version/src/renderer-texts.ts
Normal file
@ -0,0 +1,335 @@
|
||||
type LanguageCode = 'de' | 'en';
|
||||
|
||||
const UI_TEXTS = {
|
||||
de: {
|
||||
appName: 'Twitch VOD Manager',
|
||||
static: {
|
||||
navVods: 'Twitch VODs',
|
||||
navClips: 'Twitch Clips',
|
||||
navCutter: 'Video schneiden',
|
||||
navMerge: 'Videos zusammenfugen',
|
||||
navSettings: 'Einstellungen',
|
||||
queueTitle: 'Warteschlange',
|
||||
clearQueue: 'Leeren',
|
||||
refresh: 'Aktualisieren',
|
||||
streamerPlaceholder: 'Streamer hinzufugen...',
|
||||
clipsHeading: 'Twitch Clip-Download',
|
||||
clipsInfoTitle: 'Info',
|
||||
clipsInfoText: 'Unterstutzte Formate:\n- https://clips.twitch.tv/ClipName\n- https://www.twitch.tv/streamer/clip/ClipName\n\nClips werden im Download-Ordner unter "Clips/StreamerName/" gespeichert.',
|
||||
cutterSelectTitle: 'Video auswahlen',
|
||||
cutterBrowse: 'Durchsuchen',
|
||||
mergeTitle: 'Videos zusammenfugen',
|
||||
mergeDesc: 'Wahle mehrere Videos aus, um sie zu einem Video zusammenzufugen. Die Reihenfolge kann geandert werden.',
|
||||
mergeAdd: '+ Videos hinzufugen',
|
||||
designTitle: 'Design',
|
||||
themeLabel: 'Theme',
|
||||
languageLabel: 'Sprache',
|
||||
languageDe: 'Deutsch',
|
||||
languageEn: 'Englisch',
|
||||
apiTitle: 'Twitch API',
|
||||
clientIdLabel: 'Client ID',
|
||||
clientSecretLabel: 'Client Secret',
|
||||
saveSettings: 'Speichern & Verbinden',
|
||||
downloadSettingsTitle: 'Download-Einstellungen',
|
||||
storageLabel: 'Speicherort',
|
||||
openFolder: 'Offnen',
|
||||
modeLabel: 'Download-Modus',
|
||||
modeFull: 'Ganzes VOD',
|
||||
modeParts: 'In Teile splitten',
|
||||
partMinutesLabel: 'Teil-Lange (Minuten)',
|
||||
updateTitle: 'Updates',
|
||||
checkUpdates: 'Nach Updates suchen',
|
||||
notConnected: 'Nicht verbunden'
|
||||
},
|
||||
status: {
|
||||
noLogin: 'Ohne Login (Public Modus)',
|
||||
connecting: 'Verbinde...',
|
||||
connected: 'Verbunden',
|
||||
connectFailedPublic: 'Verbindung fehlgeschlagen - Public Modus aktiv'
|
||||
},
|
||||
tabs: {
|
||||
vods: 'VODs',
|
||||
clips: 'Clips',
|
||||
cutter: 'Video schneiden',
|
||||
merge: 'Videos zusammenfugen',
|
||||
settings: 'Einstellungen'
|
||||
},
|
||||
queue: {
|
||||
empty: 'Keine Downloads in der Warteschlange',
|
||||
start: 'Start',
|
||||
stop: 'Stoppen',
|
||||
statusDone: 'Abgeschlossen',
|
||||
statusFailed: 'Fehlgeschlagen',
|
||||
statusRunning: 'Laeuft',
|
||||
statusWaiting: 'Wartet',
|
||||
progressError: 'Fehler',
|
||||
progressReady: 'Bereit',
|
||||
progressLoading: 'Lade...',
|
||||
readyToDownload: 'Bereit zum Download',
|
||||
started: 'Download gestartet',
|
||||
done: 'Fertig',
|
||||
failed: 'Download fehlgeschlagen',
|
||||
speed: 'Geschwindigkeit',
|
||||
eta: 'Restzeit',
|
||||
part: 'Teil',
|
||||
emptyAlert: 'Die Warteschlange ist leer. Fuge zuerst ein VOD oder einen Clip hinzu.'
|
||||
},
|
||||
vods: {
|
||||
noneTitle: 'Keine VODs',
|
||||
noneText: 'Wahle einen Streamer aus der Liste.',
|
||||
loading: 'Lade VODs...',
|
||||
notFound: 'Streamer nicht gefunden',
|
||||
noResultsTitle: 'Keine VODs gefunden',
|
||||
noResultsText: 'Dieser Streamer hat keine VODs.',
|
||||
untitled: 'Unbenanntes VOD',
|
||||
views: 'Aufrufe',
|
||||
addQueue: '+ Warteschlange'
|
||||
},
|
||||
clips: {
|
||||
dialogTitle: 'Clip zuschneiden',
|
||||
invalidDuration: 'Ungultig!',
|
||||
endBeforeStart: 'Endzeit muss grosser als Startzeit sein!',
|
||||
outOfRange: 'Zeit ausserhalb des VOD-Bereichs!',
|
||||
enterUrl: 'Bitte URL eingeben',
|
||||
loadingButton: 'Lade...',
|
||||
loadingStatus: 'Download laeuft...',
|
||||
downloadButton: 'Clip herunterladen',
|
||||
success: 'Download erfolgreich!',
|
||||
errorPrefix: 'Fehler: ',
|
||||
unknownError: 'Unbekannter Fehler',
|
||||
formatSimple: '(Standard)',
|
||||
formatTimestamp: '(mit Zeitstempel)'
|
||||
},
|
||||
cutter: {
|
||||
videoInfoFailed: 'Konnte Video-Informationen nicht lesen. FFprobe installiert?',
|
||||
previewLoading: 'Lade Vorschau...',
|
||||
previewUnavailable: 'Vorschau nicht verfugbar',
|
||||
cutting: 'Schneidet...',
|
||||
cut: 'Schneiden',
|
||||
cutSuccess: 'Video erfolgreich geschnitten!',
|
||||
cutFailed: 'Fehler beim Schneiden des Videos.'
|
||||
},
|
||||
merge: {
|
||||
empty: 'Keine Videos ausgewahlt',
|
||||
merging: 'Zusammenfugen...',
|
||||
merge: 'Zusammenfugen',
|
||||
success: 'Videos erfolgreich zusammengefugt!',
|
||||
failed: 'Fehler beim Zusammenfugen der Videos.'
|
||||
},
|
||||
updates: {
|
||||
latest: 'Du hast die neueste Version!',
|
||||
downloading: 'Wird heruntergeladen...',
|
||||
available: 'verfugbar!',
|
||||
downloadNow: 'Jetzt herunterladen',
|
||||
downloadLabel: 'Download',
|
||||
ready: 'bereit zur Installation!',
|
||||
installNow: 'Jetzt installieren'
|
||||
}
|
||||
},
|
||||
en: {
|
||||
appName: 'Twitch VOD Manager',
|
||||
static: {
|
||||
navVods: 'Twitch VODs',
|
||||
navClips: 'Twitch Clips',
|
||||
navCutter: 'Video Cutter',
|
||||
navMerge: 'Merge Videos',
|
||||
navSettings: 'Settings',
|
||||
queueTitle: 'Queue',
|
||||
clearQueue: 'Clear',
|
||||
refresh: 'Refresh',
|
||||
streamerPlaceholder: 'Add streamer...',
|
||||
clipsHeading: 'Twitch Clip Download',
|
||||
clipsInfoTitle: 'Info',
|
||||
clipsInfoText: 'Supported formats:\n- https://clips.twitch.tv/ClipName\n- https://www.twitch.tv/streamer/clip/ClipName\n\nClips are saved in your download folder under "Clips/StreamerName/".',
|
||||
cutterSelectTitle: 'Select video',
|
||||
cutterBrowse: 'Browse',
|
||||
mergeTitle: 'Merge videos',
|
||||
mergeDesc: 'Select multiple videos to merge into one file. You can change the order before merging.',
|
||||
mergeAdd: '+ Add videos',
|
||||
designTitle: 'Design',
|
||||
themeLabel: 'Theme',
|
||||
languageLabel: 'Language',
|
||||
languageDe: 'German',
|
||||
languageEn: 'English',
|
||||
apiTitle: 'Twitch API',
|
||||
clientIdLabel: 'Client ID',
|
||||
clientSecretLabel: 'Client Secret',
|
||||
saveSettings: 'Save & Connect',
|
||||
downloadSettingsTitle: 'Download Settings',
|
||||
storageLabel: 'Storage Path',
|
||||
openFolder: 'Open',
|
||||
modeLabel: 'Download Mode',
|
||||
modeFull: 'Full VOD',
|
||||
modeParts: 'Split into parts',
|
||||
partMinutesLabel: 'Part Length (Minutes)',
|
||||
updateTitle: 'Updates',
|
||||
checkUpdates: 'Check for updates',
|
||||
notConnected: 'Not connected'
|
||||
},
|
||||
status: {
|
||||
noLogin: 'No login (public mode)',
|
||||
connecting: 'Connecting...',
|
||||
connected: 'Connected',
|
||||
connectFailedPublic: 'Connection failed - public mode active'
|
||||
},
|
||||
tabs: {
|
||||
vods: 'VODs',
|
||||
clips: 'Clips',
|
||||
cutter: 'Video Cutter',
|
||||
merge: 'Merge Videos',
|
||||
settings: 'Settings'
|
||||
},
|
||||
queue: {
|
||||
empty: 'No downloads in queue',
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
statusDone: 'Completed',
|
||||
statusFailed: 'Failed',
|
||||
statusRunning: 'Running',
|
||||
statusWaiting: 'Waiting',
|
||||
progressError: 'Error',
|
||||
progressReady: 'Ready',
|
||||
progressLoading: 'Loading...',
|
||||
readyToDownload: 'Ready to download',
|
||||
started: 'Download started',
|
||||
done: 'Done',
|
||||
failed: 'Download failed',
|
||||
speed: 'Speed',
|
||||
eta: 'ETA',
|
||||
part: 'Part',
|
||||
emptyAlert: 'Queue is empty. Add a VOD or clip first.'
|
||||
},
|
||||
vods: {
|
||||
noneTitle: 'No VODs',
|
||||
noneText: 'Select a streamer from the list.',
|
||||
loading: 'Loading VODs...',
|
||||
notFound: 'Streamer not found',
|
||||
noResultsTitle: 'No VODs found',
|
||||
noResultsText: 'This streamer has no VODs.',
|
||||
untitled: 'Untitled VOD',
|
||||
views: 'views',
|
||||
addQueue: '+ Queue'
|
||||
},
|
||||
clips: {
|
||||
dialogTitle: 'Trim clip',
|
||||
invalidDuration: 'Invalid!',
|
||||
endBeforeStart: 'End time must be greater than start time!',
|
||||
outOfRange: 'Time is outside VOD range!',
|
||||
enterUrl: 'Please enter a URL',
|
||||
loadingButton: 'Loading...',
|
||||
loadingStatus: 'Downloading...',
|
||||
downloadButton: 'Download clip',
|
||||
success: 'Download successful!',
|
||||
errorPrefix: 'Error: ',
|
||||
unknownError: 'Unknown error',
|
||||
formatSimple: '(default)',
|
||||
formatTimestamp: '(with timestamp)'
|
||||
},
|
||||
cutter: {
|
||||
videoInfoFailed: 'Could not read video info. Is FFprobe installed?',
|
||||
previewLoading: 'Loading preview...',
|
||||
previewUnavailable: 'Preview unavailable',
|
||||
cutting: 'Cutting...',
|
||||
cut: 'Cut',
|
||||
cutSuccess: 'Video cut successfully!',
|
||||
cutFailed: 'Failed to cut video.'
|
||||
},
|
||||
merge: {
|
||||
empty: 'No videos selected',
|
||||
merging: 'Merging...',
|
||||
merge: 'Merge',
|
||||
success: 'Videos merged successfully!',
|
||||
failed: 'Failed to merge videos.'
|
||||
},
|
||||
updates: {
|
||||
latest: 'You are on the latest version!',
|
||||
downloading: 'Downloading...',
|
||||
available: 'available!',
|
||||
downloadNow: 'Download now',
|
||||
downloadLabel: 'Download',
|
||||
ready: 'ready to install!',
|
||||
installNow: 'Install now'
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
|
||||
let currentLanguage: LanguageCode = 'de';
|
||||
let UI_TEXT: (typeof UI_TEXTS)[LanguageCode] = UI_TEXTS[currentLanguage];
|
||||
|
||||
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 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('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('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('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('updateTitle', UI_TEXT.static.updateTitle);
|
||||
setText('checkUpdateBtn', UI_TEXT.static.checkUpdates);
|
||||
|
||||
setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@ -7,7 +7,7 @@ async function checkUpdate(): Promise<void> {
|
||||
|
||||
setTimeout(() => {
|
||||
if (byId('updateBanner').style.display !== 'flex') {
|
||||
alert('Du hast die neueste Version!');
|
||||
alert(UI_TEXT.updates.latest);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
@ -18,7 +18,7 @@ function downloadUpdate(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
byId('updateButton').textContent = 'Wird heruntergeladen...';
|
||||
byId('updateButton').textContent = UI_TEXT.updates.downloading;
|
||||
byId('updateButton').disabled = true;
|
||||
byId('updateProgress').style.display = 'block';
|
||||
byId('updateProgressBar').classList.add('downloading');
|
||||
@ -27,8 +27,8 @@ function downloadUpdate(): void {
|
||||
|
||||
window.api.onUpdateAvailable((info: UpdateInfo) => {
|
||||
byId('updateBanner').style.display = 'flex';
|
||||
byId('updateText').textContent = `Version ${info.version} verfügbar!`;
|
||||
byId('updateButton').textContent = 'Jetzt herunterladen';
|
||||
byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.available}`;
|
||||
byId('updateButton').textContent = UI_TEXT.updates.downloadNow;
|
||||
});
|
||||
|
||||
window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
|
||||
@ -38,7 +38,7 @@ window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
|
||||
|
||||
const mb = (progress.transferred / 1024 / 1024).toFixed(1);
|
||||
const totalMb = (progress.total / 1024 / 1024).toFixed(1);
|
||||
byId('updateText').textContent = `Download: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`;
|
||||
byId('updateText').textContent = `${UI_TEXT.updates.downloadLabel}: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`;
|
||||
});
|
||||
|
||||
window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
||||
@ -48,7 +48,7 @@ window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
||||
bar.classList.remove('downloading');
|
||||
bar.style.width = '100%';
|
||||
|
||||
byId('updateText').textContent = `Version ${info.version} bereit zur Installation!`;
|
||||
byId('updateButton').textContent = 'Jetzt installieren';
|
||||
byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.ready}`;
|
||||
byId('updateButton').textContent = UI_TEXT.updates.installNow;
|
||||
byId('updateButton').disabled = false;
|
||||
});
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
async function init(): Promise<void> {
|
||||
config = await window.api.getConfig();
|
||||
const language = setLanguage((config.language as string) || 'de');
|
||||
config.language = language;
|
||||
const initialQueue = await window.api.getQueue();
|
||||
queue = Array.isArray(initialQueue) ? initialQueue : [];
|
||||
const version = await window.api.getVersion();
|
||||
|
||||
byId('versionText').textContent = `v${version}`;
|
||||
byId('versionInfo').textContent = `Version: v${version}`;
|
||||
document.title = `Twitch VOD Manager v${version}`;
|
||||
document.title = `${UI_TEXT.appName} v${version}`;
|
||||
|
||||
byId<HTMLInputElement>('clientId').value = config.client_id ?? '';
|
||||
byId<HTMLInputElement>('clientSecret').value = config.client_secret ?? '';
|
||||
byId<HTMLInputElement>('downloadPath').value = config.download_path ?? '';
|
||||
byId<HTMLSelectElement>('themeSelect').value = config.theme ?? 'twitch';
|
||||
byId<HTMLSelectElement>('languageSelect').value = config.language ?? 'de';
|
||||
byId<HTMLSelectElement>('downloadMode').value = config.download_mode ?? 'full';
|
||||
byId<HTMLInputElement>('partMinutes').value = String(config.part_minutes ?? 120);
|
||||
|
||||
@ -66,7 +69,7 @@ async function init(): Promise<void> {
|
||||
if (config.client_id && config.client_secret) {
|
||||
await connect();
|
||||
} else {
|
||||
updateStatus('Ohne Login (Public Modus)', false);
|
||||
updateStatus(UI_TEXT.status.noLogin, false);
|
||||
}
|
||||
|
||||
if (config.streamers && config.streamers.length > 0) {
|
||||
@ -111,7 +114,7 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] {
|
||||
|
||||
function updateDownloadButtonState(): void {
|
||||
const btn = byId('btnStart');
|
||||
btn.textContent = downloading ? 'Stoppen' : 'Start';
|
||||
btn.textContent = downloading ? UI_TEXT.queue.stop : UI_TEXT.queue.start;
|
||||
btn.classList.toggle('downloading', downloading);
|
||||
}
|
||||
|
||||
@ -134,15 +137,9 @@ function showTab(tab: string): void {
|
||||
query(`.nav-item[data-tab="${tab}"]`).classList.add('active');
|
||||
byId(tab + 'Tab').classList.add('active');
|
||||
|
||||
const titles: Record<string, string> = {
|
||||
vods: 'VODs',
|
||||
clips: 'Clips',
|
||||
cutter: 'Video schneiden',
|
||||
merge: 'Videos Zusammenfugen',
|
||||
settings: 'Einstellungen'
|
||||
};
|
||||
const titles: Record<string, string> = UI_TEXT.tabs;
|
||||
|
||||
byId('pageTitle').textContent = currentStreamer || titles[tab] || 'Twitch VOD Manager';
|
||||
byId('pageTitle').textContent = currentStreamer || titles[tab] || UI_TEXT.appName;
|
||||
}
|
||||
|
||||
function parseDurationToSeconds(durStr: string): number {
|
||||
@ -185,7 +182,7 @@ function openClipDialog(url: string, title: string, date: string, streamer: stri
|
||||
clipDialogData = { url, title, date, streamer, duration };
|
||||
clipTotalSeconds = parseDurationToSeconds(duration);
|
||||
|
||||
byId('clipDialogTitle').textContent = `Clip zuschneiden (${duration})`;
|
||||
byId('clipDialogTitle').textContent = `${UI_TEXT.clips.dialogTitle} (${duration})`;
|
||||
byId<HTMLInputElement>('clipStartSlider').max = String(clipTotalSeconds);
|
||||
byId<HTMLInputElement>('clipEndSlider').max = String(clipTotalSeconds);
|
||||
byId<HTMLInputElement>('clipStartSlider').value = '0';
|
||||
@ -241,7 +238,7 @@ function updateClipDuration(): void {
|
||||
durationDisplay.textContent = formatSecondsToTime(duration);
|
||||
durationDisplay.style.color = '#00c853';
|
||||
} else {
|
||||
durationDisplay.textContent = 'Ungultig!';
|
||||
durationDisplay.textContent = UI_TEXT.clips.invalidDuration;
|
||||
durationDisplay.style.color = '#ff4444';
|
||||
}
|
||||
|
||||
@ -259,8 +256,8 @@ function updateFilenameExamples(): void {
|
||||
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
||||
const timeStr = formatSecondsToTimeDashed(startSec);
|
||||
|
||||
byId('formatSimple').textContent = `${dateStr}_${partNum}.mp4 (Standard)`;
|
||||
byId('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 (mit Zeitstempel)`;
|
||||
byId('formatSimple').textContent = `${dateStr}_${partNum}.mp4 ${UI_TEXT.clips.formatSimple}`;
|
||||
byId('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 ${UI_TEXT.clips.formatTimestamp}`;
|
||||
}
|
||||
|
||||
async function confirmClipDialog(): Promise<void> {
|
||||
@ -275,12 +272,12 @@ async function confirmClipDialog(): Promise<void> {
|
||||
const filenameFormat = query<HTMLInputElement>('input[name="filenameFormat"]:checked').value as 'simple' | 'timestamp';
|
||||
|
||||
if (endSec <= startSec) {
|
||||
alert('Endzeit muss grosser als Startzeit sein!');
|
||||
alert(UI_TEXT.clips.endBeforeStart);
|
||||
return;
|
||||
}
|
||||
|
||||
if (startSec < 0 || endSec > clipTotalSeconds) {
|
||||
alert('Zeit ausserhalb des VOD-Bereichs!');
|
||||
alert(UI_TEXT.clips.outOfRange);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -310,28 +307,28 @@ async function downloadClip(): Promise<void> {
|
||||
const btn = byId('btnClip');
|
||||
|
||||
if (!url) {
|
||||
status.textContent = 'Bitte URL eingeben';
|
||||
status.textContent = UI_TEXT.clips.enterUrl;
|
||||
status.className = 'clip-status error';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Lade...';
|
||||
status.textContent = 'Download lauft...';
|
||||
btn.textContent = UI_TEXT.clips.loadingButton;
|
||||
status.textContent = UI_TEXT.clips.loadingStatus;
|
||||
status.className = 'clip-status loading';
|
||||
|
||||
const result = await window.api.downloadClip(url);
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Clip herunterladen';
|
||||
btn.textContent = UI_TEXT.clips.downloadButton;
|
||||
|
||||
if (result.success) {
|
||||
status.textContent = 'Download erfolgreich!';
|
||||
status.textContent = UI_TEXT.clips.success;
|
||||
status.className = 'clip-status success';
|
||||
return;
|
||||
}
|
||||
|
||||
status.textContent = 'Fehler: ' + (result.error || 'Unbekannter Fehler');
|
||||
status.textContent = UI_TEXT.clips.errorPrefix + (result.error || UI_TEXT.clips.unknownError);
|
||||
status.className = 'clip-status error';
|
||||
}
|
||||
|
||||
@ -346,7 +343,7 @@ async function selectCutterVideo(): Promise<void> {
|
||||
|
||||
const info = await window.api.getVideoInfo(filePath);
|
||||
if (!info) {
|
||||
alert('Konnte Video-Informationen nicht lesen. FFprobe installiert?');
|
||||
alert(UI_TEXT.cutter.videoInfoFailed);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -436,7 +433,7 @@ async function updatePreview(time: number): Promise<void> {
|
||||
}
|
||||
|
||||
const preview = byId('cutterPreview');
|
||||
preview.innerHTML = '<div class="placeholder"><p>Lade Vorschau...</p></div>';
|
||||
preview.innerHTML = `<div class="placeholder"><p>${UI_TEXT.cutter.previewLoading}</p></div>`;
|
||||
|
||||
const frame = await window.api.extractFrame(cutterFile, time);
|
||||
if (frame) {
|
||||
@ -444,7 +441,7 @@ async function updatePreview(time: number): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
preview.innerHTML = '<div class="placeholder"><p>Vorschau nicht verfugbar</p></div>';
|
||||
preview.innerHTML = `<div class="placeholder"><p>${UI_TEXT.cutter.previewUnavailable}</p></div>`;
|
||||
}
|
||||
|
||||
async function startCutting(): Promise<void> {
|
||||
@ -454,22 +451,22 @@ async function startCutting(): Promise<void> {
|
||||
|
||||
isCutting = true;
|
||||
byId('btnCut').disabled = true;
|
||||
byId('btnCut').textContent = 'Schneidet...';
|
||||
byId('btnCut').textContent = UI_TEXT.cutter.cutting;
|
||||
byId('cutProgress').classList.add('show');
|
||||
|
||||
const result = await window.api.cutVideo(cutterFile, cutterStartTime, cutterEndTime);
|
||||
|
||||
isCutting = false;
|
||||
byId('btnCut').disabled = false;
|
||||
byId('btnCut').textContent = 'Schneiden';
|
||||
byId('btnCut').textContent = UI_TEXT.cutter.cut;
|
||||
byId('cutProgress').classList.remove('show');
|
||||
|
||||
if (result.success) {
|
||||
alert('Video erfolgreich geschnitten!\n\n' + result.outputFile);
|
||||
alert(`${UI_TEXT.cutter.cutSuccess}\n\n${result.outputFile}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('Fehler beim Schneiden des Videos.');
|
||||
alert(UI_TEXT.cutter.cutFailed);
|
||||
}
|
||||
|
||||
async function addMergeFiles(): Promise<void> {
|
||||
@ -490,7 +487,7 @@ function renderMergeFiles(): void {
|
||||
list.innerHTML = `
|
||||
<div class="empty-state" style="padding: 40px 20px;">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" style="opacity:0.3"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||
<p style="margin-top:10px">Keine Videos ausgewahlt</p>
|
||||
<p style="margin-top:10px">${UI_TEXT.merge.empty}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@ -541,24 +538,24 @@ async function startMerging(): Promise<void> {
|
||||
|
||||
isMerging = true;
|
||||
byId('btnMerge').disabled = true;
|
||||
byId('btnMerge').textContent = 'Zusammenfugen...';
|
||||
byId('btnMerge').textContent = UI_TEXT.merge.merging;
|
||||
byId('mergeProgress').classList.add('show');
|
||||
|
||||
const result = await window.api.mergeVideos(mergeFiles, outputFile);
|
||||
|
||||
isMerging = false;
|
||||
byId('btnMerge').disabled = false;
|
||||
byId('btnMerge').textContent = 'Zusammenfugen';
|
||||
byId('btnMerge').textContent = UI_TEXT.merge.merge;
|
||||
byId('mergeProgress').classList.remove('show');
|
||||
|
||||
if (result.success) {
|
||||
alert('Videos erfolgreich zusammengefugt!\n\n' + result.outputFile);
|
||||
alert(`${UI_TEXT.merge.success}\n\n${result.outputFile}`);
|
||||
mergeFiles = [];
|
||||
renderMergeFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
alert('Fehler beim Zusammenfugen der Videos.');
|
||||
alert(UI_TEXT.merge.failed);
|
||||
}
|
||||
|
||||
void init();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user