From eaa6d637ff5e594e5f473ea8fb55ee224c0fd5af Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Fri, 13 Feb 2026 09:53:40 +0100 Subject: [PATCH] Split renderer into typed feature modules (v3.7.5) Move streamer, queue, settings and update logic into dedicated renderer files, introduce shared type declarations, and remove ts-nocheck so the UI codebase can continue migrating toward a maintainable TypeScript-first structure. --- typescript-version/package-lock.json | 4 +- typescript-version/package.json | 2 +- typescript-version/src/index.html | 9 +- typescript-version/src/main.ts | 2 +- typescript-version/src/renderer-globals.d.ts | 128 ++++ typescript-version/src/renderer-queue.ts | 50 ++ typescript-version/src/renderer-settings.ts | 55 ++ typescript-version/src/renderer-shared.ts | 31 + typescript-version/src/renderer-streamers.ts | 116 ++++ typescript-version/src/renderer-updates.ts | 54 ++ typescript-version/src/renderer.ts | 647 ++++++------------- 11 files changed, 638 insertions(+), 460 deletions(-) create mode 100644 typescript-version/src/renderer-globals.d.ts create mode 100644 typescript-version/src/renderer-queue.ts create mode 100644 typescript-version/src/renderer-settings.ts create mode 100644 typescript-version/src/renderer-shared.ts create mode 100644 typescript-version/src/renderer-streamers.ts create mode 100644 typescript-version/src/renderer-updates.ts diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json index 5312185..f984bdb 100644 --- a/typescript-version/package-lock.json +++ b/typescript-version/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "3.7.4", + "version": "3.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "3.7.4", + "version": "3.7.5", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/typescript-version/package.json b/typescript-version/package.json index e1b8065..a77c00d 100644 --- a/typescript-version/package.json +++ b/typescript-version/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "3.7.4", + "version": "3.7.5", "description": "Twitch VOD Manager - Download Twitch VODs easily", "main": "dist/main.js", "author": "xRangerDE", diff --git a/typescript-version/src/index.html b/typescript-version/src/index.html index 30f10cc..c3eff21 100644 --- a/typescript-version/src/index.html +++ b/typescript-version/src/index.html @@ -335,7 +335,7 @@

Updates

-

Version: v3.7.4

+

Version: v3.7.5

