From e29403505fc7eafcd727eaa943a53ce1e0322774 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Fri, 13 Feb 2026 09:44:47 +0100 Subject: [PATCH] Split renderer code out of index.html Move inline UI logic into a dedicated TypeScript renderer file and extract CSS into a standalone stylesheet to continue the migration away from monolithic HTML and toward a TS-first structure. --- typescript-version/package-lock.json | 4 +- typescript-version/package.json | 2 +- typescript-version/src/index.html | 1880 +------------------------- typescript-version/src/main.ts | 2 +- typescript-version/src/renderer.ts | 765 +++++++++++ typescript-version/src/styles.css | 1107 +++++++++++++++ 6 files changed, 1880 insertions(+), 1880 deletions(-) create mode 100644 typescript-version/src/renderer.ts create mode 100644 typescript-version/src/styles.css diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json index 7d5c338..5312185 100644 --- a/typescript-version/package-lock.json +++ b/typescript-version/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "3.7.3", + "version": "3.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "3.7.3", + "version": "3.7.4", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/typescript-version/package.json b/typescript-version/package.json index 4a744c6..e1b8065 100644 --- a/typescript-version/package.json +++ b/typescript-version/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "3.7.3", + "version": "3.7.4", "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 57b6844..30f10cc 100644 --- a/typescript-version/src/index.html +++ b/typescript-version/src/index.html @@ -5,1115 +5,7 @@ Twitch VOD Manager - +
@@ -1443,7 +335,7 @@

Updates

-

Version: v3.6.2

+

Version: v3.7.4

