Twitch-VOD-Manager/src/index.html
xRangerDE fdb096fa96 feat: streamlink quality preference + per-item notifications + path validation
Three Phase-11 wins.

1. Streamlink stream quality is now configurable. config.streamlink_quality
   defaults to "best" (preserves prior behaviour) but can be set to source,
   1080p60, 720p60, 720p, 480p, or audio_only via a new dropdown in
   Settings -> Download. The chosen quality is passed as STREAMS to
   streamlink with ",best" appended as a fallback so an old VOD lacking
   the chosen rendition still completes. Used by both the queue
   downloadVODPart and the standalone download-clip IPC. The whitelist is
   enforced via normalizeStreamlinkQuality so an arbitrary string in the
   config file falls back to "best".

2. Per-item completion notifications. Default off because long queues
   would spam the OS notifications panel. When enabled (Settings ->
   Queue zwischen App-Starts checkbox area), every successful download
   pops a "{title}" notification whose click brings the window forward
   AND opens shell.showItemInFolder on the produced file (or the
   download folder if the file is gone). The end-of-queue summary
   notification still fires regardless.

3. Download-path writability check on selectFolder. The renderer now
   asks the new check-folder-writable IPC after the user picks a
   folder; if isDownloadPathWritable returns false, a warning toast
   surfaces immediately instead of the next download failing with a
   cryptic "datei zu klein" / "ENOENT" error. Save proceeds anyway —
   the user might be picking a USB-stick path that is offline at the
   moment.

Plus DE + EN locale strings for every label/option/hint, all wired
through applyLanguageToStaticUI for live language switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:00:36 +02:00