@@ -346,11 +346,16 @@
Nicht verbunden - v3.7.4 + v3.7.5 + + + + + diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts index 08239ec..9a7a571 100644 --- a/typescript-version/src/main.ts +++ b/typescript-version/src/main.ts @@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater'; // ========================================== // CONFIG & CONSTANTS // ========================================== -const APP_VERSION = '3.7.4'; +const APP_VERSION = '3.7.5'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; // Paths diff --git a/typescript-version/src/renderer-globals.d.ts b/typescript-version/src/renderer-globals.d.ts new file mode 100644 index 0000000..bafacb0 --- /dev/null +++ b/typescript-version/src/renderer-globals.d.ts @@ -0,0 +1,128 @@ +interface AppConfig { + client_id?: string; + client_secret?: string; + download_path?: string; + streamers?: string[]; + theme?: string; + download_mode?: 'parts' | 'full'; + part_minutes?: number; + [key: string]: unknown; +} + +interface VOD { + id: string; + title: string; + created_at: string; + duration: string; + thumbnail_url: string; + url: string; + view_count: number; + stream_id?: string; +} + +interface CustomClip { + startSec: number; + durationSec: number; + startPart: number; + filenameFormat: 'simple' | 'timestamp'; +} + +interface QueueItem { + id: string; + title: string; + url: string; + date: string; + streamer: string; + duration_str: string; + status: 'pending' | 'downloading' | 'completed' | 'error'; + progress: number; + currentPart?: number; + totalParts?: number; + speed?: string; + eta?: string; + downloadedBytes?: number; + totalBytes?: number; + customClip?: CustomClip; +} + +interface DownloadProgress { + id: string; + progress: number; + speed: string; + eta: string; + status: string; + currentPart?: number; + totalParts?: number; + downloadedBytes?: number; + totalBytes?: number; +} + +interface VideoInfo { + duration: number; + width: number; + height: number; + fps: number; +} + +interface ClipDialogData { + url: string; + title: string; + date: string; + streamer: string; + duration: string; +} + +interface UpdateInfo { + version: string; + releaseDate?: string; +} + +interface UpdateDownloadProgress { + percent: number; + bytesPerSecond: number; + transferred: number; + total: number; +} + +interface ApiBridge { + getConfig(): Promise; + saveConfig(config: Partial): Promise; + login(): Promise; + getUserId(username: string): Promise; + getVODs(userId: string): Promise; + getQueue(): Promise; + addToQueue(item: Omit): Promise; + removeFromQueue(id: string): Promise; + clearCompleted(): Promise; + startDownload(): Promise; + cancelDownload(): Promise; + isDownloading(): Promise; + downloadClip(url: string): Promise<{ success: boolean; error?: string }>; + selectFolder(): Promise; + selectVideoFile(): Promise; + selectMultipleVideos(): Promise; + saveVideoDialog(defaultName: string): Promise; + openFolder(path: string): Promise; + getVideoInfo(filePath: string): Promise; + extractFrame(filePath: string, timeSeconds: number): Promise; + cutVideo(inputFile: string, startTime: number, endTime: number): Promise<{ success: boolean; outputFile: string | null }>; + mergeVideos(inputFiles: string[], outputFile: string): Promise<{ success: boolean; outputFile: string | null }>; + getVersion(): Promise; + checkUpdate(): Promise<{ checking?: boolean; error?: boolean }>; + downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean }>; + installUpdate(): Promise; + openExternal(url: string): Promise; + onDownloadProgress(callback: (progress: DownloadProgress) => void): void; + onQueueUpdated(callback: (queue: QueueItem[]) => void): void; + onDownloadStarted(callback: () => void): void; + onDownloadFinished(callback: () => void): void; + onCutProgress(callback: (percent: number) => void): void; + onMergeProgress(callback: (percent: number) => void): void; + onUpdateAvailable(callback: (info: UpdateInfo) => void): void; + onUpdateDownloadProgress(callback: (progress: UpdateDownloadProgress) => void): void; + onUpdateDownloaded(callback: (info: UpdateInfo) => void): void; +} + +interface Window { + api: ApiBridge; +} diff --git a/typescript-version/src/renderer-queue.ts b/typescript-version/src/renderer-queue.ts new file mode 100644 index 0000000..61f7075 --- /dev/null +++ b/typescript-version/src/renderer-queue.ts @@ -0,0 +1,50 @@ +async function addToQueue(url: string, title: string, date: string, streamer: string, duration: string): Promise { + queue = await window.api.addToQueue({ + url, + title, + date, + streamer, + duration_str: duration + }); + renderQueue(); +} + +async function removeFromQueue(id: string): Promise { + queue = await window.api.removeFromQueue(id); + renderQueue(); +} + +async function clearCompleted(): Promise { + queue = await window.api.clearCompleted(); + renderQueue(); +} + +function renderQueue(): void { + const list = byId('queueList'); + byId('queueCount').textContent = queue.length; + + if (queue.length === 0) { + list.innerHTML = '
Keine Downloads in der Warteschlange
'; + return; + } + + list.innerHTML = queue.map((item: QueueItem) => { + const isClip = item.customClip ? '* ' : ''; + return ` +
+
+
${isClip}${item.title}
+ x +
+ `; + }).join(''); +} + +async function toggleDownload(): Promise { + if (downloading) { + await window.api.cancelDownload(); + return; + } + + await window.api.startDownload(); +} diff --git a/typescript-version/src/renderer-settings.ts b/typescript-version/src/renderer-settings.ts new file mode 100644 index 0000000..8787eea --- /dev/null +++ b/typescript-version/src/renderer-settings.ts @@ -0,0 +1,55 @@ +async function connect(): Promise { + updateStatus('Verbinde...', false); + const success = await window.api.login(); + isConnected = success; + updateStatus(success ? 'Verbunden' : 'Verbindung fehlgeschlagen', success); +} + +function updateStatus(text: string, connected: boolean): void { + byId('statusText').textContent = text; + const dot = byId('statusDot'); + dot.classList.remove('connected', 'error'); + dot.classList.add(connected ? 'connected' : 'error'); +} + +async function saveSettings(): Promise { + const clientId = byId('clientId').value.trim(); + const clientSecret = byId('clientSecret').value.trim(); + const downloadPath = byId('downloadPath').value; + const downloadMode = byId('downloadMode').value as 'parts' | 'full'; + const partMinutes = parseInt(byId('partMinutes').value, 10) || 120; + + config = await window.api.saveConfig({ + client_id: clientId, + client_secret: clientSecret, + download_path: downloadPath, + download_mode: downloadMode, + part_minutes: partMinutes + }); + + await connect(); +} + +async function selectFolder(): Promise { + const folder = await window.api.selectFolder(); + if (!folder) { + return; + } + + byId('downloadPath').value = folder; + config = await window.api.saveConfig({ download_path: folder }); +} + +function openFolder(): void { + const folder = config.download_path; + if (!folder || typeof folder !== 'string') { + return; + } + + void window.api.openFolder(folder); +} + +function changeTheme(theme: string): void { + document.body.className = `theme-${theme}`; + void window.api.saveConfig({ theme }); +} diff --git a/typescript-version/src/renderer-shared.ts b/typescript-version/src/renderer-shared.ts new file mode 100644 index 0000000..12ca4a8 --- /dev/null +++ b/typescript-version/src/renderer-shared.ts @@ -0,0 +1,31 @@ +function byId(id: string): T { + return document.getElementById(id) as T; +} + +function query(selector: string): T { + return document.querySelector(selector) as T; +} + +function queryAll(selector: string): T[] { + return Array.from(document.querySelectorAll(selector)) as T[]; +} + +let config: AppConfig = {}; +let currentStreamer: string | null = null; +let isConnected = false; +let downloading = false; +let queue: QueueItem[] = []; + +let cutterFile: string | null = null; +let cutterVideoInfo: VideoInfo | null = null; +let cutterStartTime = 0; +let cutterEndTime = 0; +let isCutting = false; + +let mergeFiles: string[] = []; +let isMerging = false; + +let clipDialogData: ClipDialogData | null = null; +let clipTotalSeconds = 0; + +let updateReady = false; diff --git a/typescript-version/src/renderer-streamers.ts b/typescript-version/src/renderer-streamers.ts new file mode 100644 index 0000000..4a983a5 --- /dev/null +++ b/typescript-version/src/renderer-streamers.ts @@ -0,0 +1,116 @@ +function renderStreamers(): void { + const list = byId('streamerList'); + list.innerHTML = ''; + + (config.streamers ?? []).forEach((streamer: string) => { + const item = document.createElement('div'); + item.className = 'streamer-item' + (currentStreamer === streamer ? ' active' : ''); + item.innerHTML = ` + ${streamer} + x + `; + item.onclick = () => { + void selectStreamer(streamer); + }; + list.appendChild(item); + }); +} + +async function addStreamer(): Promise { + const input = byId('newStreamer'); + const name = input.value.trim().toLowerCase(); + if (!name || (config.streamers ?? []).includes(name)) { + return; + } + + config.streamers = [...(config.streamers ?? []), name]; + config = await window.api.saveConfig({ streamers: config.streamers }); + input.value = ''; + renderStreamers(); + await selectStreamer(name); +} + +async function removeStreamer(name: string): Promise { + config.streamers = (config.streamers ?? []).filter((s: string) => s !== name); + config = await window.api.saveConfig({ streamers: config.streamers }); + renderStreamers(); + + if (currentStreamer !== name) { + return; + } + + currentStreamer = null; + byId('vodGrid').innerHTML = ` +
+ +

