Windows CI / verify (push) Failing after 8s
Stabilize Start menu and desktop shortcut migration with persistent versioned icon resources, stale-registration recovery, and explicit Windows shell notifications. Keep development, packaged, and shortcut identities aligned, and add regression coverage for orphaned per-user installs, reduced-motion changelog transitions, and update-popover pointer transit.
1140 lines
98 KiB
HTML
1140 lines
98 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: blob:; media-src 'self' file: blob:;">
|
||
<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-hidden="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()" aria-controls="updateChangelogPanel" aria-expanded="false">Changelog anzeigen</button>
|
||
</div>
|
||
<div class="update-changelog-panel" id="updateChangelogPanel" aria-hidden="true">
|
||
<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>
|
||
|
||
<div class="modal-overlay" id="cutterDiscardModal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="cutterDiscardTitle" onclick="handleCutterDiscardOverlayClick(event)" onkeydown="trapCutterDiscardFocus(event)">
|
||
<div class="modal cutter-discard-modal">
|
||
<h2 id="cutterDiscardTitle">Aktuellen Schnitt verwerfen?</h2>
|
||
<p id="cutterDiscardMessage">Beim Öffnen eines anderen Videos gehen die aktuellen Schnittänderungen verloren.</p>
|
||
<div class="modal-actions">
|
||
<button class="btn-secondary" id="cutterDiscardCancelBtn" type="button" onclick="resolveCutterDiscard(false)">Aktuelles Video behalten</button>
|
||
<button class="btn-danger" id="cutterDiscardConfirmBtn" type="button" onclick="resolveCutterDiscard(true)">Verwerfen und öffnen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Clip Dialog Modal -->
|
||
<div class="modal-overlay" id="clipModal" role="dialog" aria-modal="true" aria-hidden="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-hidden="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-hidden="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-hidden="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 & 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 & 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">
|
||
<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="cutter-source-bar">
|
||
<div class="cutter-source-copy">
|
||
<span class="cutter-source-title" id="cutterSelectTitle">Video auswählen</span>
|
||
<input type="text" id="cutterFilePath" readonly aria-labelledby="cutterSelectTitle" placeholder="Keine Datei ausgewählt…">
|
||
</div>
|
||
<button type="button" class="btn-secondary" id="cutterBrowseBtn" onclick="selectCutterVideo()">Durchsuchen</button>
|
||
<button type="button" class="btn-secondary" id="cutterOpenProjectBtn" onclick="openCutterProject()" disabled>Projekt öffnen</button>
|
||
<button type="button" class="btn-secondary" id="cutterSaveProjectBtn" onclick="saveCutterProject()" disabled>Projekt speichern</button>
|
||
</div>
|
||
<div class="cutter-recovery-panel" id="cutterRecoveryPanel" role="status" hidden>
|
||
<span id="cutterRecoveryText">Gespeicherte Bearbeitung gefunden</span>
|
||
<button type="button" class="btn-secondary" onclick="recoverCutterProject()">Wiederherstellen</button>
|
||
<button type="button" class="btn-secondary" onclick="discardCutterProject()">Verwerfen</button>
|
||
</div>
|
||
|
||
<div class="cutter-workspace" id="cutterWorkspace">
|
||
<aside class="cutter-sidebar">
|
||
<div class="cutter-sidebar-heading">
|
||
<div>
|
||
<span class="cutter-eyebrow">EDITOR</span>
|
||
<h3 id="cutterEditHeading">Trimmen & schneiden</h3>
|
||
</div>
|
||
<button type="button" class="cutter-icon-button" id="cutterNewCutBtn" onclick="addCutterCut()" disabled aria-label="Neuer Schnitt" title="Neuer Schnitt">
|
||
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"></path></svg>
|
||
</button>
|
||
</div>
|
||
<label class="cutter-preview-toggle">
|
||
<span>
|
||
<strong id="cutterPreviewModeLabel">Schnittvorschau</strong>
|
||
<small id="cutterPreviewModeHint">Entfernte Bereiche überspringen</small>
|
||
</span>
|
||
<input type="checkbox" id="cutterPreviewMode" checked onchange="setCutterPreviewMode(this.checked)">
|
||
<span class="cutter-toggle-track" aria-hidden="true"></span>
|
||
</label>
|
||
<div class="cutter-export-options">
|
||
<label for="cutterExportProfile">Exportprofil</label>
|
||
<select id="cutterExportProfile" onchange="setCutterExportProfile(this.value)" disabled>
|
||
<option value="quality">Quality</option>
|
||
<option value="balanced" selected>Balanced</option>
|
||
<option value="fast">Fast</option>
|
||
<option value="archive">Archive</option>
|
||
</select>
|
||
<label for="cutterExportEncoder">Encoder</label>
|
||
<select id="cutterExportEncoder" onchange="setCutterExportEncoder(this.value)" disabled>
|
||
<option value="software">Software</option>
|
||
</select>
|
||
<label for="cutterAudioStream">Audiospur</label>
|
||
<select id="cutterAudioStream" onchange="setCutterAudioStream(this.value)" disabled>
|
||
<option value="0">Keine Audiospur</option>
|
||
</select>
|
||
</div>
|
||
<div class="cutter-trim-card">
|
||
<div class="cutter-card-title" id="cutterGlobalTrimLabel">Gesamtauswahl</div>
|
||
<div class="cutter-time-field-row">
|
||
<label for="startTime" id="cutterStartLabel">Start</label>
|
||
<input type="text" id="startTime" value="00:00:00" spellcheck="false" onchange="updateTimeFromInput()">
|
||
</div>
|
||
<div class="cutter-time-field-row">
|
||
<label for="endTime" id="cutterEndLabel">Ende</label>
|
||
<input type="text" id="endTime" value="00:00:00" spellcheck="false" onchange="updateTimeFromInput()">
|
||
</div>
|
||
</div>
|
||
<div class="cutter-cut-section">
|
||
<div class="cutter-card-title-row">
|
||
<span class="cutter-card-title" id="cutterCutsLabel">Entfernte Bereiche</span>
|
||
<span class="cutter-cut-count" id="cutterCutCount">0</span>
|
||
</div>
|
||
<div class="cutter-cut-list" id="cutterCutList">
|
||
<div class="cutter-cut-empty" id="cutterCutEmpty">Noch keine Schnitte</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="cutter-preview-panel">
|
||
<div class="video-preview" id="cutterPreview">
|
||
<video id="cutterVideo" preload="metadata"></video>
|
||
<div class="placeholder" id="cutterPreviewEmpty">
|
||
<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 eine Vorschau zu sehen</p>
|
||
</div>
|
||
<div class="cutter-player-loading" id="cutterPlayerLoading" hidden>
|
||
<span class="cutter-spinner"></span>
|
||
<span id="cutterLoadingLabel">Video wird vorbereitet…</span>
|
||
</div>
|
||
<div class="cutter-player-controls" id="cutterPlayerControls">
|
||
<button type="button" class="cutter-player-button" id="cutterPlayBtn" onclick="toggleCutterPlayback()" disabled aria-label="Abspielen">
|
||
<svg class="cutter-play-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"></path></svg>
|
||
<svg class="cutter-pause-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M7 5h4v14H7zM13 5h4v14h-4z"></path></svg>
|
||
</button>
|
||
<button type="button" class="cutter-player-button" id="cutterStopBtn" onclick="stopCutterPlayback()" disabled data-cutter-media-control aria-label="Stopp">
|
||
<svg class="cutter-filled-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M7 7h10v10H7z"></path></svg>
|
||
</button>
|
||
<button type="button" class="cutter-player-button cutter-skip-button" id="cutterRewindBtn" onclick="skipCutterPlayback(-10)" disabled data-cutter-media-control aria-label="10 Sekunden zurück">
|
||
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5.2 8.2A8 8 0 1 1 4 14.5"></path><path d="M5 4v5h5"></path></svg><span>10</span>
|
||
</button>
|
||
<button type="button" class="cutter-player-button cutter-skip-button" id="cutterForwardBtn" onclick="skipCutterPlayback(10)" disabled data-cutter-media-control aria-label="10 Sekunden vor">
|
||
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M18.8 8.2A8 8 0 1 0 20 14.5"></path><path d="M19 4v5h-5"></path></svg><span>10</span>
|
||
</button>
|
||
<div class="cutter-volume-control">
|
||
<button type="button" class="cutter-player-button" id="cutterMuteBtn" onclick="toggleCutterMute()" disabled aria-label="Ton umschalten">
|
||
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 9v6h4l5 4V5L8 9H4zm12.5 3a4 4 0 0 0-2-3.46v6.92A4 4 0 0 0 16.5 12z"></path></svg>
|
||
</button>
|
||
<input type="range" class="cutter-volume" id="cutterVolume" min="0" max="1" step="0.05" value="1" disabled aria-label="Lautstärke">
|
||
</div>
|
||
<span class="cutter-player-time"><span id="cutterCurrentTime">00:00:00</span><span>/</span><span id="cutterTotalTime">00:00:00</span></span>
|
||
<select id="cutterPlaybackRate" hidden disabled aria-label="Wiedergabegeschwindigkeit">
|
||
<option value="0.5">0,5×</option>
|
||
<option value="0.75">0,75×</option>
|
||
<option value="1" selected>1×</option>
|
||
<option value="1.25">1,25×</option>
|
||
<option value="1.5">1,5×</option>
|
||
<option value="2">2×</option>
|
||
</select>
|
||
<div class="cutter-player-settings">
|
||
<button type="button" class="cutter-player-button" id="cutterSettingsBtn" onclick="toggleCutterSettingsMenu()" disabled aria-label="Einstellungen" aria-expanded="false">
|
||
<svg class="cutter-filled-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M19.4 13a7.8 7.8 0 0 0 .1-1 7.8 7.8 0 0 0-.1-1l2.1-1.6-2-3.4-2.5 1a8 8 0 0 0-1.7-1L15 3.3h-4L10.6 6a8 8 0 0 0-1.7 1L6.4 6l-2 3.4L6.5 11a7.8 7.8 0 0 0-.1 1 7.8 7.8 0 0 0 .1 1l-2.1 1.6 2 3.4 2.5-1a8 8 0 0 0 1.7 1l.4 2.7h4l.4-2.7a8 8 0 0 0 1.7-1l2.5 1 2-3.4L19.4 13zM13 15.5A3.5 3.5 0 1 1 13 8a3.5 3.5 0 0 1 0 7.5z"></path></svg>
|
||
</button>
|
||
<div class="cutter-settings-menu" id="cutterSettingsMenu" hidden>
|
||
<span id="cutterSpeedLabel">Geschwindigkeit</span>
|
||
<div class="cutter-speed-options">
|
||
<button type="button" data-rate="0.5" onclick="setCutterPlaybackRate(0.5)">0,5×</button>
|
||
<button type="button" data-rate="0.75" onclick="setCutterPlaybackRate(0.75)">0,75×</button>
|
||
<button type="button" class="active" data-rate="1" onclick="setCutterPlaybackRate(1)">Normal</button>
|
||
<button type="button" data-rate="1.25" onclick="setCutterPlaybackRate(1.25)">1,25×</button>
|
||
<button type="button" data-rate="1.5" onclick="setCutterPlaybackRate(1.5)">1,5×</button>
|
||
<button type="button" data-rate="2" onclick="setCutterPlaybackRate(2)">2×</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="cutter-player-button" id="cutterFullscreenBtn" onclick="toggleCutterFullscreen()" disabled aria-label="Vollbild">
|
||
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5"></path></svg>
|
||
</button>
|
||
</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">----×----</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">Ausgabe</span><span class="cutter-info-value" id="infoSelection">--:--:--</span></div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div class="timeline-container" id="timelineContainer">
|
||
<div class="cutter-timeline-toolbar">
|
||
<div class="cutter-timeline-timecode" id="cutterTimelineTimecode">00:00:00</div>
|
||
<div class="cutter-history-controls">
|
||
<button type="button" class="cutter-icon-button" id="cutterUndoBtn" onclick="undoCutterEdit()" disabled aria-label="Rückgängig" title="Rückgängig (Strg+Z)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M9 7 4 12l5 5v-3h5a5 5 0 0 1 5 5v1h2v-1a7 7 0 0 0-7-7H9V7z"></path></svg></button>
|
||
<button type="button" class="cutter-icon-button" id="cutterRedoBtn" onclick="redoCutterEdit()" disabled aria-label="Wiederholen" title="Wiederholen (Strg+Y)"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="m15 7 5 5-5 5v-3h-5a5 5 0 0 0-5 5v1H3v-1a7 7 0 0 1 7-7h5V7z"></path></svg></button>
|
||
</div>
|
||
<div class="cutter-zoom-controls">
|
||
<button type="button" class="cutter-icon-button" id="cutterZoomOutBtn" onclick="changeCutterZoom(-0.25)" disabled aria-label="Verkleinern"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"></path></svg></button>
|
||
<input type="range" id="cutterZoom" min="1" max="16" step="0.05" value="1" disabled aria-label="Timeline-Zoom">
|
||
<button type="button" class="cutter-icon-button" id="cutterZoomInBtn" onclick="changeCutterZoom(0.25)" disabled aria-label="Vergrößern"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"></path></svg></button>
|
||
</div>
|
||
<div class="cutter-actions">
|
||
<button type="button" class="btn-secondary" id="cutterCancelExportBtn" onclick="cancelCutterExport()" hidden>Abbrechen</button>
|
||
<button type="button" class="btn-primary" id="btnCut" onclick="startCutting()" disabled>Video exportieren</button>
|
||
</div>
|
||
</div>
|
||
<div class="cutter-timeline-scroll" id="cutterTimelineScroll">
|
||
<div class="timeline" id="timeline">
|
||
<div class="cutter-ruler" id="cutterRuler"></div>
|
||
<div class="cutter-track cutter-video-track" id="cutterVideoTrack">
|
||
<span class="cutter-track-label" id="cutterVideoTrackLabel">VIDEO</span>
|
||
<div class="cutter-thumbnail-strip" id="cutterThumbnailStrip"></div>
|
||
</div>
|
||
<div class="cutter-track cutter-audio-track" id="cutterAudioTrack">
|
||
<span class="cutter-track-label" id="cutterAudioTrackLabel">AUDIO</span>
|
||
<img id="cutterWaveform" alt="" draggable="false">
|
||
<div class="cutter-audio-empty" id="cutterAudioEmpty">Keine Audiospur</div>
|
||
</div>
|
||
<div class="cutter-outside-shade cutter-outside-left" id="cutterOutsideLeft"></div>
|
||
<div class="cutter-outside-shade cutter-outside-right" id="cutterOutsideRight"></div>
|
||
<div class="timeline-selection" id="timelineSelection">
|
||
<button type="button" class="timeline-handle start" id="cutterTrimStartHandle" aria-label="Start verschieben"></button>
|
||
<button type="button" class="timeline-handle end" id="cutterTrimEndHandle" aria-label="Ende verschieben"></button>
|
||
</div>
|
||
<div class="cutter-cut-overlays" id="cutterCutOverlays"></div>
|
||
<div class="timeline-current" id="timelineCurrent"><span></span></div>
|
||
</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>
|
||
</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 download-policy-settings">
|
||
<h4 id="downloadPolicyTitle">Drosselung und Zeitfenster</h4>
|
||
<div class="form-group">
|
||
<label id="downloadThrottleLabel" for="downloadThrottleMiBps">Maximale Downloadrate (MiB/s)</label>
|
||
<input type="text" id="downloadThrottleMiBps" inputmode="decimal" autocomplete="off" aria-describedby="downloadThrottleHint downloadPolicyValidation">
|
||
<div id="downloadThrottleHint" class="form-note">Leer lassen für keine Drosselung. Dezimalwerte wie 1,5 sind möglich.</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label id="downloadWindowsLabel" for="downloadWindows">Lokale Download-Zeitfenster</label>
|
||
<textarea id="downloadWindows" rows="3" aria-describedby="downloadWindowsHint downloadPolicyValidation" placeholder="22:00-06:00"></textarea>
|
||
<div id="downloadWindowsHint" class="form-note">Ein Zeitfenster pro Zeile oder durch Semikolon getrennt. Über Mitternacht ist erlaubt.</div>
|
||
</div>
|
||
<div id="downloadPolicyValidation" class="form-note" role="alert"></div>
|
||
<div id="downloadPolicyStatus" class="form-note" role="status" aria-live="polite"></div>
|
||
<button type="button" class="btn-secondary" id="downloadPolicyOverrideBtn" onclick="startDownloadPolicyOverride()">Jetzt starten</button>
|
||
</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.7</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>
|
||
<h4 id="managedToolsTitle">Verwaltete Tools</h4>
|
||
<div class="form-row" style="margin-bottom: 10px;">
|
||
<button type="button" class="btn-secondary" id="btnRefreshManagedTools" onclick="refreshManagedToolStatus()">Tool-Status aktualisieren</button>
|
||
<button type="button" class="btn-secondary" id="btnRepairManagedTools" onclick="repairManagedTools()">Tools reparieren</button>
|
||
<button type="button" class="btn-secondary" id="btnResetManagedTools" onclick="resetManagedTools()">Tools zurücksetzen</button>
|
||
</div>
|
||
<pre id="managedToolStatus" class="log-panel">Noch kein Tool-Status geladen.</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 & 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-hidden="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"
|
||
role="combobox"
|
||
aria-autocomplete="list"
|
||
aria-controls="commandPaletteList"
|
||
aria-expanded="false"
|
||
/>
|
||
<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-accessibility.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-cutter.js"></script>
|
||
<script src="../dist/renderer.js"></script>
|
||
</body>
|
||
</html>
|