632 lines
45 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:;">
<title>Twitch VOD Manager</title>
<link rel="stylesheet" href="./styles.css">
</head>
<body class="theme-twitch">
<div class="update-banner" id="updateBanner">
<span id="updateText">Neue Version verfügbar!</span>
<div id="updateProgress" style="display: none; flex: 1; margin: 0 15px;">
<div style="background: rgba(0,0,0,0.3); border-radius: 4px; height: 8px; overflow: hidden;">
<div id="updateProgressBar" style="background: white; height: 100%; width: 0%; transition: width 0.3s;"></div>
</div>
</div>
<button id="updateButton" onclick="downloadUpdate()">Jetzt herunterladen</button>
</div>
<div class="modal-overlay" id="updateModal" onclick="handleUpdateModalOverlayClick(event)">
<div class="modal update-modal">
<button class="modal-close" onclick="dismissUpdateModal()">x</button>
<div class="update-modal-eyebrow" id="updateModalEyebrow">Updates</div>
<h2 id="updateModalTitle">Update verfugbar</h2>
<p class="update-modal-message" id="updateModalMessage">Version 0.0.0 ist verfugbar. Jetzt herunterladen?</p>
<div class="update-modal-meta" id="updateModalMeta" style="display:none;"></div>
<div class="update-changelog-card" id="updateChangelogCard" style="display:none;">
<div class="update-changelog-header">
<span class="update-changelog-label" id="updateChangelogLabel">Changelog</span>
<button type="button" class="update-changelog-toggle" id="updateChangelogToggle" onclick="toggleUpdateChangelog()">Changelog anzeigen</button>
</div>
<div class="update-changelog-panel" id="updateChangelogPanel" hidden>
<div class="update-changelog-content" id="updateChangelogContent"></div>
<p class="update-changelog-empty" id="updateChangelogEmpty" hidden>Kein Changelog verfugbar.</p>
</div>
</div>
<div class="modal-actions update-modal-actions">
<button class="btn-secondary" id="updateModalDismissBtn" type="button" onclick="dismissUpdateModal()">Nein</button>
<button class="btn-secondary" id="updateModalSkipBtn" type="button" onclick="skipUpdateVersion()">Diese Version ueberspringen</button>
<button class="btn-primary" id="updateModalConfirmBtn" type="button" onclick="confirmUpdateModal()">Ja, herunterladen</button>
</div>
</div>
</div>
<!-- Clip Dialog Modal -->
<div class="modal-overlay" id="clipModal">
<div class="modal" style="background: #2b2b2b; max-width: 500px;">
<button class="modal-close" onclick="closeClipDialog()">x</button>
<h2 style="color: #E5A00D; text-align: center; margin-bottom: 20px;" id="clipDialogTitle">VOD zuschneiden</h2>
<!-- Start Zeit mit Slider -->
<div style="margin-bottom: 15px;">
<label id="clipDialogStartLabel" style="display: block; margin-bottom: 5px;">Start:</label>
<input type="range" id="clipStartSlider" min="0" max="100" value="0"
style="width: 100%; height: 6px; -webkit-appearance: none; background: #1a1a1a; border-radius: 3px; cursor: pointer;"
oninput="updateFromSlider('start')">
<div style="display: flex; align-items: center; gap: 10px; margin-top: 8px;">
<label id="clipDialogStartTimeLabel" style="color: #888;">Startzeit (HH:MM:SS):</label>
<input type="text" id="clipStartTime" value="00:00:00"
style="width: 100px; background: #333; border: 1px solid #444; border-radius: 4px; padding: 6px 10px; color: white; font-family: monospace; text-align: center;"
onchange="updateFromInput('start')">
</div>
</div>
<!-- End Zeit mit Slider -->
<div style="margin-bottom: 15px;">
<label id="clipDialogEndLabel" style="display: block; margin-bottom: 5px;">Ende:</label>
<input type="range" id="clipEndSlider" min="0" max="100" value="60"
style="width: 100%; height: 6px; -webkit-appearance: none; background: #1a1a1a; border-radius: 3px; cursor: pointer;"
oninput="updateFromSlider('end')">
<div style="display: flex; align-items: center; gap: 10px; margin-top: 8px;">
<label id="clipDialogEndTimeLabel" style="color: #888;">Endzeit (HH:MM:SS):</label>
<input type="text" id="clipEndTime" value="00:01:00"
style="width: 100px; background: #333; border: 1px solid #444; border-radius: 4px; padding: 6px 10px; color: white; font-family: monospace; text-align: center;"
onchange="updateFromInput('end')">
</div>
</div>
<!-- Dauer Anzeige -->
<div style="text-align: center; margin-bottom: 20px;">
<span id="clipDialogDurationLabel" style="color: #888;">Dauer: </span>
<span id="clipDurationDisplay" style="color: #00c853;">00:01:00</span>
</div>
<!-- Teil Nummer -->
<div style="margin-bottom: 15px;">
<label id="clipDialogPartLabel" style="display: block; margin-bottom: 8px;">Start Part-Nummer (optional, fur Fortsetzung):</label>
<input type="text" id="clipStartPart" placeholder="z.B. 42"
style="width: 100px; background: #333; border: 1px solid #444; border-radius: 4px; padding: 8px 12px; color: white;"
oninput="updateFilenameExamples()">
<div id="clipDialogPartHint" style="color: #888; font-size: 12px; margin-top: 5px;">Leer lassen = Teil 1</div>
</div>
<!-- Dateinamen Format -->
<div style="margin-bottom: 20px;">
<label id="clipDialogFormatLabel" style="display: block; margin-bottom: 10px;">Dateinamen-Format:</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
<input type="radio" name="filenameFormat" value="simple" checked onchange="updateFilenameExamples()"
style="width: 18px; height: 18px; accent-color: #9146FF;">
<span id="formatSimple" style="color: #aaa;">01.02.2026_1.mp4 (Standard)</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
<input type="radio" name="filenameFormat" value="timestamp" onchange="updateFilenameExamples()"
style="width: 18px; height: 18px; accent-color: #9146FF;">
<span id="formatTimestamp" style="color: #aaa;">01.02.2026_CLIP_00-00-00_1.mp4 (mit Zeitstempel)</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
<input type="radio" name="filenameFormat" value="parts" onchange="updateFilenameExamples()"
style="width: 18px; height: 18px; accent-color: #9146FF;">
<span id="formatParts" style="color: #aaa;">01.02.2026_Part01.mp4 (Parts-Format)</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 10px;">
<input type="radio" name="filenameFormat" value="template" onchange="updateFilenameExamples()"
style="width: 18px; height: 18px; accent-color: #9146FF;">
<span id="formatTemplate" style="color: #aaa;">{date}_{part}.mp4 (benutzerdefiniert)</span>
</label>
<div id="clipFilenameTemplateWrap" style="display:none; margin-top: 10px;">
<input type="text" id="clipFilenameTemplate" value="{date}_{part}.mp4"
placeholder="{date}_{part}.mp4"
style="width: 100%; background: #333; border: 1px solid #444; border-radius: 4px; padding: 8px 12px; color: white; font-family: monospace;"
oninput="updateFilenameExamples()">
<div id="clipTemplateHelp" style="color: #888; font-size: 12px; margin-top: 6px;">Platzhalter: {title} {id} {channel} {date} {part} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}</div>
<div id="clipTemplateLint" style="color: #8bc34a; font-size: 12px; margin-top: 4px;">Template-Check: OK</div>
<button class="btn-secondary" id="clipTemplateGuideBtn" style="margin-top: 8px;" onclick="openTemplateGuide('clip')">Template Guide</button>
</div>
</div>
<!-- Button -->
<div style="text-align: center;">
<button class="btn-primary" id="clipDialogConfirmBtn" style="background: #00c853; padding: 12px 30px; border: none; border-radius: 4px; color: white; font-weight: 600; cursor: pointer;" onclick="confirmClipDialog()">Zur Queue hinzufugen</button>
</div>
</div>
</div>
<!-- Template Guide Modal -->
<div class="modal-overlay" id="templateGuideModal">
<div class="modal template-guide-modal">
<button class="modal-close" onclick="closeTemplateGuide()">x</button>
<h2 id="templateGuideTitle">Template Guide</h2>
<p id="templateGuideIntro" class="template-guide-intro">Nutze Variablen fur Dateinamen und prufe das Ergebnis als Live-Vorschau.</p>
<div class="template-guide-actions">
<button class="btn-secondary" id="templateGuideUseVod" onclick="setTemplateGuidePreset('vod')">VOD Template</button>
<button class="btn-secondary" id="templateGuideUseParts" onclick="setTemplateGuidePreset('parts')">VOD Part Template</button>
<button class="btn-secondary" id="templateGuideUseClip" onclick="setTemplateGuidePreset('clip')">Clip Template</button>
</div>
<label id="templateGuideTemplateLabel" class="template-guide-label">Template</label>
<input type="text" id="templateGuideInput" class="template-guide-input" oninput="updateTemplateGuidePreview()" placeholder="{title}.mp4">
<div class="template-guide-preview-box">
<div class="template-guide-preview-label" id="templateGuideOutputLabel">Live Vorschau</div>
<div id="templateGuideOutput" class="template-guide-output">-</div>
<div id="templateGuideContext" class="template-guide-context"></div>
</div>
<h3 id="templateGuideVarsTitle" class="template-guide-vars-title">Verfugbare Variablen</h3>
<div class="template-guide-table-wrap">
<table class="template-guide-table">
<thead>
<tr>
<th id="templateGuideVarCol">Variable</th>
<th id="templateGuideDescCol">Beschreibung</th>
<th id="templateGuideExampleCol">Beispiel</th>
</tr>
</thead>
<tbody id="templateGuideBody"></tbody>
</table>
</div>
<div class="template-guide-footer">
<button class="btn-secondary" id="templateGuideCloseBtn" onclick="closeTemplateGuide()">Schliessen</button>
</div>
</div>
</div>
<div class="app">
<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>
<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>
<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>
<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>
<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>
<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>
<span id="navSettingsText">Einstellungen</span>
</div>
</nav>
<div class="section-title" id="streamerSectionTitle" style="display:flex; align-items:center; gap:6px; justify-content:space-between;">
<span id="streamerSectionTitleText">Streamer</span>
<button id="btnStreamerBulkRemove" type="button" onclick="bulkRemoveStreamers()" title="Bulk remove" style="display:none; background:transparent; border:1px solid var(--border-soft); border-radius:4px; padding:2px 8px; color:var(--text-secondary); font-size:11px; cursor:pointer;">x</button>
</div>
<input type="text" id="streamerListFilter" placeholder="Filter..." oninput="onStreamerListFilterChange()" style="display:none; width:calc(100% - 16px); margin:0 8px 8px; background:var(--bg-card); border:1px solid var(--border-soft); border-radius:4px; padding:4px 8px; color:var(--text); font-size:12px;">
<div class="streamers" id="streamerList"></div>
<div class="queue-section">
<div class="queue-header">
<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-merge-group" id="btnMergeGroup" onclick="createMergeGroupFromSelection()" style="display:none">Merge &amp; Split</button>
<button class="btn btn-retry" id="btnRetryFailed" onclick="retryFailedDownloads()" title="Nur fehlgeschlagene Downloads erneut starten">Wiederholen</button>
<button class="btn btn-clear" id="btnClear" onclick="clearCompleted()">Leeren</button>
</div>
</div>
<div class="stats-bar" id="statsBar"></div>
</aside>
<main class="main">
<header class="header">
<h1 id="pageTitle">VODs</h1>
<div class="header-actions">
<div class="header-search">
<input type="text" id="newStreamer" placeholder="Streamer hinzufugen..." onkeypress="if(event.key==='Enter')addStreamer()">
<button onclick="addStreamer()">+</button>
</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>
<span id="refreshText">Aktualisieren</span>
</button>
</div>
</header>
<div class="content">
<!-- VODs Tab -->
<div class="tab-content active" id="vodsTab">
<div class="vod-filter-row" style="display:flex; align-items:center; gap:8px; margin-bottom:12px; flex-wrap:wrap;">
<input type="text" id="vodFilterInput" placeholder="Filter VODs..." oninput="onVodFilterInput()" style="flex:1; min-width:180px; background: var(--bg-card); border:1px solid var(--border-soft); border-radius:6px; padding:8px 12px; color: var(--text); font-size:13px;">
<button id="vodFilterClearBtn" onclick="clearVodFilter()" title="Clear filter" style="display:none; background:transparent; border:1px solid var(--border-soft); border-radius:6px; padding:8px 12px; color: var(--text-secondary); cursor:pointer;">x</button>
<label id="vodSortLabel" for="vodSortSelect" style="color: var(--text-secondary); font-size:12px; margin-left:8px;">Sort:</label>
<select id="vodSortSelect" onchange="onVodSortChange()" style="background: var(--bg-card); border:1px solid var(--border-soft); border-radius:6px; padding:7px 10px; color: var(--text); font-size:13px;">
<option value="date_desc">Newest first</option>
<option value="date_asc">Oldest first</option>
<option value="views_desc">Most viewed</option>
<option value="duration_desc">Longest first</option>
<option value="duration_asc">Shortest first</option>
</select>
<span id="vodFilterCount" style="color: var(--text-secondary); font-size:12px; min-width:80px;"></span>
<label id="vodHideDownloadedLabel" style="display:flex; align-items:center; gap:6px; color: var(--text-secondary); font-size:12px; cursor:pointer; user-select:none;" title="">
<input type="checkbox" id="vodHideDownloadedToggle" onchange="onVodHideDownloadedChange()" style="accent-color: var(--accent); cursor:pointer;">
<span id="vodHideDownloadedText">Hide downloaded</span>
</label>
</div>
<div id="vodBulkBar" class="vod-bulk-bar" style="display:none; align-items:center; gap:10px; padding:8px 12px; background: rgba(145, 70, 255, 0.12); border:1px solid rgba(145, 70, 255, 0.4); border-radius:6px; margin-bottom:12px;">
<span id="vodBulkCount" style="color: var(--text); font-size:13px; font-weight:600;">0 selected</span>
<span style="flex:1;"></span>
<button id="vodBulkAddBtn" type="button" onclick="bulkAddSelectedVodsToQueue()" style="background:var(--accent); border:none; border-radius:6px; padding:6px 14px; color:#fff; font-size:13px; font-weight:600; cursor:pointer;">+ Queue</button>
<button id="vodBulkClearBtn" type="button" onclick="clearVodSelection()" style="background:transparent; border:1px solid var(--border-soft); border-radius:6px; padding:6px 12px; color:var(--text-secondary); font-size:13px; cursor:pointer;">Clear</button>
</div>
<div class="vod-grid" id="vodGrid">
<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 oder fuge einen neuen hinzu.</p>
</div>
</div>
</div>
<!-- Clips Tab -->
<div class="tab-content" id="clipsTab">
<div class="clip-input">
<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 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>
</div>
<!-- Video Cutter Tab -->
<div class="tab-content" id="cutterTab">
<div class="cutter-container">
<div class="settings-card">
<h3 id="cutterSelectTitle">Video auswahlen</h3>
<div class="form-row">
<input type="text" id="cutterFilePath" readonly placeholder="Keine Datei ausgewahlt...">
<button class="btn-secondary" id="cutterBrowseBtn" onclick="selectCutterVideo()">Durchsuchen</button>
</div>
</div>
<div class="video-preview" id="cutterPreview">
<div class="placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="currentColor" style="opacity:0.3"><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>
<p style="margin-top:10px">Video auswahlen um Vorschau zu sehen</p>
</div>
</div>
<div class="cutter-info" id="cutterInfo" style="display:none">
<div class="cutter-info-item">
<span class="cutter-info-label" id="cutterInfoDurationLabel">Dauer</span>
<span class="cutter-info-value" id="infoDuration">--:--:--</span>
</div>
<div class="cutter-info-item">
<span class="cutter-info-label" id="cutterInfoResolutionLabel">Aufloesung</span>
<span class="cutter-info-value" id="infoResolution">----x----</span>
</div>
<div class="cutter-info-item">
<span class="cutter-info-label" id="cutterInfoFpsLabel">FPS</span>
<span class="cutter-info-value" id="infoFps">--</span>
</div>
<div class="cutter-info-item">
<span class="cutter-info-label" id="cutterInfoSelectionLabel">Auswahl</span>
<span class="cutter-info-value" id="infoSelection">--:--:--</span>
</div>
</div>
<div class="timeline-container" id="timelineContainer" style="display:none">
<div class="timeline" id="timeline" onclick="seekTimeline(event)">
<div class="timeline-selection" id="timelineSelection"></div>
<div class="timeline-current" id="timelineCurrent"></div>
</div>
<div class="time-inputs">
<div class="time-input-group">
<label id="cutterStartLabel">Start:</label>
<input type="text" id="startTime" value="00:00:00" onchange="updateTimeFromInput()">
</div>
<div class="time-input-group">
<label id="cutterEndLabel">Ende:</label>
<input type="text" id="endTime" value="00:00:00" onchange="updateTimeFromInput()">
</div>
</div>
</div>
<div class="progress-container" id="cutProgress">
<div class="progress-bar">
<div class="progress-bar-fill" id="cutProgressBar"></div>
</div>
<div class="progress-text" id="cutProgressText">0%</div>
</div>
<div class="cutter-actions">
<button class="btn-primary" id="btnCut" onclick="startCutting()" disabled>Schneiden</button>
</div>
</div>
</div>
<!-- Merge Tab -->
<div class="tab-content" id="mergeTab">
<div class="merge-container">
<div class="settings-card">
<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" id="mergeAddBtn" onclick="addMergeFiles()">+ Videos hinzufugen</button>
</div>
<div class="file-list" id="mergeFileList">
<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>
</div>
</div>
<div class="progress-container" id="mergeProgress">
<div class="progress-bar">
<div class="progress-bar-fill" id="mergeProgressBar"></div>
</div>
<div class="progress-text" id="mergeProgressText">0%</div>
</div>
<div class="merge-actions">
<button class="btn-primary" id="btnMerge" onclick="startMerging()" disabled>Zusammenfugen</button>
</div>
</div>
</div>
<!-- Settings Tab -->
<div class="tab-content" id="settingsTab">
<div class="settings-card">
<h3 id="designTitle">Design</h3>
<div class="form-group">
<label id="themeLabel">Theme</label>
<select id="themeSelect" onchange="changeTheme(this.value)">
<option value="twitch">Twitch</option>
<option value="discord">Discord</option>
<option value="youtube">YouTube</option>
<option value="apple">Apple</option>
<option value="light" id="themeLightOption">Light</option>
</select>
</div>
<div class="form-group">
<label id="languageLabel">Sprache</label>
<div class="language-picker" id="languagePicker">
<button type="button" class="lang-option" id="langOptionDe" onclick="selectLanguageOption('de')" aria-pressed="false">
<span class="flag-icon flag-de" aria-hidden="true"></span>
<span id="languageDeText">Deutsch</span>
</button>
<button type="button" class="lang-option" id="langOptionEn" onclick="selectLanguageOption('en')" aria-pressed="false">
<span class="flag-icon flag-en" aria-hidden="true"></span>
<span id="languageEnText">English</span>
</button>
</div>
<select id="languageSelect" onchange="changeLanguage(this.value)" style="display:none">
<option value="de">de</option>
<option value="en">en</option>
</select>
</div>
</div>
<div class="settings-card">
<h3 id="apiTitle">Twitch API</h3>
<p id="apiHelpText" style="color: var(--text-secondary); font-size:13px; margin-bottom:12px; line-height:1.5;">
<span id="apiHelpIntro">Du brauchst eine Client-ID und ein Client-Secret von Twitch.</span>
<a href="#" id="apiHelpLink" onclick="event.preventDefault(); openTwitchDevConsole()" style="color: var(--accent); text-decoration: underline; cursor: pointer;">dev.twitch.tv/console/apps</a>
</p>
<div class="form-group">
<label id="clientIdLabel">Client ID</label>
<input type="text" id="clientId" placeholder="Twitch Client ID">
</div>
<div class="form-group">
<label id="clientSecretLabel">Client Secret</label>
<input type="password" id="clientSecret" placeholder="Twitch Client Secret">
</div>
<button class="btn-primary" id="saveSettingsBtn" onclick="saveSettings()">Speichern & Verbinden</button>
</div>
<div class="settings-card">
<h3 id="downloadSettingsTitle">Download-Einstellungen</h3>
<div class="form-group">
<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" id="openFolderBtn" onclick="openFolder()">Offnen</button>
</div>
</div>
<div class="form-group">
<label id="modeLabel">Download-Modus</label>
<select id="downloadMode">
<option value="full" id="modeFullText">Ganzes VOD</option>
<option value="parts" id="modePartsText">In Teile splitten</option>
</select>
</div>
<div class="form-group">
<label id="partMinutesLabel">Teil-Lange (Minuten)</label>
<input type="number" id="partMinutes" value="120" min="10" max="480">
</div>
<div class="form-group">
<label id="parallelDownloadsLabel">Parallele Downloads</label>
<select id="parallelDownloads">
<option value="1" id="parallelDownloads1">1 (Standard)</option>
<option value="2" id="parallelDownloads2">2 (Parallel)</option>
</select>
</div>
<div class="form-group">
<label id="streamlinkQualityLabel">Stream-Qualitaet</label>
<select id="streamlinkQuality">
<option value="best" id="streamlinkQualityBest">Best (Standard)</option>
<option value="source" id="streamlinkQualitySource">Source (Original)</option>
<option value="1080p60" id="streamlinkQuality1080p60">1080p60</option>
<option value="720p60" id="streamlinkQuality720p60">720p60</option>
<option value="720p" id="streamlinkQuality720p">720p</option>
<option value="480p" id="streamlinkQuality480p">480p</option>
<option value="audio_only" id="streamlinkQualityAudio">Audio only</option>
</select>
</div>
<div class="form-group">
<label id="performanceModeLabel">Performance-Profil</label>
<select id="performanceMode">
<option value="stability" id="performanceModeStability">Max Stabilitat</option>
<option value="balanced" id="performanceModeBalanced">Ausgewogen</option>
<option value="speed" id="performanceModeSpeed">Max Geschwindigkeit</option>
</select>
</div>
<div class="form-group">
<label style="display:flex; align-items:center; gap:8px;">
<input type="checkbox" id="smartSchedulerToggle" checked>
<span id="smartSchedulerLabel">Smart Queue Scheduler aktivieren</span>
</label>
<label style="display:flex; align-items:center; gap:8px; margin-top: 8px;">
<input type="checkbox" id="duplicatePreventionToggle" checked>
<span id="duplicatePreventionLabel">Duplikate in Queue verhindern</span>
</label>
<label style="display:flex; align-items:center; gap:8px; margin-top: 8px;">
<input type="checkbox" id="persistQueueToggle" checked>
<span id="persistQueueLabel">Queue zwischen App-Starts speichern</span>
</label>
<label style="display:flex; align-items:center; gap:8px; margin-top: 8px;">
<input type="checkbox" id="autoResumeQueueToggle">
<span id="autoResumeQueueLabel">Queue beim Start automatisch fortsetzen</span>
</label>
<label style="display:flex; align-items:center; gap:8px; margin-top: 8px;">
<input type="checkbox" id="notifyEachCompletionToggle">
<span id="notifyEachCompletionLabel">Benachrichtigung bei jedem fertigen Download</span>
</label>
</div>
<div class="form-group">
<label id="metadataCacheMinutesLabel">Metadata-Cache (Minuten)</label>
<input type="number" id="metadataCacheMinutes" value="10" min="1" max="120">
</div>
<div class="form-group">
<div class="form-row" style="align-items:center; margin-bottom: 4px;">
<label id="filenameTemplatesTitle" style="margin: 0;">Dateinamen-Templates</label>
<button class="btn-secondary" id="settingsTemplateGuideBtn" type="button" onclick="openTemplateGuide('vod')">Template Guide</button>
</div>
<div class="form-row" style="gap: 8px; margin: 8px 0 6px;">
<button class="btn-secondary" id="templatePresetDefault" type="button" onclick="applyTemplatePreset('default')">Preset: Default</button>
<button class="btn-secondary" id="templatePresetArchive" type="button" onclick="applyTemplatePreset('archive')">Preset: Archive</button>
<button class="btn-secondary" id="templatePresetClipper" type="button" onclick="applyTemplatePreset('clipper')">Preset: Clipper</button>
</div>
<div style="display: grid; gap: 8px; margin-top: 8px;">
<label id="vodTemplateLabel" style="font-size: 13px; color: var(--text-secondary);">VOD Template</label>
<input type="text" id="vodFilenameTemplate" placeholder="{title}.mp4" style="font-family: monospace;" oninput="validateFilenameTemplates()">
<label id="partsTemplateLabel" style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;">VOD Part Template</label>
<input type="text" id="partsFilenameTemplate" placeholder="{date}_Part{part_padded}.mp4" style="font-family: monospace;" oninput="validateFilenameTemplates()">
<label id="defaultClipTemplateLabel" style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;">Clip Template</label>
<input type="text" id="defaultClipFilenameTemplate" placeholder="{date}_{part}.mp4" style="font-family: monospace;" oninput="validateFilenameTemplates()">
</div>
<div id="filenameTemplateHint" style="color: #888; font-size: 12px; margin-top: 8px;">Platzhalter: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}</div>
<div id="filenameTemplateLint" style="font-size: 12px; margin-top: 6px; color: #8bc34a;">Template-Check: OK</div>
</div>
</div>
<div class="settings-card">
<h3 id="updateTitle">Updates</h3>
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.1.13</p>
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
</div>
<div class="settings-card">
<div class="form-row" style="align-items:center; justify-content:space-between; margin-bottom: 10px;">
<h3 id="preflightTitle" style="margin: 0;">System-Check</h3>
<span class="health-badge unknown" id="healthBadge">System: Unbekannt</span>
</div>
<div class="form-row" style="margin-bottom: 10px;">
<button class="btn-secondary" id="btnPreflightRun" onclick="runPreflight(false)">Check ausfuhren</button>
<button class="btn-secondary" id="btnPreflightFix" onclick="runPreflight(true)">Auto-Fix Tools</button>
</div>
<pre id="preflightResult" class="log-panel">Noch kein Check ausgefuhrt.</pre>
</div>
<div class="settings-card">
<h3 id="debugLogTitle">Live Debug-Log</h3>
<div class="form-row" style="margin-bottom: 10px; align-items: center;">
<button class="btn-secondary" id="btnRefreshLog" onclick="refreshDebugLog()">Aktualisieren</button>
<button class="btn-secondary" id="btnOpenDebugLogFile" onclick="openDebugLogFile()">Log-Datei oeffnen</button>
<label style="display:flex; align-items:center; gap:6px; font-size:13px; color: var(--text-secondary);">
<input type="checkbox" id="debugAutoRefresh" onchange="toggleDebugAutoRefresh(this.checked)">
<span id="autoRefreshText">Auto-Refresh</span>
</label>
</div>
<pre id="debugLogOutput" class="log-panel">Lade...</pre>
</div>
<div class="settings-card">
<h3 id="backupCardTitle">Sicherung &amp; Wartung</h3>
<p id="backupCardIntro" style="color: var(--text-secondary); font-size:13px; margin-bottom:12px; line-height:1.5;">Konfiguration sichern, auf einem anderen Geraet wiederherstellen, oder die Liste der bereits heruntergeladenen VODs zuruecksetzen.</p>
<div class="form-row" style="margin-bottom: 10px; flex-wrap: wrap;">
<button class="btn-secondary" id="btnExportConfig" onclick="exportConfigToFile()">Konfiguration exportieren</button>
<button class="btn-secondary" id="btnImportConfig" onclick="importConfigFromFile()">Konfiguration importieren</button>
<button class="btn-secondary" id="btnResetDownloadedIds" onclick="resetDownloadedIds()">Downloaded-VODs zuruecksetzen</button>
</div>
</div>
<div class="settings-card">
<h3 id="runtimeMetricsTitle">Runtime Metrics</h3>
<div class="form-row" style="margin-bottom: 10px; align-items: center;">
<button class="btn-secondary" id="btnRefreshMetrics" onclick="refreshRuntimeMetrics()">Aktualisieren</button>
<button class="btn-secondary" id="btnExportMetrics" onclick="exportRuntimeMetrics()">Export JSON</button>
<label style="display:flex; align-items:center; gap:6px; font-size:13px; color: var(--text-secondary);">
<input type="checkbox" id="runtimeMetricsAutoRefresh" onchange="toggleRuntimeMetricsAutoRefresh(this.checked)">
<span id="runtimeMetricsAutoRefreshText">Auto-Refresh</span>
</label>
</div>
<pre id="runtimeMetricsOutput" class="log-panel">Lade...</pre>
</div>
</div>
</div>
<div class="status-bar">
<div class="status-indicator">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Nicht verbunden</span>
</div>
<span id="statusBarQueueSummary" style="color: var(--text-secondary); font-size:12px; margin-left:auto; padding-right:12px;"></span>
<span id="versionText">v4.1.13</span>
</div>
</main>
</div>
<script src="../dist/renderer-locale-de.js"></script>
<script src="../dist/renderer-locale-en.js"></script>
<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>
<script src="../dist/renderer-queue.js"></script>
<script src="../dist/renderer-updates.js"></script>
<script src="../dist/renderer.js"></script>
</body>
</html>