Keine VODs

+

Wahle einen Streamer aus der Liste.

+
+ `; +} + +async function selectStreamer(name: string): Promise { + currentStreamer = name; + renderStreamers(); + byId('pageTitle').textContent = name; + + if (!isConnected) { + await connect(); + if (!isConnected) { + byId('vodGrid').innerHTML = '

Nicht verbunden

Bitte Twitch API Daten in den Einstellungen prufen.

'; + return; + } + } + + byId('vodGrid').innerHTML = '

Lade VODs...

'; + + const userId = await window.api.getUserId(name); + if (!userId) { + byId('vodGrid').innerHTML = '

Streamer nicht gefunden

'; + return; + } + + const vods = await window.api.getVODs(userId); + renderVODs(vods, name); +} + +function renderVODs(vods: VOD[] | null | undefined, streamer: string): void { + const grid = byId('vodGrid'); + + if (!vods || vods.length === 0) { + grid.innerHTML = '

Keine VODs gefunden

Dieser Streamer hat keine VODs.

'; + return; + } + + grid.innerHTML = vods.map((vod: VOD) => { + 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, '"'); + + return ` +
+ +
+
${vod.title}
+
+ ${date} + ${vod.duration} + ${vod.view_count.toLocaleString()} Views +
+
+
+ + +
+
+ `; + }).join(''); +} + +async function refreshVODs(): Promise { + if (!currentStreamer) { + return; + } + + await selectStreamer(currentStreamer); +} diff --git a/typescript-version/src/renderer-updates.ts b/typescript-version/src/renderer-updates.ts new file mode 100644 index 0000000..7c07a4d --- /dev/null +++ b/typescript-version/src/renderer-updates.ts @@ -0,0 +1,54 @@ +async function checkUpdateSilent(): Promise { + await window.api.checkUpdate(); +} + +async function checkUpdate(): Promise { + await window.api.checkUpdate(); + + setTimeout(() => { + if (byId('updateBanner').style.display !== 'flex') { + alert('Du hast die neueste Version!'); + } + }, 2000); +} + +function downloadUpdate(): void { + if (updateReady) { + void window.api.installUpdate(); + return; + } + + byId('updateButton').textContent = 'Wird heruntergeladen...'; + byId('updateButton').disabled = true; + byId('updateProgress').style.display = 'block'; + byId('updateProgressBar').classList.add('downloading'); + void window.api.downloadUpdate(); +} + +window.api.onUpdateAvailable((info: UpdateInfo) => { + byId('updateBanner').style.display = 'flex'; + byId('updateText').textContent = `Version ${info.version} verfügbar!`; + byId('updateButton').textContent = 'Jetzt herunterladen'; +}); + +window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => { + const bar = byId('updateProgressBar'); + bar.classList.remove('downloading'); + bar.style.width = progress.percent + '%'; + + 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)}%)`; +}); + +window.api.onUpdateDownloaded((info: UpdateInfo) => { + updateReady = true; + + const bar = byId('updateProgressBar'); + bar.classList.remove('downloading'); + bar.style.width = '100%'; + + byId('updateText').textContent = `Version ${info.version} bereit zur Installation!`; + byId('updateButton').textContent = 'Jetzt installieren'; + byId('updateButton').disabled = false; +}); diff --git a/typescript-version/src/renderer.ts b/typescript-version/src/renderer.ts index 86085c9..727180c 100644 --- a/typescript-version/src/renderer.ts +++ b/typescript-version/src/renderer.ts @@ -1,369 +1,181 @@ -// @ts-nocheck - -// State -let config = {}; -let currentStreamer = null; -let isConnected = false; -let downloading = false; -let queue = []; - -// Cutter State -let cutterFile = null; -let cutterVideoInfo = null; -let cutterStartTime = 0; -let cutterEndTime = 0; -let isCutting = false; - -// Merge State -let mergeFiles = []; -let isMerging = false; - -// Init -async function init() { +async function init(): Promise { config = await window.api.getConfig(); queue = await window.api.getQueue(); const version = await window.api.getVersion(); - document.getElementById('versionText').textContent = `v${version}`; - document.getElementById('versionInfo').textContent = `Version: v${version}`; + byId('versionText').textContent = `v${version}`; + byId('versionInfo').textContent = `Version: v${version}`; document.title = `Twitch VOD Manager v${version}`; - document.getElementById('clientId').value = config.client_id || ''; - document.getElementById('clientSecret').value = config.client_secret || ''; - document.getElementById('downloadPath').value = config.download_path || ''; - document.getElementById('themeSelect').value = config.theme || 'twitch'; - document.getElementById('downloadMode').value = config.download_mode || 'full'; - document.getElementById('partMinutes').value = config.part_minutes || 120; - changeTheme(config.theme || 'twitch'); + byId('clientId').value = config.client_id ?? ''; + byId('clientSecret').value = config.client_secret ?? ''; + byId('downloadPath').value = config.download_path ?? ''; + byId('themeSelect').value = config.theme ?? 'twitch'; + byId('downloadMode').value = config.download_mode ?? 'full'; + byId('partMinutes').value = String(config.part_minutes ?? 120); + + changeTheme(config.theme ?? 'twitch'); renderStreamers(); renderQueue(); if (config.client_id && config.client_secret) { await connect(); - // Auto-select first streamer if available if (config.streamers && config.streamers.length > 0) { - selectStreamer(config.streamers[0]); + await selectStreamer(config.streamers[0]); } } - // Event listeners - window.api.onQueueUpdated((q) => { + window.api.onQueueUpdated((q: QueueItem[]) => { queue = q; renderQueue(); }); - window.api.onDownloadProgress((progress) => { - const item = queue.find(i => i.id === progress.id); - if (item) { - item.progress = progress.progress; - renderQueue(); + window.api.onDownloadProgress((progress: DownloadProgress) => { + const item = queue.find((i: QueueItem) => i.id === progress.id); + if (!item) { + return; } + + item.progress = progress.progress; + renderQueue(); }); window.api.onDownloadStarted(() => { downloading = true; - document.getElementById('btnStart').textContent = 'Stoppen'; - document.getElementById('btnStart').classList.add('downloading'); + byId('btnStart').textContent = 'Stoppen'; + byId('btnStart').classList.add('downloading'); }); window.api.onDownloadFinished(() => { downloading = false; - document.getElementById('btnStart').textContent = 'Start'; - document.getElementById('btnStart').classList.remove('downloading'); + byId('btnStart').textContent = 'Start'; + byId('btnStart').classList.remove('downloading'); }); - window.api.onCutProgress((percent) => { - document.getElementById('cutProgressBar').style.width = percent + '%'; - document.getElementById('cutProgressText').textContent = Math.round(percent) + '%'; + window.api.onCutProgress((percent: number) => { + byId('cutProgressBar').style.width = percent + '%'; + byId('cutProgressText').textContent = Math.round(percent) + '%'; }); - window.api.onMergeProgress((percent) => { - document.getElementById('mergeProgressBar').style.width = percent + '%'; - document.getElementById('mergeProgressText').textContent = Math.round(percent) + '%'; + window.api.onMergeProgress((percent: number) => { + byId('mergeProgressBar').style.width = percent + '%'; + byId('mergeProgressText').textContent = Math.round(percent) + '%'; }); - setTimeout(checkUpdateSilent, 3000); + setTimeout(() => { + void checkUpdateSilent(); + }, 3000); } -async function connect() { - updateStatus('Verbinde...', false); - const success = await window.api.login(); - isConnected = success; - updateStatus(success ? 'Verbunden' : 'Verbindung fehlgeschlagen', success); -} +function showTab(tab: string): void { + queryAll('.nav-item').forEach((i) => i.classList.remove('active')); + queryAll('.tab-content').forEach((c) => c.classList.remove('active')); -function updateStatus(text, connected) { - document.getElementById('statusText').textContent = text; - const dot = document.getElementById('statusDot'); - dot.classList.remove('connected', 'error'); - dot.classList.add(connected ? 'connected' : 'error'); -} + query(`.nav-item[data-tab="${tab}"]`).classList.add('active'); + byId(tab + 'Tab').classList.add('active'); -// Navigation -function showTab(tab) { - document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active')); - document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); - - document.querySelector(`.nav-item[data-tab="${tab}"]`).classList.add('active'); - document.getElementById(tab + 'Tab').classList.add('active'); - - const titles = { + const titles: Record = { vods: 'VODs', clips: 'Clips', cutter: 'Video Cutter', merge: 'Videos Zusammenfugen', settings: 'Einstellungen' }; - document.getElementById('pageTitle').textContent = currentStreamer || titles[tab]; + + byId('pageTitle').textContent = currentStreamer || titles[tab] || 'Twitch VOD Manager'; } -// Streamers -function renderStreamers() { - const list = document.getElementById('streamerList'); - list.innerHTML = ''; - - (config.streamers || []).forEach(streamer => { - const item = document.createElement('div'); - item.className = 'streamer-item' + (currentStreamer === streamer ? ' active' : ''); - item.innerHTML = ` - ${streamer} - x - `; - item.onclick = () => selectStreamer(streamer); - list.appendChild(item); - }); -} - -async function addStreamer() { - const input = document.getElementById('newStreamer'); - const name = input.value.trim().toLowerCase(); - if (!name || (config.streamers || []).includes(name)) return; - - config.streamers = [...(config.streamers || []), name]; - config = await window.api.saveConfig({ streamers: config.streamers }); - input.value = ''; - renderStreamers(); - selectStreamer(name); -} - -async function removeStreamer(name) { - config.streamers = (config.streamers || []).filter(s => s !== name); - config = await window.api.saveConfig({ streamers: config.streamers }); - renderStreamers(); - if (currentStreamer === name) { - currentStreamer = null; - document.getElementById('vodGrid').innerHTML = ` -
- -