@@ -1454,775 +346,11 @@
Nicht verbunden - v3.6.2 + v3.7.4 - + diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts index d9dcced..08239ec 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.3'; +const APP_VERSION = '3.7.4'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; // Paths diff --git a/typescript-version/src/renderer.ts b/typescript-version/src/renderer.ts new file mode 100644 index 0000000..86085c9 --- /dev/null +++ b/typescript-version/src/renderer.ts @@ -0,0 +1,765 @@ +// @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() { + 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}`; + 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'); + 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]); + } + } + + // Event listeners + window.api.onQueueUpdated((q) => { + 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.onDownloadStarted(() => { + downloading = true; + document.getElementById('btnStart').textContent = 'Stoppen'; + document.getElementById('btnStart').classList.add('downloading'); + }); + + window.api.onDownloadFinished(() => { + downloading = false; + document.getElementById('btnStart').textContent = 'Start'; + document.getElementById('btnStart').classList.remove('downloading'); + }); + + window.api.onCutProgress((percent) => { + document.getElementById('cutProgressBar').style.width = percent + '%'; + document.getElementById('cutProgressText').textContent = Math.round(percent) + '%'; + }); + + window.api.onMergeProgress((percent) => { + document.getElementById('mergeProgressBar').style.width = percent + '%'; + document.getElementById('mergeProgressText').textContent = Math.round(percent) + '%'; + }); + + setTimeout(checkUpdateSilent, 3000); +} + +async function connect() { + updateStatus('Verbinde...', false); + const success = await window.api.login(); + isConnected = success; + updateStatus(success ? 'Verbunden' : 'Verbindung fehlgeschlagen', success); +} + +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'); +} + +// 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 = { + vods: 'VODs', + clips: 'Clips', + cutter: 'Video Cutter', + merge: 'Videos Zusammenfugen', + settings: 'Einstellungen' + }; + document.getElementById('pageTitle').textContent = currentStreamer || titles[tab]; +} + +// 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) { + 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]); + return seconds; +} + +function formatSecondsToTime(seconds) { + 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) { + 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); + if (parts.length === 3) { + return parts[0] * 3600 + parts[1] * 60 + parts[2]; + } + return 0; +} + +function openClipDialog(url, title, date, streamer, duration) { + clipDialogData = { url, title, date, streamer, duration }; + clipTotalSeconds = parseDurationToSeconds(duration); + + document.getElementById('clipDialogTitle').textContent = 'Clip zuschneiden (' + duration + ')'; + + // 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 = ''; + + updateClipDuration(); + updateFilenameExamples(); + document.getElementById('clipModal').classList.add('show'); +} + +function closeClipDialog() { + document.getElementById('clipModal').classList.remove('show'); + clipDialogData = null; +} + +function updateFromSlider(which) { + const startSlider = document.getElementById('clipStartSlider'); + const endSlider = document.getElementById('clipEndSlider'); + + if (which === 'start') { + document.getElementById('clipStartTime').value = formatSecondsToTime(parseInt(startSlider.value)); + } else { + document.getElementById('clipEndTime').value = formatSecondsToTime(parseInt(endSlider.value)); + } + + updateClipDuration(); +} + +function updateFromInput(which) { + const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value); + const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value); + + if (which === 'start') { + document.getElementById('clipStartSlider').value = Math.max(0, Math.min(startSec, clipTotalSeconds)); + } else { + document.getElementById('clipEndSlider').value = Math.max(0, Math.min(endSec, clipTotalSeconds)); + } + + updateClipDuration(); +} + +function updateClipDuration() { + const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value); + const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value); + const duration = endSec - startSec; + const durationDisplay = document.getElementById('clipDurationDisplay'); + + if (duration > 0) { + durationDisplay.textContent = formatSecondsToTime(duration); + durationDisplay.style.color = '#00c853'; + } else { + durationDisplay.textContent = 'Ungultig!'; + durationDisplay.style.color = '#ff4444'; + } + + updateFilenameExamples(); +} + +function updateFilenameExamples() { + 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 timeStr = formatSecondsToTimeDashed(startSec); + + document.getElementById('formatSimple').textContent = `${dateStr}_${partNum}.mp4 (Standard)`; + document.getElementById('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 (mit Zeitstempel)`; +} + +async function confirmClipDialog() { + 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; + + if (endSec <= startSec) { + alert('Endzeit muss grosser als Startzeit sein!'); + return; + } + + if (startSec < 0 || endSec > clipTotalSeconds) { + alert('Zeit ausserhalb des VOD-Bereichs!'); + return; + } + + const durationSec = endSec - startSec; + + queue = await window.api.addToQueue({ + url: clipDialogData.url, + title: clipDialogData.title, + date: clipDialogData.date, + streamer: clipDialogData.streamer, + duration_str: clipDialogData.duration, + customClip: { + startSec: startSec, + durationSec: durationSec, + startPart: startPart, + filenameFormat: filenameFormat + } + }); + + renderQueue(); + 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'); + + if (!url) { + status.textContent = 'Bitte URL eingeben'; + status.className = 'clip-status error'; + return; + } + + btn.disabled = true; + btn.textContent = 'Lade...'; + status.textContent = 'Download lauft...'; + status.className = 'clip-status loading'; + + const result = await window.api.downloadClip(url); + + btn.disabled = false; + btn.textContent = 'Clip herunterladen'; + + if (result.success) { + status.textContent = 'Download erfolgreich!'; + status.className = 'clip-status success'; + } else { + status.textContent = 'Fehler: ' + result.error; + status.className = 'clip-status error'; + } +} + +// Video Cutter +async function selectCutterVideo() { + const filePath = await window.api.selectVideoFile(); + if (!filePath) return; + + cutterFile = filePath; + document.getElementById('cutterFilePath').value = filePath; + + const info = await window.api.getVideoInfo(filePath); + if (!info) { + alert('Konnte Video-Informationen nicht lesen. FFprobe installiert?'); + return; + } + + cutterVideoInfo = info; + cutterStartTime = 0; + cutterEndTime = info.duration; + + document.getElementById('cutterInfo').style.display = 'flex'; + document.getElementById('timelineContainer').style.display = 'block'; + document.getElementById('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); + + document.getElementById('startTime').value = '00:00:00'; + document.getElementById('endTime').value = formatTime(info.duration); + + updateTimeline(); + await updatePreview(0); +} + +function formatTime(seconds) { + 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); + if (parts.length === 3) { + return parts[0] * 3600 + parts[1] * 60 + parts[2]; + } + return 0; +} + +function updateTimeline() { + if (!cutterVideoInfo) return; + + const selection = document.getElementById('timelineSelection'); + const startPercent = (cutterStartTime / cutterVideoInfo.duration) * 100; + const endPercent = (cutterEndTime / cutterVideoInfo.duration) * 100; + + selection.style.left = startPercent + '%'; + selection.style.width = (endPercent - startPercent) + '%'; + + const duration = cutterEndTime - cutterStartTime; + document.getElementById('infoSelection').textContent = formatTime(duration); +} + +function updateTimeFromInput() { + const startStr = document.getElementById('startTime').value; + const endStr = document.getElementById('endTime').value; + + cutterStartTime = Math.max(0, parseTime(startStr)); + cutterEndTime = Math.min(cutterVideoInfo?.duration || 0, parseTime(endStr)); + + if (cutterEndTime <= cutterStartTime) { + cutterEndTime = cutterStartTime + 1; + } + + updateTimeline(); +} + +async function seekTimeline(event) { + if (!cutterVideoInfo) return; + + const timeline = document.getElementById('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) + '%'; + await updatePreview(time); +} + +async function updatePreview(time) { + if (!cutterFile) return; + + const preview = document.getElementById('cutterPreview'); + preview.innerHTML = '

