Files
Twitch-VOD-Manager/src/index.html
T
Sucukdeluxe b3c2a8b9fd release: publish Twitch VOD Manager 1.0.3
Polish navigation, settings, streamer and queue workflows; add safe pause and partial-file lifecycle handling; apply the product identity across Windows surfaces; and replace the public README with a complete English product guide and isolated screenshot.
2026-08-10 19:25:08 +02:00

970 lines
79 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">
<link rel="stylesheet" href="./workspace.css">
</head>
<body class="theme-twitch">
<div class="modal-overlay" id="updateModal" role="dialog" aria-modal="true" aria-labelledby="updateModalTitle" onclick="handleUpdateModalOverlayClick(event)">
<div class="modal update-modal">
<button type="button" class="modal-close modal-close-localizable" aria-label="Close" onclick="dismissUpdateModal()">x</button>
<div class="update-modal-eyebrow" id="updateModalEyebrow">Updates</div>
<h2 id="updateModalTitle">Update verfügbar</h2>
<p class="update-modal-message" id="updateModalMessage">Version 0.0.0 ist verfügbar. Jetzt herunterladen?</p>
<div class="update-modal-meta is-hidden" id="updateModalMeta"></div>
<div class="update-changelog-card is-hidden" id="updateChangelogCard">
<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 verfügbar.</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 überspringen</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" role="dialog" aria-modal="true" aria-labelledby="clipDialogTitle">
<div class="modal clip-modal">
<button type="button" class="modal-close modal-close-localizable" aria-label="Close" onclick="closeClipDialog()">x</button>
<h2 class="clip-modal-title" id="clipDialogTitle">VOD zuschneiden</h2>
<div class="clip-modal-field">
<label class="clip-modal-label" id="clipDialogStartLabel" for="clipStartSlider">Start:</label>
<input type="range" id="clipStartSlider" min="0" max="100" value="0" oninput="updateFromSlider('start')">
<div class="clip-modal-time-row">
<label class="clip-modal-meta" id="clipDialogStartTimeLabel" for="clipStartTime">Startzeit (HH:MM:SS):</label>
<input type="text" id="clipStartTime" value="00:00:00" class="clip-modal-time-input" onchange="updateFromInput('start')">
</div>
</div>
<div class="clip-modal-field">
<label class="clip-modal-label" id="clipDialogEndLabel" for="clipEndSlider">Ende:</label>
<input type="range" id="clipEndSlider" min="0" max="100" value="60" oninput="updateFromSlider('end')">
<div class="clip-modal-time-row">
<label class="clip-modal-meta" id="clipDialogEndTimeLabel" for="clipEndTime">Endzeit (HH:MM:SS):</label>
<input type="text" id="clipEndTime" value="00:01:00" class="clip-modal-time-input" onchange="updateFromInput('end')">
</div>
</div>
<div class="clip-modal-duration">
<span id="clipDialogDurationLabel" class="clip-modal-meta">Dauer: </span>
<span id="clipDurationDisplay" class="clip-modal-duration-value">00:01:00</span>
</div>
<div class="clip-modal-field">
<label class="clip-modal-label" id="clipDialogPartLabel" for="clipStartPart">Start Part-Nummer (optional, für Fortsetzung):</label>
<input type="text" id="clipStartPart" placeholder="z.B. 42" class="clip-modal-part-input" oninput="updateFilenameExamples()">
<div id="clipDialogPartHint" class="clip-modal-hint">Leer lassen = Teil 1</div>
</div>
<div class="clip-modal-field">
<label class="clip-modal-label" id="clipDialogFormatLabel">Dateinamen-Format:</label>
<label class="clip-radio-row">
<input type="radio" name="filenameFormat" value="simple" checked onchange="updateFilenameExamples()">
<span id="formatSimple" class="clip-radio-label">01.02.2026_1.mp4 (Standard)</span>
</label>
<label class="clip-radio-row">
<input type="radio" name="filenameFormat" value="timestamp" onchange="updateFilenameExamples()">
<span id="formatTimestamp" class="clip-radio-label">01.02.2026_CLIP_00-00-00_1.mp4 (mit Zeitstempel)</span>
</label>
<label class="clip-radio-row">
<input type="radio" name="filenameFormat" value="parts" onchange="updateFilenameExamples()">
<span id="formatParts" class="clip-radio-label">01.02.2026_Part01.mp4 (Parts-Format)</span>
</label>
<label class="clip-radio-row">
<input type="radio" name="filenameFormat" value="template" onchange="updateFilenameExamples()">
<span id="formatTemplate" class="clip-radio-label">{date}_{part}.mp4 (benutzerdefiniert)</span>
</label>
<div id="clipFilenameTemplateWrap" class="clip-template-wrap">
<input type="text" id="clipFilenameTemplate" value="{date}_{part}.mp4" placeholder="{date}_{part}.mp4" class="clip-modal-template-input" oninput="updateFilenameExamples()">
<div id="clipTemplateHelp" class="clip-modal-hint">Platzhalter: {title} {id} {channel} {date} {part} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}</div>
<div id="clipTemplateLint" class="template-lint ok">Template-Check: OK</div>
<button type="button" class="btn-secondary" id="clipTemplateGuideBtn" onclick="openTemplateGuide('clip')">Template Guide</button>
</div>
</div>
<div class="clip-modal-actions">
<button type="button" class="btn-pill success" id="clipDialogConfirmBtn" style="padding: 12px 30px;" onclick="confirmClipDialog()">Zur Queue hinzufügen</button>
</div>
</div>
</div>
<!-- Events Viewer Modal -->
<div class="modal-overlay" id="eventsViewerModal" role="dialog" aria-modal="true" aria-labelledby="eventsViewerTitle">
<div class="modal viewer-modal viewer-modal-events">
<button type="button" class="modal-close modal-close-localizable" aria-label="Close" onclick="closeEventsViewer()">x</button>
<h2 id="eventsViewerTitle" class="viewer-modal-title"></h2>
<div id="eventsViewerStatus" class="viewer-modal-status" role="status" aria-live="polite"></div>
<div id="eventsViewerList" class="viewer-modal-list"></div>
</div>
</div>
<!-- Chat Replay Viewer Modal -->
<div class="modal-overlay" id="chatViewerModal" role="dialog" aria-modal="true" aria-labelledby="chatViewerTitle">
<div class="modal viewer-modal viewer-modal-chat">
<button type="button" class="modal-close modal-close-localizable" aria-label="Close" onclick="closeChatViewer()">x</button>
<h2 id="chatViewerTitle" class="viewer-modal-title"></h2>
<div class="viewer-modal-filter-row">
<input type="text" id="chatViewerFilter" class="viewer-modal-filter-input" placeholder="Filter..." oninput="onChatViewerFilterChange()">
<span id="chatViewerStatus" class="viewer-modal-status viewer-modal-status-inline" role="status" aria-live="polite"></span>
</div>
<div id="chatViewerList" class="viewer-modal-list viewer-modal-list-chat"></div>
</div>
</div>
<!-- Template Guide Modal -->
<div class="modal-overlay" id="templateGuideModal" role="dialog" aria-modal="true" aria-labelledby="templateGuideTitle">
<div class="modal template-guide-modal">
<button type="button" class="modal-close modal-close-localizable" aria-label="Close" onclick="closeTemplateGuide()">x</button>
<h2 id="templateGuideTitle">Template Guide</h2>
<p id="templateGuideIntro" class="template-guide-intro">Nutze Variablen für Dateinamen und prüfe das Ergebnis als Live-Vorschau.</p>
<div class="template-guide-actions">
<button type="button" class="btn-secondary" id="templateGuideUseVod" onclick="setTemplateGuidePreset('vod')">VOD Template</button>
<button type="button" class="btn-secondary" id="templateGuideUseParts" onclick="setTemplateGuidePreset('parts')">VOD Part Template</button>
<button type="button" class="btn-secondary" id="templateGuideUseClip" onclick="setTemplateGuidePreset('clip')">Clip Template</button>
</div>
<label id="templateGuideTemplateLabel" for="templateGuideInput" 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">Verfügbare Variablen</h3>
<div class="template-guide-table-wrap">
<table class="template-guide-table" aria-labelledby="templateGuideVarsTitle">
<thead>
<tr>
<th id="templateGuideVarCol" scope="col">Variable</th>
<th id="templateGuideDescCol" scope="col">Beschreibung</th>
<th id="templateGuideExampleCol" scope="col">Beispiel</th>
</tr>
</thead>
<tbody id="templateGuideBody"></tbody>
</table>
</div>
<div class="template-guide-footer">
<button type="button" class="btn-secondary" id="templateGuideCloseBtn" onclick="closeTemplateGuide()">Schließen</button>
</div>
</div>
</div>
<header class="app-topbar">
<div class="topbar-navigation-cluster">
<div class="topbar-brand">
<img class="topbar-brand-mark" src="../build/icon.png" alt="" aria-hidden="true">
<span id="logoText">Twitch VOD Manager</span>
</div>
<nav class="nav top-nav" aria-label="Main navigation">
<button class="nav-item top-nav-item active" type="button" aria-current="page" data-tab="vods" onclick="showTab('vods')" title="Twitch VODs">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m10 9 5 3-5 3z"></path></svg>
<span class="top-nav-label" id="navVodsText">Twitch VODs</span>
</button>
<button class="nav-item top-nav-item" type="button" data-tab="clips" onclick="showTab('clips')" title="Twitch Clips">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h11a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4z"></path><path d="m17 10 4-2v8l-4-2"></path></svg>
<span class="top-nav-label" id="navClipsText">Twitch Clips</span>
</button>
<button class="nav-item top-nav-item" type="button" data-tab="cutter" onclick="showTab('cutter')" title="Video schneiden">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="7" r="3"></circle><circle cx="6" cy="17" r="3"></circle><path d="m8.6 8.5 11 7M8.6 15.5 20 8"></path></svg>
<span class="top-nav-label" id="navCutterText">Video schneiden</span>
</button>
<button class="nav-item top-nav-item" type="button" data-tab="merge" onclick="showTab('merge')" title="Videos zusammenfügen">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5h4a3 3 0 0 1 3 3v8a3 3 0 0 0 3 3h4"></path><path d="m16 16 3 3-3 3M5 19h4a3 3 0 0 0 3-3V8a3 3 0 0 1 3-3h4"></path><path d="m16 2 3 3-3 3"></path></svg>
<span class="top-nav-label" id="navMergeText">Videos zusammenfügen</span>
</button>
<button class="nav-item top-nav-item" type="button" data-tab="stats" onclick="showTab('stats')" title="Statistik">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V9M10 19V5M16 19v-7M22 19V3"></path></svg>
<span class="top-nav-label" id="navStatsText">Statistik</span>
</button>
<button class="nav-item top-nav-item" type="button" data-tab="archive" onclick="showTab('archive')" title="Archiv">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16v13H4zM3 3h18v4H3z"></path><path d="M9 11h6"></path></svg>
<span class="top-nav-label" id="navArchiveText">Archiv</span>
</button>
<button class="nav-item top-nav-item" type="button" data-tab="settings" onclick="showTab('settings')" title="Einstellungen">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.82 2.82-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21H9.6v-.1a1.7 1.7 0 0 0-1.08-1.55 1.7 1.7 0 0 0-1.88.35l-.06.06-2.82-2.82.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H3V9.6h.1a1.7 1.7 0 0 0 1.55-1.08 1.7 1.7 0 0 0-.35-1.88l-.06-.06 2.82-2.82.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.1V3h4v.1A1.7 1.7 0 0 0 15.48 4.65a1.7 1.7 0 0 0 1.88-.35l.06-.06 2.82 2.82-.06.06A1.7 1.7 0 0 0 19.4 9c.16.38.36.72.6 1 .3.3.68.5 1.1.6h.1v4h-.1a1.7 1.7 0 0 0-1.7.4z"></path></svg>
<span class="top-nav-label" id="navSettingsText">Einstellungen</span>
</button>
</nav>
</div>
<div class="topbar-actions">
<div class="update-banner workspace-update" id="updateBanner" data-update-state="idle" hidden>
<button type="button" class="workspace-update-button" id="workspaceUpdateButton" onclick="handleWorkspaceUpdateAction()" aria-describedby="updateText" aria-controls="workspaceUpdatePopover" aria-expanded="false">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"></path><path d="m8 11 4 4 4-4"></path><path d="M4 19h16"></path></svg>
<span id="workspaceUpdateLabel">Update</span>
</button>
<div class="workspace-update-popover" id="workspaceUpdatePopover">
<div class="workspace-update-popover-header">
<span id="updateText" role="status" aria-live="polite">Nach Updates suchen.</span>
<button type="button" id="workspaceUpdateDismiss" onclick="dismissWorkspaceUpdatePopover()" aria-label="Update notification dismiss">
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="m6 6 12 12M18 6 6 18"></path></svg>
<span class="sr-only" id="workspaceUpdateDismissText">Close</span>
</button>
</div>
<div id="updateProgress" class="update-banner-progress-wrap is-hidden">
<div class="update-banner-progress-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Update download" id="updateProgressGauge">
<div id="updateProgressBar" class="update-banner-progress-bar"></div>
</div>
</div>
<div class="workspace-update-popover-actions">
<button type="button" id="updateButton" onclick="downloadUpdate()">Jetzt herunterladen</button>
<button type="button" id="workspaceUpdateLater" onclick="postponeWorkspaceUpdatePopover()">Spater</button>
</div>
</div>
</div>
</div>
</header>
<div class="app workspace-shell">
<aside class="sidebar context-sidebar" aria-label="Workspace context">
<section class="context-panel" data-context-for="vods" data-vods-workspace="streamers" data-vods-layout="split">
<div class="context-switcher" role="group" aria-label="VOD workspace">
<button type="button" id="streamerWorkspaceSwitch" class="active" aria-pressed="true" onclick="setVodsWorkspace('streamers', this)">Streamer</button>
<button type="button" id="queueWorkspaceSwitch" aria-pressed="false" onclick="setVodsWorkspace('queue', this)">Warteschlange</button>
</div>
<div class="streamer-section">
<div class="section-title" id="streamerSectionTitle">
<span class="section-title-label">
<span id="streamerSectionTitleText">Streamer</span>
</span>
<span id="streamerSectionCounter" class="streamer-section-counter"></span>
<button id="btnStreamerBulkRemove" class="btn-close is-hidden" type="button" onclick="bulkRemoveStreamers()" title="Bulk remove">x</button>
</div>
<input type="text" id="streamerListFilter" class="filter-input compact is-hidden" placeholder="Filter..." oninput="onStreamerListFilterChange()">
<div class="streamers" id="streamerList"></div>
</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 type="button" class="btn btn-start" id="btnStart" onclick="toggleDownload()" disabled>Start</button>
<button type="button" class="btn btn-merge-group is-hidden" id="btnMergeGroup" onclick="createMergeGroupFromSelection()">Merge &amp; Split</button>
<button type="button" class="btn btn-retry" id="btnRetryFailed" onclick="retryFailedDownloads()" title="Nur fehlgeschlagene Downloads erneut starten">Wiederholen</button>
<button type="button" class="btn btn-clear" id="btnClear" onclick="clearCompleted()" disabled>Leeren</button>
</div>
</div>
<div class="stats-bar" id="statsBar"></div>
</section>
<section class="context-panel" data-context-for="clips" hidden>
<div class="context-panel-heading" data-context-heading>Clips</div>
<nav class="context-list" aria-label="Clip sections">
<button type="button" class="context-link active" onclick="focusWorkspaceTarget('clipUrl', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 6h11a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4zM17 10l4-2v8l-4-2"></path></svg><span data-label-source="clipsHeading">Clip-Download</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('clipsInfoTitle', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><path d="M12 11v6M12 7h.01"></path></svg><span data-label-source="clipsInfoTitle">Info</span></button>
</nav>
</section>
<section class="context-panel" data-context-for="cutter" hidden>
<div class="context-panel-heading" data-context-heading>Video schneiden</div>
<nav class="context-list" aria-label="Cutter sections">
<button type="button" class="context-link active" onclick="focusWorkspaceTarget('cutterBrowseBtn', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h7l2 2h9v11H3z"></path></svg><span data-label-source="cutterSelectTitle">Video auswählen</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('timelineContainer', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12h16M7 8v8M17 8v8"></path></svg><span data-label-source="cutterInfoSelectionLabel">Auswahl</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('btnCut', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="6" cy="7" r="3"></circle><circle cx="6" cy="17" r="3"></circle><path d="m8.5 8.5 11 7M8.5 15.5 20 8"></path></svg><span data-label-source="btnCut">Schneiden</span></button>
</nav>
</section>
<section class="context-panel" data-context-for="merge" hidden>
<div class="context-panel-heading" data-context-heading>Zusammenfügen</div>
<nav class="context-list" aria-label="Merge sections">
<button type="button" class="context-link active" onclick="focusWorkspaceTarget('mergeAddBtn', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 6h16v12H4zM12 9v6M9 12h6"></path></svg><span data-label-source="mergeAddBtn">Videos hinzufügen</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('mergeFileList', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5 6h14M5 12h14M5 18h14"></path></svg><span data-label-source="mergeTitle">Videos zusammenfügen</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('btnMerge', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5 5h4a3 3 0 0 1 3 3v8a3 3 0 0 0 3 3h4M16 16l3 3-3 3"></path></svg><span data-label-source="btnMerge">Zusammenfügen</span></button>
</nav>
</section>
<section class="context-panel" data-context-for="stats" hidden>
<div class="context-panel-heading" data-context-heading>Statistik</div>
<nav class="context-list" aria-label="Statistics sections">
<button type="button" class="context-link active" onclick="focusWorkspaceTarget('statsSummaryTitle', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 19V9M10 19V5M16 19v-7M22 19V3"></path></svg><span data-label-source="statsSummaryTitle">Übersicht</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('statsTopStreamersTitle', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 7h16M4 12h12M4 17h8"></path></svg><span data-label-source="statsTopStreamersTitle">Top Streamer</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('statsActivityTitle', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="m3 17 5-5 4 3 7-8 2 2"></path></svg><span data-label-source="statsActivityTitle">Aktivität</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('statsSizeBucketsTitle', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><path d="M12 3v9h9"></path></svg><span data-label-source="statsSizeBucketsTitle">Größen-Verteilung</span></button>
</nav>
</section>
<section class="context-panel" data-context-for="archive" hidden>
<div class="context-panel-heading" data-context-heading>Archiv</div>
<nav class="context-list" aria-label="Archive sections">
<button type="button" class="context-link active" onclick="focusWorkspaceTarget('archiveSearchQuery', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="10" cy="10" r="6"></circle><path d="m15 15 5 5"></path></svg><span data-label-source="archiveTitle">Archiv durchsuchen</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('archiveSearchType', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 5h16M7 12h10M10 19h4"></path></svg><span data-label-source="archiveSearchType">Filter</span></button>
<button type="button" class="context-link" onclick="focusWorkspaceTarget('archiveSearchResults', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 4h16v16H4zM8 8h8M8 12h8M8 16h5"></path></svg><span id="archiveResultsNavText">Ergebnisse</span></button>
</nav>
</section>
<section class="context-panel" data-context-for="settings" hidden>
<div class="context-panel-heading" data-context-heading>Einstellungen</div>
<nav class="context-list" aria-label="Settings sections">
<button type="button" class="context-link active" data-settings-pane="design" onclick="setSettingsPane('design', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1"></path></svg><span data-label-source="designTitle">Design</span></button>
<button type="button" class="context-link" data-settings-pane="api" onclick="setSettingsPane('api', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M8 12h8M12 8v8"></path><circle cx="12" cy="12" r="9"></circle></svg><span data-label-source="apiTitle">Twitch API</span></button>
<button type="button" class="context-link" data-settings-pane="downloads" onclick="setSettingsPane('downloads', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3v12M8 11l4 4 4-4M4 20h16"></path></svg><span data-label-source="downloadSettingsTitle">Downloads</span></button>
<button type="button" class="context-link" data-settings-pane="automation" onclick="setSettingsPane('automation', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12a8 8 0 1 0 2.3-5.7M4 4v6h6"></path></svg><span data-label-source="autoVodCardTitle">Automatisierung</span></button>
<button type="button" class="context-link" data-settings-pane="notifications" onclick="setSettingsPane('notifications', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"></path></svg><span data-label-source="discordCardTitle">Benachrichtigungen</span></button>
<button type="button" class="context-link" data-settings-pane="storage" onclick="setSettingsPane('storage', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><ellipse cx="12" cy="6" rx="8" ry="3"></ellipse><path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"></path></svg><span data-label-source="storageCardTitle">Storage</span></button>
<button type="button" class="context-link" data-settings-pane="maintenance" onclick="setSettingsPane('maintenance', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 7h16v13H4zM8 7V4h8v3M9 12h6"></path></svg><span data-label-source="backupCardTitle">Sicherung &amp; Wartung</span></button>
<button type="button" class="context-link" data-settings-pane="updates" onclick="setSettingsPane('updates', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3v12M8 11l4 4 4-4M4 19h16"></path></svg><span data-label-source="updateTitle">Updates</span></button>
<button type="button" class="context-link" data-settings-pane="system" onclick="setSettingsPane('system', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3 4 7v5c0 5 3.4 8 8 9 4.6-1 8-4 8-9V7z"></path><path d="m9 12 2 2 4-4"></path></svg><span data-label-source="preflightTitle">System-Check</span></button>
<button type="button" class="context-link" data-settings-pane="debug" onclick="setSettingsPane('debug', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5 5h14v14H5zM8 9l2 2-2 2M12 15h4"></path></svg><span data-label-source="debugLogTitle">Debug-Log</span></button>
<button type="button" class="context-link" data-settings-pane="metrics" onclick="setSettingsPane('metrics', this)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 19V9M10 19V5M16 19v-7M22 19V3"></path></svg><span data-label-source="runtimeMetricsTitle">Runtime Metrics</span></button>
</nav>
</section>
</aside>
<main class="main workspace-main">
<header class="header workspace-toolbar">
<div class="toolbar-context" data-toolbar-for="vods">
<button type="button" class="toolbar-icon-button" id="toolbarRefreshVodsBtn" onclick="refreshVODs()" aria-label="Refresh VODs" title="Refresh VODs"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M20 6v5h-5M4 18v-5h5M18.5 9A7 7 0 0 0 6 7M5.5 15A7 7 0 0 0 18 17"></path></svg></button>
<button type="button" class="toolbar-primary" onclick="focusWorkspaceTarget('newStreamer')"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"></path></svg><span>Streamer</span></button>
</div>
<div class="toolbar-context" data-toolbar-for="clips" hidden>
<button type="button" class="toolbar-primary" onclick="focusWorkspaceTarget('clipUrl')"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"></path></svg><span data-label-source="clipsHeading">Clip</span></button>
<button type="button" class="toolbar-icon-button" id="toolbarClipDownloadBtn" onclick="downloadClip()" aria-label="Download clip" title="Download clip"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3v12M8 11l4 4 4-4M4 20h16"></path></svg></button>
</div>
<div class="toolbar-context" data-toolbar-for="cutter" hidden>
<button type="button" class="toolbar-primary" onclick="selectCutterVideo()"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h7l2 2h9v11H3z"></path></svg><span data-label-source="cutterBrowseBtn">Durchsuchen</span></button>
<button type="button" class="toolbar-icon-button" id="toolbarCutBtn" onclick="startCutting()" aria-label="Cut video" title="Cut video"><svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="6" cy="7" r="3"></circle><circle cx="6" cy="17" r="3"></circle><path d="m8.5 8.5 11 7M8.5 15.5 20 8"></path></svg></button>
</div>
<div class="toolbar-context" data-toolbar-for="merge" hidden>
<button type="button" class="toolbar-primary" onclick="addMergeFiles()"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"></path></svg><span data-label-source="mergeAddBtn">Videos hinzufügen</span></button>
<button type="button" class="toolbar-icon-button" id="toolbarMergeBtn" onclick="startMerging()" aria-label="Merge videos" title="Merge videos"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5 5h4a3 3 0 0 1 3 3v8a3 3 0 0 0 3 3h4M16 16l3 3-3 3"></path></svg></button>
</div>
<div class="toolbar-context" data-toolbar-for="stats" hidden>
<button type="button" class="toolbar-primary" id="toolbarStatsRefreshBtn" onclick="refreshArchiveStats()"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M20 6v5h-5M4 18v-5h5M18.5 9A7 7 0 0 0 6 7M5.5 15A7 7 0 0 0 18 17"></path></svg><span data-label-source="btnStatsRefresh">Aktualisieren</span></button>
</div>
<div class="toolbar-context" data-toolbar-for="archive" hidden>
<button type="button" class="toolbar-primary" onclick="performArchiveSearch()"><svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="10" cy="10" r="6"></circle><path d="m15 15 5 5"></path></svg><span data-label-source="btnArchiveSearch">Suchen</span></button>
</div>
<div class="toolbar-context" data-toolbar-for="settings" hidden>
<label class="workspace-settings-search" for="settingsSearchInput">
<svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="10" cy="10" r="6"></circle><path d="m15 15 5 5"></path></svg>
<input type="search" id="settingsSearchInput" placeholder="Search settings…" oninput="filterSettings(this.value)" autocomplete="off">
</label>
<button type="button" class="toolbar-icon-button" id="toolbarCheckUpdateBtn" onclick="checkUpdate()" aria-label="Check for updates" title="Check for updates"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M20 6v5h-5M4 18v-5h5M18.5 9A7 7 0 0 0 6 7M5.5 15A7 7 0 0 0 18 17"></path></svg></button>
</div>
<h1 class="workspace-title" id="pageTitle">VODs</h1>
<div class="header-actions workspace-toolbar-actions" data-toolbar-search-for="vods">
<div class="header-search workspace-search">
<input type="text" id="newStreamer" placeholder="Streamer hinzufügen…" onkeypress="if(event.key==='Enter')addStreamer()">
<button id="btnAddStreamer" type="button" onclick="addStreamer()" aria-label="Add streamer" title="Add streamer">+</button>
</div>
<span id="refreshText" class="sr-only">Aktualisieren</span>
</div>
</header>
<div class="content">
<!-- VODs Tab -->
<div class="tab-content active" id="vodsTab">
<div id="streamerProfileHeader" class="streamer-profile-header is-hidden"></div>
<div class="vod-filter-row">
<input type="text" id="vodFilterInput" class="filter-input" placeholder="Filter VODs..." oninput="onVodFilterInput()">
<button type="button" id="vodFilterClearBtn" class="btn-close is-hidden" onclick="clearVodFilter()" title="Clear filter">x</button>
<span id="vodFilterCount" class="form-sublabel vod-filter-count"></span>
<label id="vodHideDownloadedLabel" class="inline-toggle vod-hide-downloaded" title="">
<input type="checkbox" id="vodHideDownloadedToggle" onchange="onVodHideDownloadedChange()">
<span id="vodHideDownloadedText">Hide downloaded</span>
</label>
<label id="vodSortLabel" for="vodSortSelect" class="form-sublabel vod-sort-label">Sort:</label>
<select id="vodSortSelect" class="select-compact" onchange="onVodSortChange()">
<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>
</div>
<div id="vodBulkBar" class="vod-bulk-bar is-hidden">
<span id="vodBulkCount" class="vod-bulk-count">0 selected</span>
<span class="vod-bulk-spacer"></span>
<button id="vodBulkAddBtn" class="btn-pill primary" type="button" onclick="bulkAddSelectedVodsToQueue()">+ Queue</button>
<button id="vodBulkMarkBtn" class="btn-pill" type="button" onclick="bulkMarkSelectedDownloaded(true)">Mark as downloaded</button>
<button id="vodBulkUnmarkBtn" class="btn-pill" type="button" onclick="bulkMarkSelectedDownloaded(false)">Unmark</button>
<button id="vodBulkClearBtn" class="btn-pill" type="button" onclick="clearVodSelection()">Clear</button>
</div>
<div class="vod-grid" id="vodGrid">
<div class="empty-state">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2h8l4 4v16H6z"></path><path d="M14 2v5h5M10 11l5 3-5 3z"></path></svg>
<h3 id="vodGridEmptyTitle">Keine VODs</h3>
<p id="vodGridEmptyText">Wähle einen Streamer aus der Liste oder füge 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 type="button" class="btn-primary" onclick="downloadClip()" id="btnClip">Clip herunterladen</button>
<div class="clip-status" id="clipStatus" role="status" aria-live="polite"></div>
</div>
<div class="settings-card centered">
<h3 id="clipsInfoTitle">Info</h3>
<p id="clipsInfoText" class="info-text">
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 auswählen</h3>
<div class="form-row">
<input type="text" id="cutterFilePath" readonly placeholder="Keine Datei ausgewahlt...">
<button type="button" class="btn-secondary" id="cutterBrowseBtn" onclick="selectCutterVideo()">Durchsuchen</button>
</div>
</div>
<div class="video-preview" id="cutterPreview">
<div class="placeholder">
<svg aria-hidden="true" width="64" height="64" 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>
<p id="cutterPreviewPlaceholder">Video auswählen um Vorschau zu sehen</p>
</div>
</div>
<div class="cutter-info" id="cutterInfo">
<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">Auflösung</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">
<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" for="startTime">Start:</label>
<input type="text" id="startTime" value="00:00:00" onchange="updateTimeFromInput()">
</div>
<div class="time-input-group">
<label id="cutterEndLabel" for="endTime">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" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Cut progress" id="cutProgressGauge">
<div class="progress-bar-fill" id="cutProgressBar"></div>
</div>
<div class="progress-text" id="cutProgressText">0%</div>
</div>
<div class="cutter-actions">
<button type="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 zusammenfügen</h3>
<p id="mergeDesc" class="card-intro">
Wähle mehrere Videos aus um sie zu einem Video zusammenzufügen.
Die Reihenfolge kann per Drag & Drop geändert werden.
</p>
<button type="button" class="btn-secondary" id="mergeAddBtn" onclick="addMergeFiles()">+ Videos hinzufügen</button>
</div>
<div class="file-list" id="mergeFileList">
<div class="empty-state merge-empty-state">
<svg aria-hidden="true" width="48" height="48" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<p id="mergeEmptyText">Keine Videos ausgewahlt</p>
</div>
</div>
<div class="progress-container" id="mergeProgress">
<div class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Merge progress" id="mergeProgressGauge">
<div class="progress-bar-fill" id="mergeProgressBar"></div>
</div>
<div class="progress-text" id="mergeProgressText">0%</div>
</div>
<div class="merge-actions">
<button type="button" class="btn-primary" id="btnMerge" onclick="startMerging()" disabled>Zusammenfügen</button>
</div>
</div>
</div>
<!-- Statistics Tab -->
<div class="tab-content" id="statsTab">
<div class="settings-card">
<div class="form-row section-header">
<h3 id="statsTitle">Archiv-Statistik</h3>
<div class="section-header-actions">
<span id="statsLastScannedLabel" class="form-sublabel" role="status" aria-live="polite"></span>
<button type="button" class="btn-secondary" id="btnStatsRefresh" onclick="refreshArchiveStats()">Aktualisieren</button>
</div>
</div>
<p id="statsIntro" class="card-intro flush">Aggregiert über den Download-Ordner. Live-Aufnahmen liegen unter <code>{streamer}/live/</code>, VOD-Downloads direkt unter <code>{streamer}/</code>. Lade-Zeit skaliert mit der Anzahl Dateien.</p>
</div>
<div class="settings-card">
<h3 id="statsSummaryTitle">Übersicht</h3>
<div id="statsSummaryGrid" class="stats-summary-grid"></div>
</div>
<div class="settings-card">
<h3 id="statsTopStreamersTitle">Top Streamer (nach Größe)</h3>
<div id="statsTopStreamers"></div>
</div>
<div class="settings-card">
<h3 id="statsActivityTitle">Aktivität (letzte 30 Tage)</h3>
<div id="statsActivity"></div>
</div>
<div class="settings-card">
<h3 id="statsSizeBucketsTitle">Aufnahme-Größen-Verteilung</h3>
<div id="statsSizeBuckets"></div>
</div>
</div>
<!-- Archive Search Tab -->
<div class="tab-content" id="archiveTab">
<div class="settings-card">
<h3 id="archiveTitle">Archiv durchsuchen</h3>
<p id="archiveIntro" class="card-intro">Suche nach Dateinamen, Streamern oder Datumsangaben. Treffer zeigen Aufnahmen (Live + VOD); zugehörige Chat- und Event-Dateien werden als zusätzliche Schaltflächen angeboten.</p>
<div class="form-row search-bar">
<input type="text" id="archiveSearchQuery" class="filter-input flex-1-1-240" placeholder="Suche...">
<select id="archiveSearchType" class="select-compact">
<option value="all">Alle Typen</option>
<option value="live">Live-Aufnahmen</option>
<option value="vod">VOD-Downloads</option>
</select>
<select id="archiveSearchStreamer" class="select-compact size-md">
<option value="">Alle Streamer</option>
</select>
<select id="archiveSearchSort" class="select-compact">
<option value="date_desc">Neueste zuerst</option>
<option value="date_asc">Älteste zuerst</option>
<option value="size_desc">Größte zuerst</option>
<option value="size_asc">Kleinste zuerst</option>
<option value="name_asc">Name (A-Z)</option>
</select>
<button type="button" class="btn-secondary" id="btnArchiveSearch" onclick="performArchiveSearch()">Suchen</button>
</div>
<div id="archiveSearchSummary" class="form-sublabel" role="status" aria-live="polite"></div>
</div>
<div class="settings-card">
<div id="archiveSearchResults"></div>
</div>
</div>
<!-- Settings Tab -->
<div class="tab-content" id="settingsTab" data-settings-pane="design">
<div class="settings-card" data-settings-pane="design">
<h3 id="designTitle">Design</h3>
<div class="form-group">
<label id="themeLabel" for="themeSelect">Theme</label>
<div class="workspace-theme-picker" id="workspaceThemePicker" role="group" aria-labelledby="themeLabel">
<button type="button" class="workspace-theme-choice" data-theme="light" aria-pressed="false" onclick="selectWorkspaceTheme('light')">
<span class="workspace-theme-preview theme-preview-light" aria-hidden="true"><span></span><span></span><span></span></span>
<span data-label-source="themeLightOption">Light</span>
</button>
<button type="button" class="workspace-theme-choice active" data-theme="twitch" aria-pressed="true" onclick="selectWorkspaceTheme('twitch')">
<span class="workspace-theme-preview theme-preview-dark" aria-hidden="true"><span></span><span></span><span></span></span>
<span id="themeDarkOption">Dark</span>
</button>
<button type="button" class="workspace-theme-choice" data-theme="system" aria-pressed="false" onclick="selectWorkspaceTheme('system')">
<span class="workspace-theme-preview theme-preview-system" aria-hidden="true"><span></span><span></span><span></span></span>
<span id="themeSystemOption">System</span>
</button>
</div>
<select id="themeSelect" class="theme-select-fallback" 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>
<option value="system">System</option>
</select>
</div>
<div class="form-group">
<label id="languageLabel">Sprache</label>
<div class="language-picker" id="languagePicker" role="group" aria-labelledby="languageLabel">
<button type="button" class="lang-option" id="langOptionEn" onclick="selectLanguageOption('en')" aria-pressed="false">
<svg class="flag-icon flag-en" viewBox="0 0 18 12" aria-hidden="true" focusable="false" shape-rendering="crispEdges">
<rect width="18" height="12" fill="#b31942"></rect>
<path d="M0 2h18v2H0zm0 4h18v2H0zm0 4h18v2H0z" fill="#fff"></path>
<path d="M0 0h8v6H0z" fill="#0a3161"></path>
<path d="M1 1h1v1H1zm3 0h1v1H4zm3 0h1v1H7zM2 3h1v1H2zm3 0h1v1H5zM1 5h1v1H1zm3 0h1v1H4zm3 0h1v1H7z" fill="#fff"></path>
</svg>
<span id="languageEnText">English</span>
</button>
<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>
</div>
<select id="languageSelect" onchange="changeLanguage(this.value)" style="display:none">
<option value="de">de</option>
<option value="en">en</option>
</select>
</div>
<div class="form-group sidebar-layout-setting">
<label class="toggle-row">
<input type="checkbox" id="sidebarSplitViewToggle" checked onchange="applySidebarLayoutPreference(this.checked)">
<span id="sidebarSplitViewLabel">Streamer und Queue gleichzeitig anzeigen</span>
</label>
<p id="sidebarSplitViewHint" class="form-note">Teilt die linke Seitenleiste gleichmäßig auf. Deaktiviert erscheinen wieder die Umschaltflächen.</p>
</div>
</div>
<div class="settings-card" data-settings-pane="api" hidden>
<h3 id="apiTitle">Twitch API</h3>
<p id="apiHelpText" class="card-intro">
<span id="apiHelpIntro">Du brauchst eine Client-ID und ein Client-Secret von Twitch.</span>
<a href="#" id="apiHelpLink" onclick="event.preventDefault(); openTwitchDevConsole()">dev.twitch.tv/console/apps</a>
</p>
<div class="form-group">
<label id="clientIdLabel" for="clientId">Client ID</label>
<input type="text" id="clientId" placeholder="Twitch Client ID">
</div>
<div class="form-group">
<label id="clientSecretLabel" for="clientSecret">Client Secret</label>
<input type="password" id="clientSecret" placeholder="Twitch Client Secret">
</div>
<button type="button" class="btn-primary" id="saveSettingsBtn" onclick="saveSettings()">Speichern & Verbinden</button>
</div>
<div class="settings-card" data-settings-pane="downloads" hidden>
<h3 id="downloadSettingsTitle">Download-Einstellungen</h3>
<div class="form-group download-settings-storage">
<label id="storageLabel" for="downloadPath">Speicherort</label>
<div class="form-row">
<input type="text" id="downloadPath" readonly>
<button type="button" class="btn-secondary" id="selectFolderBtn" onclick="selectFolder()">Ordner</button>
<button type="button" class="btn-secondary" id="openFolderBtn" onclick="openFolder()">Offnen</button>
</div>
</div>
<div class="download-settings-layout">
<div class="download-settings-column">
<section class="download-settings-section">
<h4 id="downloadBasicsTitle">Download und Qualität</h4>
<div class="form-group">
<label id="modeLabel" for="downloadMode">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" for="partMinutes">Teil-Lange (Minuten)</label>
<input type="number" id="partMinutes" value="120" min="10" max="480">
</div>
<div class="form-group">
<label id="parallelDownloadsLabel" for="parallelDownloads">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" for="streamlinkQuality">Stream-Qualität</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" for="performanceMode">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>
</section>
<section class="download-settings-section">
<div class="download-settings-section-heading">
<h4 id="filenameTemplatesTitle">Dateinamen-Templates</h4>
<button class="btn-secondary" id="settingsTemplateGuideBtn" type="button" onclick="openTemplateGuide('vod')">Template Guide</button>
</div>
<div class="form-row template-presets">
<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 class="filename-template-grid">
<label id="vodTemplateLabel" for="vodFilenameTemplate">VOD Template</label>
<input type="text" id="vodFilenameTemplate" class="input-monospace" placeholder="{title}.mp4" oninput="validateFilenameTemplates()">
<label id="partsTemplateLabel" for="partsFilenameTemplate">VOD Part Template</label>
<input type="text" id="partsFilenameTemplate" class="input-monospace" placeholder="{date}_Part{part_padded}.mp4" oninput="validateFilenameTemplates()">
<label id="defaultClipTemplateLabel" for="defaultClipFilenameTemplate">Clip Template</label>
<input type="text" id="defaultClipFilenameTemplate" class="input-monospace" placeholder="{date}_{part}.mp4" oninput="validateFilenameTemplates()">
</div>
<div id="filenameTemplateHint" class="form-note">Platzhalter: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}</div>
<div id="filenameTemplateLint" class="template-lint ok">Template-Check: OK</div>
</section>
</div>
<div class="download-settings-column">
<section class="download-settings-section">
<h4 id="queueAutomationTitle">Queue und Automatisierung</h4>
<div class="download-settings-toggles">
<label class="toggle-row"><input type="checkbox" id="smartSchedulerToggle" checked><span id="smartSchedulerLabel">Smart Queue Scheduler aktivieren</span></label>
<label class="toggle-row"><input type="checkbox" id="duplicatePreventionToggle" checked><span id="duplicatePreventionLabel">Duplikate in Queue verhindern</span></label>
<label class="toggle-row"><input type="checkbox" id="persistQueueToggle" checked><span id="persistQueueLabel">Queue zwischen App-Starts speichern</span></label>
<label class="toggle-row"><input type="checkbox" id="autoResumeQueueToggle"><span id="autoResumeQueueLabel">Queue beim Start automatisch fortsetzen</span></label>
<label class="toggle-row"><input type="checkbox" id="notifyEachCompletionToggle"><span id="notifyEachCompletionLabel">Benachrichtigung bei jedem fertigen Download</span></label>
</div>
</section>
<section class="download-settings-section">
<h4 id="recordingMetadataTitle">Aufnahmen und Metadaten</h4>
<div class="download-settings-toggles">
<label class="toggle-row"><input type="checkbox" id="streamlinkDisableAdsToggle" checked><span id="streamlinkDisableAdsLabel">Twitch-Ads beim Download überspringen</span></label>
<label class="toggle-row"><input type="checkbox" id="downloadChatReplayToggle"><span id="downloadChatReplayLabel">Chat-Replay parallel zum VOD speichern (.chat.json)</span></label>
<label class="toggle-row"><input type="checkbox" id="captureLiveChatToggle"><span id="captureLiveChatLabel">Live-Chat während der Aufnahme mitschneiden (.chat.jsonl)</span></label>
<label class="toggle-row"><input type="checkbox" id="logStreamEventsToggle" checked><span id="logStreamEventsLabel">Stream-Events bei Live-Aufnahmen mitloggen (.events.jsonl)</span></label>
<label class="toggle-row"><input type="checkbox" id="autoResumeLiveRecordingToggle" checked><span id="autoResumeLiveRecordingLabel">Live-Aufnahme automatisch fortsetzen wenn Streamlink abbricht (max. 5 Versuche)</span></label>
<label class="toggle-row"><input type="checkbox" id="autoMergeResumedPartsToggle"><span id="autoMergeResumedPartsLabel">Fortgesetzte Aufnahme-Parts automatisch zu einer Datei zusammenfügen (ffmpeg concat)</span></label>
<label class="toggle-row indented"><input type="checkbox" id="deletePartsAfterMergeToggle"><span id="deletePartsAfterMergeLabel">Einzelne Parts nach erfolgreichem Merge löschen</span></label>
</div>
<div class="form-group metadata-cache-setting">
<label id="metadataCacheMinutesLabel" for="metadataCacheMinutes">Metadata-Cache (Minuten)</label>
<input type="number" id="metadataCacheMinutes" value="10" min="1" max="120">
</div>
</section>
</div>
</div>
</div>
<div class="settings-card" data-settings-pane="updates" hidden>
<h3 id="updateTitle">Updates</h3>
<p id="versionInfo" class="card-intro">Version: v1.0.3</p>
<button type="button" class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
</div>
<div class="settings-card" data-settings-pane="system" hidden>
<div class="form-row section-header">
<h3 id="preflightTitle">System-Check</h3>
<span class="health-badge unknown" id="healthBadge">System: Unbekannt</span>
</div>
<div class="form-row" style="margin-bottom: 10px;">
<button type="button" class="btn-secondary" id="btnPreflightRun" onclick="runPreflight(false)">Check ausfuhren</button>
<button type="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" data-settings-pane="debug" hidden>
<h3 id="debugLogTitle">Live Debug-Log</h3>
<div class="form-row aligned">
<button type="button" class="btn-secondary" id="btnRefreshLog" onclick="refreshDebugLog()">Aktualisieren</button>
<button type="button" class="btn-secondary" id="btnOpenDebugLogFile" onclick="openDebugLogFile()">Log-Datei öffnen</button>
<label class="inline-toggle">
<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" data-settings-pane="storage" hidden>
<div class="form-row section-header">
<h3 id="storageCardTitle">Storage</h3>
<button type="button" class="btn-secondary" id="btnRefreshStorage" onclick="refreshStorageStats()">Aktualisieren</button>
</div>
<p id="storageCardIntro" class="card-intro">Disk-Verbrauch pro Streamer im aktuellen Download-Ordner. Live-Aufnahmen werden separat ausgewiesen.</p>
<div id="storageSummary" class="form-sublabel" style="margin-bottom:8px;" role="status" aria-live="polite"></div>
<div id="storageList"></div>
<hr>
<h4 id="cleanupTitle">Auto-Cleanup</h4>
<p id="cleanupIntro" class="card-intro">Aufnahmen älter als X Tage automatisch archivieren oder löschen. Schiebt Sidecar-Chat-Dateien (.chat.json/.chat.jsonl) mit der Aufnahme.</p>
<label class="toggle-row" style="margin-bottom: 8px;">
<input type="checkbox" id="autoCleanupEnabledToggle">
<span id="autoCleanupEnabledLabel">Auto-Cleanup aktivieren</span>
</label>
<div class="form-row" style="gap:12px; flex-wrap:wrap; margin-bottom: 8px;">
<label class="form-stack size-sm">
<span id="autoCleanupDaysLabel" class="form-sublabel">Tage-Schwelle</span>
<input type="number" id="autoCleanupDays" min="1" max="3650" value="30">
</label>
<label class="form-stack size-md">
<span id="autoCleanupTargetLabel" class="form-sublabel">Bereich</span>
<select id="autoCleanupTarget">
<option value="live_only" id="autoCleanupTargetLive">Nur Live-Aufnahmen</option>
<option value="all" id="autoCleanupTargetAll">Alle Aufnahmen</option>
</select>
</label>
<label class="form-stack size-md">
<span id="autoCleanupActionLabel" class="form-sublabel">Aktion</span>
<select id="autoCleanupAction">
<option value="archive" id="autoCleanupActionArchive">In Archiv verschieben</option>
<option value="delete" id="autoCleanupActionDelete">Löschen</option>
</select>
</label>
</div>
<div class="form-row" style="margin-bottom: 8px; gap: 8px;">
<button type="button" class="btn-secondary" id="btnCleanupDryRun" onclick="runCleanupDryRun()">Vorschau</button>
<button type="button" class="btn-secondary" id="btnCleanupRunNow" onclick="runCleanupNow()">Jetzt ausführen</button>
</div>
<div id="cleanupReport" class="form-note" role="status" aria-live="polite"></div>
</div>
<div class="settings-card" data-settings-pane="notifications" hidden>
<h3 id="discordCardTitle">Discord-Webhook</h3>
<p id="discordCardIntro" class="card-intro">Sende Benachrichtigungen an einen Discord-Channel via Webhook — nützlich für Multi-Device-Setups oder eine dedizierte Archiv-Maschine.</p>
<div class="form-group">
<label id="discordWebhookUrlLabel" for="discordWebhookUrl">Webhook-URL</label>
<input type="text" id="discordWebhookUrl" placeholder="https://discord.com/api/webhooks/...">
</div>
<div class="form-group">
<label class="toggle-row">
<input type="checkbox" id="discordNotifyLiveStartToggle">
<span id="discordNotifyLiveStartLabel">Bei Live-Aufnahme-Start benachrichtigen</span>
</label>
<label class="toggle-row">
<input type="checkbox" id="discordNotifyLiveEndToggle">
<span id="discordNotifyLiveEndLabel">Bei Live-Aufnahme-Ende benachrichtigen</span>
</label>
<label class="toggle-row">
<input type="checkbox" id="discordNotifyVodCompleteToggle">
<span id="discordNotifyVodCompleteLabel">Bei abgeschlossenem VOD-Download benachrichtigen</span>
</label>
<label class="toggle-row">
<input type="checkbox" id="discordNotifyVodAutoQueuedToggle">
<span id="discordNotifyVodAutoQueuedLabel">Bei automatisch eingereihten VODs benachrichtigen</span>
</label>
</div>
</div>
<div class="settings-card" data-settings-pane="automation" hidden>
<h3 id="autoVodCardTitle">Auto-VOD-Download</h3>
<p id="autoVodCardIntro" class="card-intro">Streamer mit aktiviertem VOD-Toggle werden in dem hier festgelegten Intervall auf neue Twitch-VODs geprüft. Neue VODs innerhalb des Alters-Fensters werden automatisch zur Download-Queue hinzugefügt.</p>
<div class="form-row aligned">
<label id="autoVodPollMinutesLabel" class="form-sublabel" for="autoVodPollMinutes">Poll-Intervall (Minuten)</label>
<input type="number" id="autoVodPollMinutes" min="5" max="360" value="15" class="input-narrow">
<label id="autoVodMaxAgeHoursLabel" class="form-sublabel" for="autoVodMaxAgeHours" style="margin-left:12px;">Max. Alter (Stunden)</label>
<input type="number" id="autoVodMaxAgeHours" min="1" max="720" value="24" class="input-narrow">
</div>
<div class="form-row" style="align-items: center; gap: 12px; flex-wrap: wrap;">
<button type="button" class="btn-secondary" id="btnAutoVodScanNow" onclick="triggerManualAutoVodScan()">Jetzt scannen</button>
<button type="button" class="btn-secondary" id="btnAutoRecordScanNow" onclick="triggerManualAutoRecordScan()">Live-Status prüfen</button>
<span id="autoVodStatusLine" class="form-sublabel"></span>
</div>
</div>
<div class="settings-card" data-settings-pane="maintenance" hidden>
<h3 id="backupCardTitle">Sicherung &amp; Wartung</h3>
<p id="backupCardIntro" class="card-intro">Konfiguration sichern, auf einem anderen Gerät wiederherstellen, oder die Liste der bereits heruntergeladenen VODs zurücksetzen.</p>
<div class="form-row" style="margin-bottom: 10px; flex-wrap: wrap;">
<button type="button" class="btn-secondary" id="btnExportConfig" onclick="exportConfigToFile()">Konfiguration exportieren</button>
<button type="button" class="btn-secondary" id="btnImportConfig" onclick="importConfigFromFile()">Konfiguration importieren</button>
<button type="button" class="btn-secondary" id="btnResetDownloadedIds" onclick="resetDownloadedIds()">Downloaded-VODs zurücksetzen</button>
</div>
</div>
<div class="settings-card" data-settings-pane="metrics" hidden>
<h3 id="runtimeMetricsTitle">Runtime Metrics</h3>
<div class="form-row aligned">
<button type="button" class="btn-secondary" id="btnRefreshMetrics" onclick="refreshRuntimeMetrics()">Aktualisieren</button>
<button type="button" class="btn-secondary" id="btnExportMetrics" onclick="exportRuntimeMetrics()">Export JSON</button>
<label class="inline-toggle">
<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" aria-hidden="true"></div>
<span id="statusText">Nicht verbunden</span>
</div>
<span id="statusBarQueueSummary" class="status-bar-queue-summary"></span>
<span id="versionText" class="status-bar-version"></span>
</div>
</main>
</div>
<div class="modal-overlay" id="commandPaletteModal" role="dialog" aria-modal="true" aria-labelledby="commandPaletteTitle">
<div class="modal command-palette">
<h2 id="commandPaletteTitle" class="cp-title">Befehlspalette</h2>
<input
type="text"
id="commandPaletteInput"
class="cp-input"
placeholder="Suche Befehl..."
autocomplete="off"
spellcheck="false"
aria-label="Befehlspalette"
/>
<ul id="commandPaletteList" class="cp-list" role="listbox" aria-label="Befehlsergebnisse"></ul>
<p class="cp-hint" id="commandPaletteHint">Up/Down zum Navigieren, Enter zum Ausführen, Esc zum Schließen</p>
</div>
</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-stats.js"></script>
<script src="../dist/renderer-archive.js"></script>
<script src="../dist/renderer-profile.js"></script>
<script src="../dist/renderer-vod-hover.js"></script>
<script src="../dist/renderer-command-palette.js"></script>
<script src="../dist/renderer.js"></script>
</body>
</html>