Keine VODs

-

Wahle einen Streamer aus der Liste.

-
- `; - } -} - -async function selectStreamer(name) { - currentStreamer = name; - renderStreamers(); - document.getElementById('pageTitle').textContent = name; - - if (!isConnected) { - await connect(); - if (!isConnected) { - document.getElementById('vodGrid').innerHTML = '

Nicht verbunden

Bitte Twitch API Daten in den Einstellungen prufen.

'; - return; - } - } - - document.getElementById('vodGrid').innerHTML = '

Lade VODs...

'; - - const userId = await window.api.getUserId(name); - if (!userId) { - document.getElementById('vodGrid').innerHTML = '

Streamer nicht gefunden

'; - return; - } - - const vods = await window.api.getVODs(userId); - renderVODs(vods, name); -} - -function renderVODs(vods, streamer) { - const grid = document.getElementById('vodGrid'); - - if (!vods || vods.length === 0) { - grid.innerHTML = '

Keine VODs gefunden

Dieser Streamer hat keine VODs.

'; - return; - } - - grid.innerHTML = vods.map(vod => { - 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, """); - return ` -
- -
-
${vod.title}
-
- ${date} - ${vod.duration} - ${vod.view_count.toLocaleString()} Views -
-
-
- - -
-
- `; - }).join(''); -} - -async function refreshVODs() { - if (currentStreamer) { - await selectStreamer(currentStreamer); - } -} - -// Queue -async function addToQueue(url, title, date, streamer, duration) { - queue = await window.api.addToQueue({ url, title, date, streamer, duration_str: duration }); - renderQueue(); -} - -async function removeFromQueue(id) { - queue = await window.api.removeFromQueue(id); - renderQueue(); -} - -async function clearCompleted() { - queue = await window.api.clearCompleted(); - renderQueue(); -} - -function renderQueue() { - const list = document.getElementById('queueList'); - document.getElementById('queueCount').textContent = queue.length; - - if (queue.length === 0) { - list.innerHTML = '
Keine Downloads in der Warteschlange
'; - return; - } - - list.innerHTML = queue.map(item => { - const isClip = item.customClip ? '* ' : ''; - return ` -
-
-
${isClip}${item.title}
- x -
- `; - }).join(''); -} - -async function toggleDownload() { - if (downloading) { - await window.api.cancelDownload(); - } else { - await window.api.startDownload(); - } -} - -// Clip Dialog -let clipDialogData = null; -let clipTotalSeconds = 0; - -function parseDurationToSeconds(durStr) { +function parseDurationToSeconds(durStr: string): number { let seconds = 0; const hours = durStr.match(/(\d+)h/); const minutes = durStr.match(/(\d+)m/); const secs = durStr.match(/(\d+)s/); - if (hours) seconds += parseInt(hours[1]) * 3600; - if (minutes) seconds += parseInt(minutes[1]) * 60; - if (secs) seconds += parseInt(secs[1]); + + if (hours) seconds += parseInt(hours[1], 10) * 3600; + if (minutes) seconds += parseInt(minutes[1], 10) * 60; + if (secs) seconds += parseInt(secs[1], 10); + return seconds; } -function formatSecondsToTime(seconds) { +function formatSecondsToTime(seconds: number): string { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } -function formatSecondsToTimeDashed(seconds) { +function formatSecondsToTimeDashed(seconds: number): string { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`; } -function parseTimeToSeconds(timeStr) { - const parts = timeStr.split(':').map(p => parseInt(p) || 0); +function parseTimeToSeconds(timeStr: string): number { + const parts = timeStr.split(':').map((p: string) => parseInt(p, 10) || 0); if (parts.length === 3) { return parts[0] * 3600 + parts[1] * 60 + parts[2]; } + return 0; } -function openClipDialog(url, title, date, streamer, duration) { +function openClipDialog(url: string, title: string, date: string, streamer: string, duration: string): void { clipDialogData = { url, title, date, streamer, duration }; clipTotalSeconds = parseDurationToSeconds(duration); - document.getElementById('clipDialogTitle').textContent = 'Clip zuschneiden (' + duration + ')'; + byId('clipDialogTitle').textContent = `Clip zuschneiden (${duration})`; + byId('clipStartSlider').max = String(clipTotalSeconds); + byId('clipEndSlider').max = String(clipTotalSeconds); + byId('clipStartSlider').value = '0'; + byId('clipEndSlider').value = String(Math.min(60, clipTotalSeconds)); - // Setup sliders - document.getElementById('clipStartSlider').max = clipTotalSeconds; - document.getElementById('clipEndSlider').max = clipTotalSeconds; - document.getElementById('clipStartSlider').value = 0; - document.getElementById('clipEndSlider').value = Math.min(60, clipTotalSeconds); - - document.getElementById('clipStartTime').value = '00:00:00'; - document.getElementById('clipEndTime').value = formatSecondsToTime(Math.min(60, clipTotalSeconds)); - document.getElementById('clipStartPart').value = ''; + byId('clipStartTime').value = '00:00:00'; + byId('clipEndTime').value = formatSecondsToTime(Math.min(60, clipTotalSeconds)); + byId('clipStartPart').value = ''; updateClipDuration(); updateFilenameExamples(); - document.getElementById('clipModal').classList.add('show'); + byId('clipModal').classList.add('show'); } -function closeClipDialog() { - document.getElementById('clipModal').classList.remove('show'); +function closeClipDialog(): void { + byId('clipModal').classList.remove('show'); clipDialogData = null; } -function updateFromSlider(which) { - const startSlider = document.getElementById('clipStartSlider'); - const endSlider = document.getElementById('clipEndSlider'); +function updateFromSlider(which: string): void { + const startSlider = byId('clipStartSlider'); + const endSlider = byId('clipEndSlider'); if (which === 'start') { - document.getElementById('clipStartTime').value = formatSecondsToTime(parseInt(startSlider.value)); + byId('clipStartTime').value = formatSecondsToTime(parseInt(startSlider.value, 10)); } else { - document.getElementById('clipEndTime').value = formatSecondsToTime(parseInt(endSlider.value)); + byId('clipEndTime').value = formatSecondsToTime(parseInt(endSlider.value, 10)); } updateClipDuration(); } -function updateFromInput(which) { - const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value); - const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value); +function updateFromInput(which: string): void { + const startSec = parseTimeToSeconds(byId('clipStartTime').value); + const endSec = parseTimeToSeconds(byId('clipEndTime').value); if (which === 'start') { - document.getElementById('clipStartSlider').value = Math.max(0, Math.min(startSec, clipTotalSeconds)); + byId('clipStartSlider').value = String(Math.max(0, Math.min(startSec, clipTotalSeconds))); } else { - document.getElementById('clipEndSlider').value = Math.max(0, Math.min(endSec, clipTotalSeconds)); + byId('clipEndSlider').value = String(Math.max(0, Math.min(endSec, clipTotalSeconds))); } updateClipDuration(); } -function updateClipDuration() { - const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value); - const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value); +function updateClipDuration(): void { + const startSec = parseTimeToSeconds(byId('clipStartTime').value); + const endSec = parseTimeToSeconds(byId('clipEndTime').value); const duration = endSec - startSec; - const durationDisplay = document.getElementById('clipDurationDisplay'); + const durationDisplay = byId('clipDurationDisplay'); if (duration > 0) { durationDisplay.textContent = formatSecondsToTime(duration); @@ -376,27 +188,31 @@ function updateClipDuration() { updateFilenameExamples(); } -function updateFilenameExamples() { - if (!clipDialogData) return; +function updateFilenameExamples(): void { + if (!clipDialogData) { + return; + } const date = new Date(clipDialogData.date); const dateStr = `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getFullYear()}`; - const partNum = document.getElementById('clipStartPart').value || '1'; - const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value); + const partNum = byId('clipStartPart').value || '1'; + const startSec = parseTimeToSeconds(byId('clipStartTime').value); const timeStr = formatSecondsToTimeDashed(startSec); - document.getElementById('formatSimple').textContent = `${dateStr}_${partNum}.mp4 (Standard)`; - document.getElementById('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 (mit Zeitstempel)`; + byId('formatSimple').textContent = `${dateStr}_${partNum}.mp4 (Standard)`; + byId('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 (mit Zeitstempel)`; } -async function confirmClipDialog() { - if (!clipDialogData) return; +async function confirmClipDialog(): Promise { + if (!clipDialogData) { + return; + } - const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value); - const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value); - const startPartStr = document.getElementById('clipStartPart').value.trim(); - const startPart = startPartStr ? parseInt(startPartStr) : 1; - const filenameFormat = document.querySelector('input[name="filenameFormat"]:checked').value; + const startSec = parseTimeToSeconds(byId('clipStartTime').value); + const endSec = parseTimeToSeconds(byId('clipEndTime').value); + const startPartStr = byId('clipStartPart').value.trim(); + const startPart = startPartStr ? parseInt(startPartStr, 10) : 1; + const filenameFormat = query('input[name="filenameFormat"]:checked').value as 'simple' | 'timestamp'; if (endSec <= startSec) { alert('Endzeit muss grosser als Startzeit sein!'); @@ -417,10 +233,10 @@ async function confirmClipDialog() { streamer: clipDialogData.streamer, duration_str: clipDialogData.duration, customClip: { - startSec: startSec, - durationSec: durationSec, - startPart: startPart, - filenameFormat: filenameFormat + startSec, + durationSec, + startPart, + filenameFormat } }); @@ -428,47 +244,10 @@ async function confirmClipDialog() { closeClipDialog(); } -// Settings -async function saveSettings() { - const clientId = document.getElementById('clientId').value.trim(); - const clientSecret = document.getElementById('clientSecret').value.trim(); - const downloadPath = document.getElementById('downloadPath').value; - const downloadMode = document.getElementById('downloadMode').value; - const partMinutes = parseInt(document.getElementById('partMinutes').value); - - config = await window.api.saveConfig({ - client_id: clientId, - client_secret: clientSecret, - download_path: downloadPath, - download_mode: downloadMode, - part_minutes: partMinutes - }); - - await connect(); -} - -async function selectFolder() { - const folder = await window.api.selectFolder(); - if (folder) { - document.getElementById('downloadPath').value = folder; - config = await window.api.saveConfig({ download_path: folder }); - } -} - -function openFolder() { - window.api.openFolder(config.download_path); -} - -function changeTheme(theme) { - document.body.className = `theme-${theme}`; - window.api.saveConfig({ theme }); -} - -// Clips -async function downloadClip() { - const url = document.getElementById('clipUrl').value.trim(); - const status = document.getElementById('clipStatus'); - const btn = document.getElementById('btnClip'); +async function downloadClip(): Promise { + const url = byId('clipUrl').value.trim(); + const status = byId('clipStatus'); + const btn = byId('btnClip'); if (!url) { status.textContent = 'Bitte URL eingeben'; @@ -489,19 +268,21 @@ async function downloadClip() { if (result.success) { status.textContent = 'Download erfolgreich!'; status.className = 'clip-status success'; - } else { - status.textContent = 'Fehler: ' + result.error; - status.className = 'clip-status error'; + return; } + + status.textContent = 'Fehler: ' + (result.error || 'Unbekannter Fehler'); + status.className = 'clip-status error'; } -// Video Cutter -async function selectCutterVideo() { +async function selectCutterVideo(): Promise { const filePath = await window.api.selectVideoFile(); - if (!filePath) return; + if (!filePath) { + return; + } cutterFile = filePath; - document.getElementById('cutterFilePath').value = filePath; + byId('cutterFilePath').value = filePath; const info = await window.api.getVideoInfo(filePath); if (!info) { @@ -513,41 +294,44 @@ async function selectCutterVideo() { cutterStartTime = 0; cutterEndTime = info.duration; - document.getElementById('cutterInfo').style.display = 'flex'; - document.getElementById('timelineContainer').style.display = 'block'; - document.getElementById('btnCut').disabled = false; + byId('cutterInfo').style.display = 'flex'; + byId('timelineContainer').style.display = 'block'; + byId('btnCut').disabled = false; - document.getElementById('infoDuration').textContent = formatTime(info.duration); - document.getElementById('infoResolution').textContent = `${info.width}x${info.height}`; - document.getElementById('infoFps').textContent = Math.round(info.fps); - document.getElementById('infoSelection').textContent = formatTime(info.duration); + byId('infoDuration').textContent = formatTime(info.duration); + byId('infoResolution').textContent = `${info.width}x${info.height}`; + byId('infoFps').textContent = Math.round(info.fps); + byId('infoSelection').textContent = formatTime(info.duration); - document.getElementById('startTime').value = '00:00:00'; - document.getElementById('endTime').value = formatTime(info.duration); + byId('startTime').value = '00:00:00'; + byId('endTime').value = formatTime(info.duration); updateTimeline(); await updatePreview(0); } -function formatTime(seconds) { +function formatTime(seconds: number): string { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } -function parseTime(timeStr) { - const parts = timeStr.split(':').map(p => parseInt(p) || 0); +function parseTime(timeStr: string): number { + const parts = timeStr.split(':').map((p: string) => parseInt(p, 10) || 0); if (parts.length === 3) { return parts[0] * 3600 + parts[1] * 60 + parts[2]; } + return 0; } -function updateTimeline() { - if (!cutterVideoInfo) return; +function updateTimeline(): void { + if (!cutterVideoInfo) { + return; + } - const selection = document.getElementById('timelineSelection'); + const selection = byId('timelineSelection'); const startPercent = (cutterStartTime / cutterVideoInfo.duration) * 100; const endPercent = (cutterEndTime / cutterVideoInfo.duration) * 100; @@ -555,12 +339,12 @@ function updateTimeline() { selection.style.width = (endPercent - startPercent) + '%'; const duration = cutterEndTime - cutterStartTime; - document.getElementById('infoSelection').textContent = formatTime(duration); + byId('infoSelection').textContent = formatTime(duration); } -function updateTimeFromInput() { - const startStr = document.getElementById('startTime').value; - const endStr = document.getElementById('endTime').value; +function updateTimeFromInput(): void { + const startStr = byId('startTime').value; + const endStr = byId('endTime').value; cutterStartTime = Math.max(0, parseTime(startStr)); cutterEndTime = Math.min(cutterVideoInfo?.duration || 0, parseTime(endStr)); @@ -572,66 +356,75 @@ function updateTimeFromInput() { updateTimeline(); } -async function seekTimeline(event) { - if (!cutterVideoInfo) return; +async function seekTimeline(event: MouseEvent): Promise { + if (!cutterVideoInfo) { + return; + } - const timeline = document.getElementById('timeline'); + const timeline = byId('timeline'); const rect = timeline.getBoundingClientRect(); const percent = (event.clientX - rect.left) / rect.width; const time = percent * cutterVideoInfo.duration; - document.getElementById('timelineCurrent').style.left = (percent * 100) + '%'; + byId('timelineCurrent').style.left = (percent * 100) + '%'; await updatePreview(time); } -async function updatePreview(time) { - if (!cutterFile) return; +async function updatePreview(time: number): Promise { + if (!cutterFile) { + return; + } - const preview = document.getElementById('cutterPreview'); + const preview = byId('cutterPreview'); preview.innerHTML = '

Lade Vorschau...

'; const frame = await window.api.extractFrame(cutterFile, time); if (frame) { preview.innerHTML = `Preview`; - } else { - preview.innerHTML = '

Vorschau nicht verfugbar

'; + return; } + + preview.innerHTML = '

Vorschau nicht verfugbar

'; } -async function startCutting() { - if (!cutterFile || isCutting) return; +async function startCutting(): Promise { + if (!cutterFile || isCutting) { + return; + } isCutting = true; - document.getElementById('btnCut').disabled = true; - document.getElementById('btnCut').textContent = 'Schneidet...'; - document.getElementById('cutProgress').classList.add('show'); + byId('btnCut').disabled = true; + byId('btnCut').textContent = 'Schneidet...'; + byId('cutProgress').classList.add('show'); const result = await window.api.cutVideo(cutterFile, cutterStartTime, cutterEndTime); isCutting = false; - document.getElementById('btnCut').disabled = false; - document.getElementById('btnCut').textContent = 'Schneiden'; - document.getElementById('cutProgress').classList.remove('show'); + byId('btnCut').disabled = false; + byId('btnCut').textContent = 'Schneiden'; + byId('cutProgress').classList.remove('show'); if (result.success) { alert('Video erfolgreich geschnitten!\n\n' + result.outputFile); - } else { - alert('Fehler beim Schneiden des Videos.'); + return; } + + alert('Fehler beim Schneiden des Videos.'); } -// Merge Videos -async function addMergeFiles() { +async function addMergeFiles(): Promise { const files = await window.api.selectMultipleVideos(); - if (files && files.length > 0) { - mergeFiles = [...mergeFiles, ...files]; - renderMergeFiles(); + if (!files || files.length === 0) { + return; } + + mergeFiles = [...mergeFiles, ...files]; + renderMergeFiles(); } -function renderMergeFiles() { - const list = document.getElementById('mergeFileList'); - document.getElementById('btnMerge').disabled = mergeFiles.length < 2; +function renderMergeFiles(): void { + const list = byId('mergeFileList'); + byId('btnMerge').disabled = mergeFiles.length < 2; if (mergeFiles.length === 0) { list.innerHTML = ` @@ -643,7 +436,7 @@ function renderMergeFiles() { return; } - list.innerHTML = mergeFiles.map((file, index) => { + list.innerHTML = mergeFiles.map((file: string, index: number) => { const name = file.split(/[/\\]/).pop(); return `
@@ -659,9 +452,11 @@ function renderMergeFiles() { }).join(''); } -function moveMergeFile(index, direction) { +function moveMergeFile(index: number, direction: number): void { const newIndex = index + direction; - if (newIndex < 0 || newIndex >= mergeFiles.length) return; + if (newIndex < 0 || newIndex >= mergeFiles.length) { + return; + } const temp = mergeFiles[index]; mergeFiles[index] = mergeFiles[newIndex]; @@ -669,97 +464,41 @@ function moveMergeFile(index, direction) { renderMergeFiles(); } -function removeMergeFile(index) { +function removeMergeFile(index: number): void { mergeFiles.splice(index, 1); renderMergeFiles(); } -async function startMerging() { - if (mergeFiles.length < 2 || isMerging) return; +async function startMerging(): Promise { + if (mergeFiles.length < 2 || isMerging) { + return; + } const outputFile = await window.api.saveVideoDialog('merged_video.mp4'); - if (!outputFile) return; + if (!outputFile) { + return; + } isMerging = true; - document.getElementById('btnMerge').disabled = true; - document.getElementById('btnMerge').textContent = 'Zusammenfugen...'; - document.getElementById('mergeProgress').classList.add('show'); + byId('btnMerge').disabled = true; + byId('btnMerge').textContent = 'Zusammenfugen...'; + byId('mergeProgress').classList.add('show'); const result = await window.api.mergeVideos(mergeFiles, outputFile); isMerging = false; - document.getElementById('btnMerge').disabled = false; - document.getElementById('btnMerge').textContent = 'Zusammenfugen'; - document.getElementById('mergeProgress').classList.remove('show'); + byId('btnMerge').disabled = false; + byId('btnMerge').textContent = 'Zusammenfugen'; + byId('mergeProgress').classList.remove('show'); if (result.success) { alert('Videos erfolgreich zusammengefugt!\n\n' + result.outputFile); mergeFiles = []; renderMergeFiles(); - } else { - alert('Fehler beim Zusammenfugen der Videos.'); + return; } + + alert('Fehler beim Zusammenfugen der Videos.'); } -// Updates - wird jetzt automatisch vom main process via Events gesteuert -async function checkUpdateSilent() { - // Auto-Updater läuft automatisch beim App-Start - await window.api.checkUpdate(); -} - -async function checkUpdate() { - // Manueller Check - zeigt Info wenn kein Update - await window.api.checkUpdate(); - // Wenn kein Update, kommt kein Event - kurz warten dann Info zeigen - setTimeout(() => { - if (document.getElementById('updateBanner').style.display !== 'flex') { - alert('Du hast die neueste Version!'); - } - }, 2000); -} - -let updateReady = false; - -function downloadUpdate() { - if (updateReady) { - // Update ist heruntergeladen - installieren - window.api.installUpdate(); - } else { - // Update herunterladen - document.getElementById('updateButton').textContent = 'Wird heruntergeladen...'; - document.getElementById('updateButton').disabled = true; - document.getElementById('updateProgress').style.display = 'block'; - // Start animated progress bar - document.getElementById('updateProgressBar').classList.add('downloading'); - window.api.downloadUpdate(); - } -} - -// Auto-Update Event Listeners -window.api.onUpdateAvailable((info) => { - document.getElementById('updateBanner').style.display = 'flex'; - document.getElementById('updateText').textContent = `Version ${info.version} verfügbar!`; - document.getElementById('updateButton').textContent = 'Jetzt herunterladen'; -}); - -window.api.onUpdateDownloadProgress((progress) => { - const bar = document.getElementById('updateProgressBar'); - bar.classList.remove('downloading'); - bar.style.width = progress.percent + '%'; - const mb = (progress.transferred / 1024 / 1024).toFixed(1); - const totalMb = (progress.total / 1024 / 1024).toFixed(1); - document.getElementById('updateText').textContent = `Download: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`; -}); - -window.api.onUpdateDownloaded((info) => { - updateReady = true; - const bar = document.getElementById('updateProgressBar'); - bar.classList.remove('downloading'); - bar.style.width = '100%'; - document.getElementById('updateText').textContent = `Version ${info.version} bereit zur Installation!`; - document.getElementById('updateButton').textContent = 'Jetzt installieren'; - document.getElementById('updateButton').disabled = false; -}); - -// Start -init(); +void init();