Lade Vorschau...

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

Vorschau nicht verfugbar

'; + } +} + +async function startCutting() { + if (!cutterFile || isCutting) return; + + isCutting = true; + document.getElementById('btnCut').disabled = true; + document.getElementById('btnCut').textContent = 'Schneidet...'; + document.getElementById('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'); + + if (result.success) { + alert('Video erfolgreich geschnitten!\n\n' + result.outputFile); + } else { + alert('Fehler beim Schneiden des Videos.'); + } +} + +// Merge Videos +async function addMergeFiles() { + const files = await window.api.selectMultipleVideos(); + if (files && files.length > 0) { + mergeFiles = [...mergeFiles, ...files]; + renderMergeFiles(); + } +} + +function renderMergeFiles() { + const list = document.getElementById('mergeFileList'); + document.getElementById('btnMerge').disabled = mergeFiles.length < 2; + + if (mergeFiles.length === 0) { + list.innerHTML = ` +
+ +

Keine Videos ausgewahlt

+
+ `; + return; + } + + list.innerHTML = mergeFiles.map((file, index) => { + const name = file.split(/[/\\]/).pop(); + return ` +
+
${index + 1}
+
${name}
+
+ + + +
+
+ `; + }).join(''); +} + +function moveMergeFile(index, direction) { + const newIndex = index + direction; + if (newIndex < 0 || newIndex >= mergeFiles.length) return; + + const temp = mergeFiles[index]; + mergeFiles[index] = mergeFiles[newIndex]; + mergeFiles[newIndex] = temp; + renderMergeFiles(); +} + +function removeMergeFile(index) { + mergeFiles.splice(index, 1); + renderMergeFiles(); +} + +async function startMerging() { + if (mergeFiles.length < 2 || isMerging) return; + + const outputFile = await window.api.saveVideoDialog('merged_video.mp4'); + if (!outputFile) return; + + isMerging = true; + document.getElementById('btnMerge').disabled = true; + document.getElementById('btnMerge').textContent = 'Zusammenfugen...'; + document.getElementById('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'); + + if (result.success) { + alert('Videos erfolgreich zusammengefugt!\n\n' + result.outputFile); + mergeFiles = []; + renderMergeFiles(); + } else { + 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(); diff --git a/typescript-version/src/styles.css b/typescript-version/src/styles.css new file mode 100644 index 0000000..a79fd9a --- /dev/null +++ b/typescript-version/src/styles.css @@ -0,0 +1,1107 @@ +:root { + --bg-main: #0e0e10; + --bg-sidebar: #18181b; + --bg-card: #1f1f23; + --accent: #9146FF; + --accent-hover: #772ce8; + --text: #efeff1; + --text-secondary: #adadb8; + --success: #00c853; + --error: #ff4444; + --warning: #ffab00; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-main); + color: var(--text); + height: 100vh; + overflow: hidden; +} + +.app { + display: flex; + height: 100vh; +} + +/* Sidebar */ +.sidebar { + width: 300px; + background: var(--bg-sidebar); + display: flex; + flex-direction: column; + border-right: 1px solid rgba(255,255,255,0.1); + overflow-y: auto; + overflow-x: hidden; +} + +.logo { + padding: 20px; + font-size: 18px; + font-weight: bold; + color: var(--accent); + display: flex; + align-items: center; + gap: 10px; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.logo svg { + width: 28px; + height: 28px; + fill: currentColor; +} + +.nav { + padding: 15px; +} + +.nav-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 15px; + border-radius: 6px; + cursor: pointer; + color: var(--text-secondary); + transition: all 0.2s; + margin-bottom: 4px; + font-size: 14px; +} + +.nav-item:hover { + background: rgba(145, 71, 255, 0.15); + color: var(--text); +} + +.nav-item.active { + background: var(--accent); + color: white; +} + +.nav-item svg { + width: 20px; + height: 20px; +} + +.section-title { + font-size: 11px; + text-transform: uppercase; + color: var(--text-secondary); + padding: 15px 15px 8px; + letter-spacing: 0.5px; +} + +.streamers { + flex: 1; + overflow-y: auto; + padding: 0 10px; +} + +.streamer-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 6px; + cursor: pointer; + color: var(--text-secondary); + transition: all 0.2s; + font-size: 14px; +} + +.streamer-item:hover { + background: rgba(255,255,255,0.05); + color: var(--text); +} + +.streamer-item.active { + background: rgba(145, 71, 255, 0.2); + color: var(--text); + border-left: 3px solid var(--accent); +} + +.streamer-item .remove { + margin-left: auto; + opacity: 0; + color: var(--error); + cursor: pointer; +} + +.streamer-item:hover .remove { + opacity: 1; +} + +.add-streamer { + padding: 10px; + display: flex; + gap: 8px; +} + +.add-streamer input { + flex: 1; + background: var(--bg-card); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 4px; + padding: 8px 12px; + color: var(--text); + font-size: 13px; +} + +.add-streamer input::placeholder { + color: var(--text-secondary); +} + +.add-streamer button { + background: var(--accent); + border: none; + border-radius: 4px; + color: white; + width: 36px; + cursor: pointer; + font-size: 18px; +} + +/* Queue Section */ +.queue-section { + border-top: 1px solid rgba(255,255,255,0.1); + padding: 15px; +} + +.queue-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.queue-title { + font-size: 13px; + font-weight: 600; +} + +.queue-count { + background: var(--accent); + color: white; + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; +} + +.queue-list { + max-height: 150px; + overflow-y: auto; +} + +.queue-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + background: var(--bg-card); + border-radius: 4px; + margin-bottom: 6px; + font-size: 12px; +} + +.queue-item .status { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-secondary); +} + +.queue-item .status.pending { background: var(--warning); } +.queue-item .status.downloading { background: var(--accent); animation: pulse 1s infinite; } +.queue-item .status.completed { background: var(--success); } +.queue-item .status.error { background: var(--error); } + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.queue-item .title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.queue-item .remove { + cursor: pointer; + color: var(--error); + opacity: 0.7; +} + +.queue-item .remove:hover { + opacity: 1; +} + +.queue-actions { + display: flex; + gap: 8px; + margin-top: 10px; +} + +.btn { + flex: 1; + padding: 10px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + font-size: 13px; + transition: all 0.2s; +} + +.btn-start { + background: var(--success); + color: white; +} + +.btn-start:hover { + background: #00a844; +} + +.btn-start.downloading { + background: var(--error); +} + +.btn-clear { + background: var(--bg-card); + color: var(--text-secondary); +} + +.btn-clear:hover { + background: #2a2a2e; +} + +/* Main Content */ +.main { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.header { + padding: 20px 30px; + border-bottom: 1px solid rgba(255,255,255,0.1); + display: flex; + justify-content: space-between; + align-items: center; +} + +.header h1 { + font-size: 22px; + font-weight: 600; +} + +.header-actions { + display: flex; + align-items: center; + gap: 15px; +} + +.header-search { + display: flex; + gap: 8px; +} + +.header-search input { + background: var(--bg-card); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 4px; + padding: 8px 12px; + color: var(--text); + font-size: 13px; + width: 200px; +} + +.header-search input:focus { + outline: none; + border-color: var(--accent); +} + +.header-search button { + background: var(--accent); + border: none; + border-radius: 4px; + color: white; + padding: 8px 12px; + cursor: pointer; + font-size: 14px; + font-weight: bold; +} + +.header-search button:hover { + opacity: 0.9; +} + +.btn-icon { + background: var(--bg-card); + border: none; + border-radius: 4px; + color: var(--text); + padding: 8px 14px; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; +} + +.btn-icon:hover { + background: #2a2a2e; +} + +.content { + flex: 1; + overflow-y: auto; + padding: 25px 30px; +} + +/* Tabs */ +.tab-content { + display: none; +} + +.tab-content.active { + display: flex; + flex-direction: column; + min-height: 100%; +} + +/* VOD Grid */ +.vod-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + flex: 1; +} + +.vod-grid:has(.empty-state) { + display: flex; + align-items: center; + justify-content: center; +} + +.vod-card { + background: var(--bg-card); + border-radius: 8px; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; + cursor: pointer; +} + +.vod-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 25px rgba(0,0,0,0.3); +} + +.vod-thumbnail { + width: 100%; + aspect-ratio: 16/9; + background: #333; + object-fit: cover; +} + +.vod-info { + padding: 12px 15px; +} + +.vod-title { + font-weight: 600; + font-size: 14px; + margin-bottom: 6px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.4; +} + +.vod-meta { + display: flex; + gap: 12px; + font-size: 12px; + color: var(--text-secondary); +} + +.vod-actions { + padding: 10px 15px 15px; + display: flex; + gap: 8px; +} + +.vod-btn { + flex: 1; + padding: 8px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 500; + font-size: 12px; + transition: all 0.2s; +} + +.vod-btn.primary { + background: var(--accent); + color: white; +} + +.vod-btn.primary:hover { + background: var(--accent-hover); +} + +.vod-btn.secondary { + background: rgba(255,255,255,0.1); + color: var(--text); +} + +.vod-btn.secondary:hover { + background: rgba(255,255,255,0.15); +} + +/* Settings */ +.settings-card { + background: var(--bg-card); + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; +} + +.settings-card h3 { + font-size: 16px; + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 8px; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + font-size: 13px; + color: var(--text-secondary); + margin-bottom: 6px; +} + +.form-group input, .form-group select { + width: 100%; + background: var(--bg-main); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 4px; + padding: 10px 12px; + color: var(--text); + font-size: 14px; +} + +.form-group input:focus, .form-group select:focus { + outline: none; + border-color: var(--accent); +} + +.form-row { + display: flex; + gap: 10px; +} + +.form-row input { + flex: 1; +} + +.btn-primary { + background: var(--accent); + color: white; + border: none; + border-radius: 4px; + padding: 10px 20px; + cursor: pointer; + font-weight: 600; +} + +.btn-primary:hover { + background: var(--accent-hover); +} + +.btn-primary:disabled { + background: var(--text-secondary); + cursor: not-allowed; +} + +.btn-secondary { + background: var(--bg-card); + color: var(--text); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 4px; + padding: 10px 20px; + cursor: pointer; +} + +/* Clips */ +.clip-input { + max-width: 600px; + margin: 0 auto; + text-align: center; + padding: 40px 20px; +} + +.clip-input h2 { + margin-bottom: 20px; +} + +.clip-input input { + width: 100%; + background: var(--bg-card); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 4px; + padding: 12px 15px; + color: var(--text); + font-size: 14px; + margin-bottom: 15px; +} + +.clip-status { + margin-top: 15px; + font-size: 14px; +} + +.clip-status.success { color: var(--success); } +.clip-status.error { color: var(--error); } +.clip-status.loading { color: var(--warning); } + +/* Empty State */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + min-height: 60vh; + padding: 20px; + color: var(--text-secondary); +} + +.empty-state svg { + width: 64px; + height: 64px; + margin-bottom: 15px; + opacity: 0.5; +} + +.empty-state h3 { + margin-bottom: 8px; + color: var(--text); +} + +/* Status Bar */ +.status-bar { + padding: 10px 30px; + background: var(--bg-sidebar); + border-top: 1px solid rgba(255,255,255,0.1); + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--text-secondary); +} + +.status-indicator { + display: flex; + align-items: center; + gap: 8px; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-secondary); +} + +.status-dot.connected { background: var(--success); } +.status-dot.error { background: var(--error); } + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.15); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.25); +} + +/* Update Banner */ +.update-banner { + background: linear-gradient(90deg, var(--accent), #5a2d82); + padding: 10px 20px; + display: none; + justify-content: center; + align-items: center; + gap: 15px; + font-size: 13px; +} + +.update-banner.show { + display: flex; +} + +.update-banner button { + background: white; + color: var(--accent); + border: none; + border-radius: 4px; + padding: 6px 15px; + cursor: pointer; + font-weight: 600; +} + +.update-banner button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +#updateProgressBar.downloading { + width: 30% !important; + animation: indeterminate 1.5s ease-in-out infinite; +} + +@keyframes indeterminate { + 0% { margin-left: 0; width: 30%; } + 50% { margin-left: 35%; width: 30%; } + 100% { margin-left: 70%; width: 30%; } +} + +/* Video Cutter Styles */ +.cutter-container { + max-width: 900px; + margin: 0 auto; +} + +.video-preview { + background: #000; + border-radius: 8px; + overflow: hidden; + margin-bottom: 20px; + aspect-ratio: 16/9; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.video-preview img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.video-preview .placeholder { + color: var(--text-secondary); + text-align: center; +} + +.timeline-container { + background: var(--bg-card); + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; +} + +.timeline { + position: relative; + height: 60px; + background: var(--bg-main); + border-radius: 4px; + margin: 15px 0; + cursor: pointer; +} + +.timeline-selection { + position: absolute; + top: 0; + height: 100%; + background: rgba(145, 71, 255, 0.3); + border-left: 3px solid var(--accent); + border-right: 3px solid var(--accent); +} + +.timeline-handle { + position: absolute; + top: -5px; + width: 12px; + height: 70px; + background: var(--accent); + border-radius: 3px; + cursor: ew-resize; +} + +.timeline-handle.start { left: 0; transform: translateX(-50%); } +.timeline-handle.end { right: 0; transform: translateX(50%); } + +.timeline-current { + position: absolute; + top: 0; + width: 2px; + height: 100%; + background: var(--success); + pointer-events: none; +} + +.time-inputs { + display: flex; + gap: 20px; + align-items: center; + justify-content: center; + margin-top: 15px; +} + +.time-input-group { + display: flex; + align-items: center; + gap: 8px; +} + +.time-input-group label { + color: var(--text-secondary); + font-size: 13px; +} + +.time-input-group input { + width: 100px; + background: var(--bg-main); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 4px; + padding: 8px 10px; + color: var(--text); + text-align: center; + font-family: monospace; +} + +.cutter-actions { + display: flex; + gap: 10px; + justify-content: center; + margin-top: 20px; +} + +.cutter-info { + background: var(--bg-card); + border-radius: 8px; + padding: 15px 20px; + margin-bottom: 20px; + display: flex; + justify-content: space-around; + text-align: center; +} + +.cutter-info-item { + display: flex; + flex-direction: column; + gap: 5px; +} + +.cutter-info-label { + font-size: 12px; + color: var(--text-secondary); +} + +.cutter-info-value { + font-size: 16px; + font-weight: 600; + font-family: monospace; +} + +/* Merge Styles */ +.merge-container { + max-width: 800px; + margin: 0 auto; +} + +.file-list { + background: var(--bg-card); + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + min-height: 200px; +} + +.file-item { + display: flex; + align-items: center; + gap: 15px; + padding: 12px 15px; + background: var(--bg-main); + border-radius: 6px; + margin-bottom: 10px; +} + +.file-item .file-order { + width: 30px; + height: 30px; + background: var(--accent); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 14px; +} + +.file-item .file-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-item .file-actions { + display: flex; + gap: 8px; +} + +.file-item .file-btn { + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 5px; + font-size: 16px; +} + +.file-item .file-btn:hover { + color: var(--text); +} + +.file-item .file-btn.remove:hover { + color: var(--error); +} + +.merge-actions { + display: flex; + gap: 10px; + justify-content: center; +} + +/* Progress Bar */ +.progress-container { + background: var(--bg-card); + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + display: none; +} + +.progress-container.show { + display: block; +} + +.progress-bar { + height: 8px; + background: var(--bg-main); + border-radius: 4px; + overflow: hidden; + margin-bottom: 10px; +} + +.progress-bar-fill { + height: 100%; + background: var(--accent); + transition: width 0.3s; +} + +.progress-text { + text-align: center; + color: var(--text-secondary); + font-size: 14px; +} + +/* Theme variations */ +body.theme-discord { + --bg-main: #36393f; + --bg-sidebar: #202225; + --bg-card: #2f3136; + --accent: #5865F2; + --accent-hover: #4752C4; +} + +body.theme-youtube { + --bg-main: #0f0f0f; + --bg-sidebar: #0f0f0f; + --bg-card: #272727; + --accent: #FF0000; + --accent-hover: #cc0000; +} + +body.theme-apple { + --bg-main: #1c1c1e; + --bg-sidebar: #2c2c2e; + --bg-card: #3a3a3c; + --accent: #0A84FF; + --accent-hover: #0071e3; +} + +/* Modal Styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: none; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-overlay.show { + display: flex; +} + +.modal { + background: var(--bg-card); + border-radius: 12px; + padding: 25px; + width: 90%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; +} + +.modal h2 { + margin-bottom: 20px; + font-size: 18px; +} + +.modal-close { + float: right; + background: none; + border: none; + color: var(--text-secondary); + font-size: 24px; + cursor: pointer; + padding: 0; + line-height: 1; +} + +.modal-close:hover { + color: var(--text); +} + +.slider-group { + margin-bottom: 20px; +} + +.slider-group label { + display: block; + margin-bottom: 8px; + color: var(--text-secondary); + font-size: 13px; +} + +.slider-group input[type="range"], +.modal input[type="range"] { + width: 100%; + height: 6px; + -webkit-appearance: none; + background: #1a1a1a; + border-radius: 3px; + outline: none; +} + +.slider-group input[type="range"]::-webkit-slider-thumb, +.modal input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: #E5A00D; + border-radius: 50%; + cursor: pointer; +} + +.modal input[type="range"]::-webkit-slider-thumb:hover { + background: #ffb825; +} + +.clip-time-display { + display: flex; + justify-content: space-between; + margin-top: 8px; + font-family: monospace; + font-size: 14px; +} + +.clip-info-row { + background: var(--bg-main); + padding: 12px 15px; + border-radius: 6px; + margin-bottom: 15px; + text-align: center; +} + +.clip-info-row .label { + color: var(--text-secondary); + font-size: 12px; + margin-bottom: 4px; +} + +.clip-info-row .value { + font-size: 18px; + font-weight: 600; + color: var(--success); +} + +.clip-info-row .value.error { + color: var(--error); +} + +.part-number-group { + margin-bottom: 20px; +} + +.part-number-group input { + width: 100px; + background: var(--bg-main); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 4px; + padding: 8px 12px; + color: var(--text); + font-size: 14px; +} + +.part-number-group small { + display: block; + margin-top: 5px; + color: var(--text-secondary); + font-size: 11px; +} + +.modal-actions { + display: flex; + gap: 10px; + margin-top: 20px; +} + +.modal-actions button { + flex: 1; +}