Two real UX wins.
1. Auto-resume queue on startup. New checkbox in Settings -> Download
("Queue beim Start automatisch fortsetzen"). When enabled and the
persisted queue has pending items, processQueue() fires ~5 seconds
after did-finish-load — long enough for the user to see the queue
and pause if they did not actually want this. Default off so the
existing behaviour (explicit Start click) is preserved on upgrade.
The Settings auto-save fingerprint includes the new flag and
syncSettingsFormFromConfig restores it. Tooltip explains the
timing on hover.
2. Already-downloaded indicator on VOD cards. Config gains
downloaded_vod_ids: string[] (bounded to 4096 latest entries).
Every successful queue-item download appends its parsed VOD ID
(or every component ID for merge groups). On the VOD grid each
card whose vod.id is in the set gets a small green checkmark
badge in the top-right plus a slightly dimmed thumbnail, with a
localized "Already downloaded" / "Bereits heruntergeladen"
tooltip. The lookup builds a Set once per render so it stays
O(1) per card. The renderer refreshes its local config copy on
every "newly completed" queue update so the badge appears live
without waiting for a settings save.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
598 lines
42 KiB
HTML
598 lines
42 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:;">
|
|
<title>Twitch VOD Manager</title>
|
|
<link rel="stylesheet" href="./styles.css">
|
|
</head>
|
|
<body class="theme-twitch">
|
|
<div class="update-banner" id="updateBanner">
|
|
<span id="updateText">Neue Version verfügbar!</span>
|
|
<div id="updateProgress" style="display: none; flex: 1; margin: 0 15px;">
|
|
<div style="background: rgba(0,0,0,0.3); border-radius: 4px; height: 8px; overflow: hidden;">
|
|
<div id="updateProgressBar" style="background: white; height: 100%; width: 0%; transition: width 0.3s;"></div>
|
|
</div>
|
|
</div>
|
|
<button id="updateButton" onclick="downloadUpdate()">Jetzt herunterladen</button>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="updateModal" onclick="handleUpdateModalOverlayClick(event)">
|
|
<div class="modal update-modal">
|
|
<button class="modal-close" onclick="dismissUpdateModal()">x</button>
|
|
<div class="update-modal-eyebrow" id="updateModalEyebrow">Updates</div>
|
|
<h2 id="updateModalTitle">Update verfugbar</h2>
|
|
<p class="update-modal-message" id="updateModalMessage">Version 0.0.0 ist verfugbar. Jetzt herunterladen?</p>
|
|
<div class="update-modal-meta" id="updateModalMeta" style="display:none;"></div>
|
|
|
|
<div class="update-changelog-card" id="updateChangelogCard" style="display:none;">
|
|
<div class="update-changelog-header">
|
|
<span class="update-changelog-label" id="updateChangelogLabel">Changelog</span>
|
|
<button type="button" class="update-changelog-toggle" id="updateChangelogToggle" onclick="toggleUpdateChangelog()">Changelog anzeigen</button>
|
|
</div>
|
|
<div class="update-changelog-panel" id="updateChangelogPanel" hidden>
|
|
<div class="update-changelog-content" id="updateChangelogContent"></div>
|
|
<p class="update-changelog-empty" id="updateChangelogEmpty" hidden>Kein Changelog verfugbar.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-actions update-modal-actions">
|
|
<button class="btn-secondary" id="updateModalDismissBtn" type="button" onclick="dismissUpdateModal()">Nein</button>
|
|
<button class="btn-secondary" id="updateModalSkipBtn" type="button" onclick="skipUpdateVersion()">Diese Version ueberspringen</button>
|
|
<button class="btn-primary" id="updateModalConfirmBtn" type="button" onclick="confirmUpdateModal()">Ja, herunterladen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clip Dialog Modal -->
|
|
<div class="modal-overlay" id="clipModal">
|
|
<div class="modal" style="background: #2b2b2b; max-width: 500px;">
|
|
<button class="modal-close" onclick="closeClipDialog()">x</button>
|
|
<h2 style="color: #E5A00D; text-align: center; margin-bottom: 20px;" id="clipDialogTitle">VOD zuschneiden</h2>
|
|
|
|
<!-- Start Zeit mit Slider -->
|
|
<div style="margin-bottom: 15px;">
|
|
<label id="clipDialogStartLabel" style="display: block; margin-bottom: 5px;">Start:</label>
|
|
<input type="range" id="clipStartSlider" min="0" max="100" value="0"
|
|
style="width: 100%; height: 6px; -webkit-appearance: none; background: #1a1a1a; border-radius: 3px; cursor: pointer;"
|
|
oninput="updateFromSlider('start')">
|
|
<div style="display: flex; align-items: center; gap: 10px; margin-top: 8px;">
|
|
<label id="clipDialogStartTimeLabel" style="color: #888;">Startzeit (HH:MM:SS):</label>
|
|
<input type="text" id="clipStartTime" value="00:00:00"
|
|
style="width: 100px; background: #333; border: 1px solid #444; border-radius: 4px; padding: 6px 10px; color: white; font-family: monospace; text-align: center;"
|
|
onchange="updateFromInput('start')">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- End Zeit mit Slider -->
|
|
<div style="margin-bottom: 15px;">
|
|
<label id="clipDialogEndLabel" style="display: block; margin-bottom: 5px;">Ende:</label>
|
|
<input type="range" id="clipEndSlider" min="0" max="100" value="60"
|
|
style="width: 100%; height: 6px; -webkit-appearance: none; background: #1a1a1a; border-radius: 3px; cursor: pointer;"
|
|
oninput="updateFromSlider('end')">
|
|
<div style="display: flex; align-items: center; gap: 10px; margin-top: 8px;">
|
|
<label id="clipDialogEndTimeLabel" style="color: #888;">Endzeit (HH:MM:SS):</label>
|
|
<input type="text" id="clipEndTime" value="00:01:00"
|
|
style="width: 100px; background: #333; border: 1px solid #444; border-radius: 4px; padding: 6px 10px; color: white; font-family: monospace; text-align: center;"
|
|
onchange="updateFromInput('end')">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dauer Anzeige -->
|
|
<div style="text-align: center; margin-bottom: 20px;">
|
|
<span id="clipDialogDurationLabel" style="color: #888;">Dauer: </span>
|
|
<span id="clipDurationDisplay" style="color: #00c853;">00:01:00</span>
|
|
</div>
|
|
|
|
<!-- Teil Nummer -->
|
|
<div style="margin-bottom: 15px;">
|
|
<label id="clipDialogPartLabel" style="display: block; margin-bottom: 8px;">Start Part-Nummer (optional, fur Fortsetzung):</label>
|
|
<input type="text" id="clipStartPart" placeholder="z.B. 42"
|
|
style="width: 100px; background: #333; border: 1px solid #444; border-radius: 4px; padding: 8px 12px; color: white;"
|
|
oninput="updateFilenameExamples()">
|
|
<div id="clipDialogPartHint" style="color: #888; font-size: 12px; margin-top: 5px;">Leer lassen = Teil 1</div>
|
|
</div>
|
|
|
|
<!-- Dateinamen Format -->
|
|
<div style="margin-bottom: 20px;">
|
|
<label id="clipDialogFormatLabel" style="display: block; margin-bottom: 10px;">Dateinamen-Format:</label>
|
|
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
|
|
<input type="radio" name="filenameFormat" value="simple" checked onchange="updateFilenameExamples()"
|
|
style="width: 18px; height: 18px; accent-color: #9146FF;">
|
|
<span id="formatSimple" style="color: #aaa;">01.02.2026_1.mp4 (Standard)</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
|
|
<input type="radio" name="filenameFormat" value="timestamp" onchange="updateFilenameExamples()"
|
|
style="width: 18px; height: 18px; accent-color: #9146FF;">
|
|
<span id="formatTimestamp" style="color: #aaa;">01.02.2026_CLIP_00-00-00_1.mp4 (mit Zeitstempel)</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
|
|
<input type="radio" name="filenameFormat" value="parts" onchange="updateFilenameExamples()"
|
|
style="width: 18px; height: 18px; accent-color: #9146FF;">
|
|
<span id="formatParts" style="color: #aaa;">01.02.2026_Part01.mp4 (Parts-Format)</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 10px;">
|
|
<input type="radio" name="filenameFormat" value="template" onchange="updateFilenameExamples()"
|
|
style="width: 18px; height: 18px; accent-color: #9146FF;">
|
|
<span id="formatTemplate" style="color: #aaa;">{date}_{part}.mp4 (benutzerdefiniert)</span>
|
|
</label>
|
|
|
|
<div id="clipFilenameTemplateWrap" style="display:none; margin-top: 10px;">
|
|
<input type="text" id="clipFilenameTemplate" value="{date}_{part}.mp4"
|
|
placeholder="{date}_{part}.mp4"
|
|
style="width: 100%; background: #333; border: 1px solid #444; border-radius: 4px; padding: 8px 12px; color: white; font-family: monospace;"
|
|
oninput="updateFilenameExamples()">
|
|
<div id="clipTemplateHelp" style="color: #888; font-size: 12px; margin-top: 6px;">Platzhalter: {title} {id} {channel} {date} {part} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}</div>
|
|
<div id="clipTemplateLint" style="color: #8bc34a; font-size: 12px; margin-top: 4px;">Template-Check: OK</div>
|
|
<button class="btn-secondary" id="clipTemplateGuideBtn" style="margin-top: 8px;" onclick="openTemplateGuide('clip')">Template Guide</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Button -->
|
|
<div style="text-align: center;">
|
|
<button class="btn-primary" id="clipDialogConfirmBtn" style="background: #00c853; padding: 12px 30px; border: none; border-radius: 4px; color: white; font-weight: 600; cursor: pointer;" onclick="confirmClipDialog()">Zur Queue hinzufugen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Template Guide Modal -->
|
|
<div class="modal-overlay" id="templateGuideModal">
|
|
<div class="modal template-guide-modal">
|
|
<button class="modal-close" onclick="closeTemplateGuide()">x</button>
|
|
<h2 id="templateGuideTitle">Template Guide</h2>
|
|
<p id="templateGuideIntro" class="template-guide-intro">Nutze Variablen fur Dateinamen und prufe das Ergebnis als Live-Vorschau.</p>
|
|
|
|
<div class="template-guide-actions">
|
|
<button class="btn-secondary" id="templateGuideUseVod" onclick="setTemplateGuidePreset('vod')">VOD Template</button>
|
|
<button class="btn-secondary" id="templateGuideUseParts" onclick="setTemplateGuidePreset('parts')">VOD Part Template</button>
|
|
<button class="btn-secondary" id="templateGuideUseClip" onclick="setTemplateGuidePreset('clip')">Clip Template</button>
|
|
</div>
|
|
|
|
<label id="templateGuideTemplateLabel" class="template-guide-label">Template</label>
|
|
<input type="text" id="templateGuideInput" class="template-guide-input" oninput="updateTemplateGuidePreview()" placeholder="{title}.mp4">
|
|
|
|
<div class="template-guide-preview-box">
|
|
<div class="template-guide-preview-label" id="templateGuideOutputLabel">Live Vorschau</div>
|
|
<div id="templateGuideOutput" class="template-guide-output">-</div>
|
|
<div id="templateGuideContext" class="template-guide-context"></div>
|
|
</div>
|
|
|
|
<h3 id="templateGuideVarsTitle" class="template-guide-vars-title">Verfugbare Variablen</h3>
|
|
<div class="template-guide-table-wrap">
|
|
<table class="template-guide-table">
|
|
<thead>
|
|
<tr>
|
|
<th id="templateGuideVarCol">Variable</th>
|
|
<th id="templateGuideDescCol">Beschreibung</th>
|
|
<th id="templateGuideExampleCol">Beispiel</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="templateGuideBody"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="template-guide-footer">
|
|
<button class="btn-secondary" id="templateGuideCloseBtn" onclick="closeTemplateGuide()">Schliessen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="app">
|
|
<aside class="sidebar">
|
|
<div class="logo">
|
|
<svg viewBox="0 0 24 24"><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/></svg>
|
|
<span id="logoText">Twitch VOD Manager</span>
|
|
</div>
|
|
|
|
<nav class="nav">
|
|
<div class="nav-item active" data-tab="vods" onclick="showTab('vods')">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 8l7 4-7 4V8z"/></svg>
|
|
<span id="navVodsText">Twitch VODs</span>
|
|
</div>
|
|
<div class="nav-item" data-tab="clips" onclick="showTab('clips')">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
|
<span id="navClipsText">Twitch Clips</span>
|
|
</div>
|
|
<div class="nav-item" data-tab="cutter" onclick="showTab('cutter')">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3h-3z"/></svg>
|
|
<span id="navCutterText">Video schneiden</span>
|
|
</div>
|
|
<div class="nav-item" data-tab="merge" onclick="showTab('merge')">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 20.41L18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z"/></svg>
|
|
<span id="navMergeText">Videos zusammenfugen</span>
|
|
</div>
|
|
<div class="nav-item" data-tab="settings" onclick="showTab('settings')">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
|
<span id="navSettingsText">Einstellungen</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="section-title">Streamer</div>
|
|
<div class="streamers" id="streamerList"></div>
|
|
|
|
<div class="queue-section">
|
|
<div class="queue-header">
|
|
<span class="queue-title" id="queueTitleText">Warteschlange</span>
|
|
<span class="queue-count" id="queueCount">0</span>
|
|
</div>
|
|
<div class="queue-list" id="queueList"></div>
|
|
<div class="queue-actions">
|
|
<button class="btn btn-start" id="btnStart" onclick="toggleDownload()">Start</button>
|
|
<button class="btn btn-merge-group" id="btnMergeGroup" onclick="createMergeGroupFromSelection()" style="display:none">Merge & Split</button>
|
|
<button class="btn btn-retry" id="btnRetryFailed" onclick="retryFailedDownloads()" title="Nur fehlgeschlagene Downloads erneut starten">Wiederholen</button>
|
|
<button class="btn btn-clear" id="btnClear" onclick="clearCompleted()">Leeren</button>
|
|
</div>
|
|
</div>
|
|
<div class="stats-bar" id="statsBar"></div>
|
|
</aside>
|
|
|
|
<main class="main">
|
|
<header class="header">
|
|
<h1 id="pageTitle">VODs</h1>
|
|
<div class="header-actions">
|
|
<div class="header-search">
|
|
<input type="text" id="newStreamer" placeholder="Streamer hinzufugen..." onkeypress="if(event.key==='Enter')addStreamer()">
|
|
<button onclick="addStreamer()">+</button>
|
|
</div>
|
|
<button class="btn-icon" onclick="refreshVODs()">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
<span id="refreshText">Aktualisieren</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content">
|
|
<!-- VODs Tab -->
|
|
<div class="tab-content active" id="vodsTab">
|
|
<div class="vod-filter-row" style="display:flex; align-items:center; gap:8px; margin-bottom:12px; flex-wrap:wrap;">
|
|
<input type="text" id="vodFilterInput" placeholder="Filter VODs..." oninput="onVodFilterInput()" style="flex:1; min-width:180px; background: var(--bg-card); border:1px solid var(--border-soft); border-radius:6px; padding:8px 12px; color: var(--text); font-size:13px;">
|
|
<button id="vodFilterClearBtn" onclick="clearVodFilter()" title="Clear filter" style="display:none; background:transparent; border:1px solid var(--border-soft); border-radius:6px; padding:8px 12px; color: var(--text-secondary); cursor:pointer;">x</button>
|
|
<label id="vodSortLabel" for="vodSortSelect" style="color: var(--text-secondary); font-size:12px; margin-left:8px;">Sort:</label>
|
|
<select id="vodSortSelect" onchange="onVodSortChange()" style="background: var(--bg-card); border:1px solid var(--border-soft); border-radius:6px; padding:7px 10px; color: var(--text); font-size:13px;">
|
|
<option value="date_desc">Newest first</option>
|
|
<option value="date_asc">Oldest first</option>
|
|
<option value="views_desc">Most viewed</option>
|
|
<option value="duration_desc">Longest first</option>
|
|
<option value="duration_asc">Shortest first</option>
|
|
</select>
|
|
<span id="vodFilterCount" style="color: var(--text-secondary); font-size:12px; min-width:80px;"></span>
|
|
</div>
|
|
<div id="vodBulkBar" class="vod-bulk-bar" style="display:none; align-items:center; gap:10px; padding:8px 12px; background: rgba(145, 70, 255, 0.12); border:1px solid rgba(145, 70, 255, 0.4); border-radius:6px; margin-bottom:12px;">
|
|
<span id="vodBulkCount" style="color: var(--text); font-size:13px; font-weight:600;">0 selected</span>
|
|
<span style="flex:1;"></span>
|
|
<button id="vodBulkAddBtn" type="button" onclick="bulkAddSelectedVodsToQueue()" style="background:var(--accent); border:none; border-radius:6px; padding:6px 14px; color:#fff; font-size:13px; font-weight:600; cursor:pointer;">+ Queue</button>
|
|
<button id="vodBulkClearBtn" type="button" onclick="clearVodSelection()" style="background:transparent; border:1px solid var(--border-soft); border-radius:6px; padding:6px 12px; color:var(--text-secondary); font-size:13px; cursor:pointer;">Clear</button>
|
|
</div>
|
|
<div class="vod-grid" id="vodGrid">
|
|
<div class="empty-state">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-9 14l-5-4 5-4v8zm2-8l5 4-5 4V9z"/></svg>
|
|
<h3>Keine VODs</h3>
|
|
<p>Wahle einen Streamer aus der Liste oder fuge einen neuen hinzu.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clips Tab -->
|
|
<div class="tab-content" id="clipsTab">
|
|
<div class="clip-input">
|
|
<h2 id="clipsHeading">Twitch Clip-Download</h2>
|
|
<input type="text" id="clipUrl" placeholder="https://clips.twitch.tv/... oder https://www.twitch.tv/.../clip/...">
|
|
<button class="btn-primary" onclick="downloadClip()" id="btnClip">Clip herunterladen</button>
|
|
<div class="clip-status" id="clipStatus"></div>
|
|
</div>
|
|
|
|
<div class="settings-card" style="max-width: 600px; margin: 20px auto;">
|
|
<h3 id="clipsInfoTitle">Info</h3>
|
|
<p style="color: var(--text-secondary); line-height: 1.6; white-space: pre-line;" id="clipsInfoText">
|
|
Unterstutzte Formate:
|
|
- https://clips.twitch.tv/ClipName
|
|
- https://www.twitch.tv/streamer/clip/ClipName
|
|
|
|
Clips werden im Download-Ordner unter "Clips/StreamerName/" gespeichert.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Cutter Tab -->
|
|
<div class="tab-content" id="cutterTab">
|
|
<div class="cutter-container">
|
|
<div class="settings-card">
|
|
<h3 id="cutterSelectTitle">Video auswahlen</h3>
|
|
<div class="form-row">
|
|
<input type="text" id="cutterFilePath" readonly placeholder="Keine Datei ausgewahlt...">
|
|
<button class="btn-secondary" id="cutterBrowseBtn" onclick="selectCutterVideo()">Durchsuchen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="video-preview" id="cutterPreview">
|
|
<div class="placeholder">
|
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="currentColor" style="opacity:0.3"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 8l7 4-7 4V8z"/></svg>
|
|
<p style="margin-top:10px">Video auswahlen um Vorschau zu sehen</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cutter-info" id="cutterInfo" style="display:none">
|
|
<div class="cutter-info-item">
|
|
<span class="cutter-info-label" id="cutterInfoDurationLabel">Dauer</span>
|
|
<span class="cutter-info-value" id="infoDuration">--:--:--</span>
|
|
</div>
|
|
<div class="cutter-info-item">
|
|
<span class="cutter-info-label" id="cutterInfoResolutionLabel">Aufloesung</span>
|
|
<span class="cutter-info-value" id="infoResolution">----x----</span>
|
|
</div>
|
|
<div class="cutter-info-item">
|
|
<span class="cutter-info-label" id="cutterInfoFpsLabel">FPS</span>
|
|
<span class="cutter-info-value" id="infoFps">--</span>
|
|
</div>
|
|
<div class="cutter-info-item">
|
|
<span class="cutter-info-label" id="cutterInfoSelectionLabel">Auswahl</span>
|
|
<span class="cutter-info-value" id="infoSelection">--:--:--</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="timeline-container" id="timelineContainer" style="display:none">
|
|
<div class="timeline" id="timeline" onclick="seekTimeline(event)">
|
|
<div class="timeline-selection" id="timelineSelection"></div>
|
|
<div class="timeline-current" id="timelineCurrent"></div>
|
|
</div>
|
|
|
|
<div class="time-inputs">
|
|
<div class="time-input-group">
|
|
<label id="cutterStartLabel">Start:</label>
|
|
<input type="text" id="startTime" value="00:00:00" onchange="updateTimeFromInput()">
|
|
</div>
|
|
<div class="time-input-group">
|
|
<label id="cutterEndLabel">Ende:</label>
|
|
<input type="text" id="endTime" value="00:00:00" onchange="updateTimeFromInput()">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="progress-container" id="cutProgress">
|
|
<div class="progress-bar">
|
|
<div class="progress-bar-fill" id="cutProgressBar"></div>
|
|
</div>
|
|
<div class="progress-text" id="cutProgressText">0%</div>
|
|
</div>
|
|
|
|
<div class="cutter-actions">
|
|
<button class="btn-primary" id="btnCut" onclick="startCutting()" disabled>Schneiden</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Merge Tab -->
|
|
<div class="tab-content" id="mergeTab">
|
|
<div class="merge-container">
|
|
<div class="settings-card">
|
|
<h3 id="mergeTitle">Videos zusammenfugen</h3>
|
|
<p style="color: var(--text-secondary); margin-bottom: 15px;" id="mergeDesc">
|
|
Wahle mehrere Videos aus um sie zu einem Video zusammenzufugen.
|
|
Die Reihenfolge kann per Drag & Drop geandert werden.
|
|
</p>
|
|
<button class="btn-secondary" id="mergeAddBtn" onclick="addMergeFiles()">+ Videos hinzufugen</button>
|
|
</div>
|
|
|
|
<div class="file-list" id="mergeFileList">
|
|
<div class="empty-state" style="padding: 40px 20px;">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" style="opacity:0.3"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
<p style="margin-top:10px">Keine Videos ausgewahlt</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="progress-container" id="mergeProgress">
|
|
<div class="progress-bar">
|
|
<div class="progress-bar-fill" id="mergeProgressBar"></div>
|
|
</div>
|
|
<div class="progress-text" id="mergeProgressText">0%</div>
|
|
</div>
|
|
|
|
<div class="merge-actions">
|
|
<button class="btn-primary" id="btnMerge" onclick="startMerging()" disabled>Zusammenfugen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Tab -->
|
|
<div class="tab-content" id="settingsTab">
|
|
<div class="settings-card">
|
|
<h3 id="designTitle">Design</h3>
|
|
<div class="form-group">
|
|
<label id="themeLabel">Theme</label>
|
|
<select id="themeSelect" onchange="changeTheme(this.value)">
|
|
<option value="twitch">Twitch</option>
|
|
<option value="discord">Discord</option>
|
|
<option value="youtube">YouTube</option>
|
|
<option value="apple">Apple</option>
|
|
<option value="light" id="themeLightOption">Light</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label id="languageLabel">Sprache</label>
|
|
<div class="language-picker" id="languagePicker">
|
|
<button type="button" class="lang-option" id="langOptionDe" onclick="selectLanguageOption('de')" aria-pressed="false">
|
|
<span class="flag-icon flag-de" aria-hidden="true"></span>
|
|
<span id="languageDeText">Deutsch</span>
|
|
</button>
|
|
<button type="button" class="lang-option" id="langOptionEn" onclick="selectLanguageOption('en')" aria-pressed="false">
|
|
<span class="flag-icon flag-en" aria-hidden="true"></span>
|
|
<span id="languageEnText">English</span>
|
|
</button>
|
|
</div>
|
|
<select id="languageSelect" onchange="changeLanguage(this.value)" style="display:none">
|
|
<option value="de">de</option>
|
|
<option value="en">en</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-card">
|
|
<h3 id="apiTitle">Twitch API</h3>
|
|
<p id="apiHelpText" style="color: var(--text-secondary); font-size:13px; margin-bottom:12px; line-height:1.5;">
|
|
<span id="apiHelpIntro">Du brauchst eine Client-ID und ein Client-Secret von Twitch.</span>
|
|
<a href="#" id="apiHelpLink" onclick="event.preventDefault(); openTwitchDevConsole()" style="color: var(--accent); text-decoration: underline; cursor: pointer;">dev.twitch.tv/console/apps</a>
|
|
</p>
|
|
<div class="form-group">
|
|
<label id="clientIdLabel">Client ID</label>
|
|
<input type="text" id="clientId" placeholder="Twitch Client ID">
|
|
</div>
|
|
<div class="form-group">
|
|
<label id="clientSecretLabel">Client Secret</label>
|
|
<input type="password" id="clientSecret" placeholder="Twitch Client Secret">
|
|
</div>
|
|
<button class="btn-primary" id="saveSettingsBtn" onclick="saveSettings()">Speichern & Verbinden</button>
|
|
</div>
|
|
|
|
<div class="settings-card">
|
|
<h3 id="downloadSettingsTitle">Download-Einstellungen</h3>
|
|
<div class="form-group">
|
|
<label id="storageLabel">Speicherort</label>
|
|
<div class="form-row">
|
|
<input type="text" id="downloadPath" readonly>
|
|
<button class="btn-secondary" onclick="selectFolder()">Ordner</button>
|
|
<button class="btn-secondary" id="openFolderBtn" onclick="openFolder()">Offnen</button>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label id="modeLabel">Download-Modus</label>
|
|
<select id="downloadMode">
|
|
<option value="full" id="modeFullText">Ganzes VOD</option>
|
|
<option value="parts" id="modePartsText">In Teile splitten</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label id="partMinutesLabel">Teil-Lange (Minuten)</label>
|
|
<input type="number" id="partMinutes" value="120" min="10" max="480">
|
|
</div>
|
|
<div class="form-group">
|
|
<label id="parallelDownloadsLabel">Parallele Downloads</label>
|
|
<select id="parallelDownloads">
|
|
<option value="1" id="parallelDownloads1">1 (Standard)</option>
|
|
<option value="2" id="parallelDownloads2">2 (Parallel)</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label id="performanceModeLabel">Performance-Profil</label>
|
|
<select id="performanceMode">
|
|
<option value="stability" id="performanceModeStability">Max Stabilitat</option>
|
|
<option value="balanced" id="performanceModeBalanced">Ausgewogen</option>
|
|
<option value="speed" id="performanceModeSpeed">Max Geschwindigkeit</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label style="display:flex; align-items:center; gap:8px;">
|
|
<input type="checkbox" id="smartSchedulerToggle" checked>
|
|
<span id="smartSchedulerLabel">Smart Queue Scheduler aktivieren</span>
|
|
</label>
|
|
<label style="display:flex; align-items:center; gap:8px; margin-top: 8px;">
|
|
<input type="checkbox" id="duplicatePreventionToggle" checked>
|
|
<span id="duplicatePreventionLabel">Duplikate in Queue verhindern</span>
|
|
</label>
|
|
<label style="display:flex; align-items:center; gap:8px; margin-top: 8px;">
|
|
<input type="checkbox" id="persistQueueToggle" checked>
|
|
<span id="persistQueueLabel">Queue zwischen App-Starts speichern</span>
|
|
</label>
|
|
<label style="display:flex; align-items:center; gap:8px; margin-top: 8px;">
|
|
<input type="checkbox" id="autoResumeQueueToggle">
|
|
<span id="autoResumeQueueLabel">Queue beim Start automatisch fortsetzen</span>
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label id="metadataCacheMinutesLabel">Metadata-Cache (Minuten)</label>
|
|
<input type="number" id="metadataCacheMinutes" value="10" min="1" max="120">
|
|
</div>
|
|
<div class="form-group">
|
|
<div class="form-row" style="align-items:center; margin-bottom: 4px;">
|
|
<label id="filenameTemplatesTitle" style="margin: 0;">Dateinamen-Templates</label>
|
|
<button class="btn-secondary" id="settingsTemplateGuideBtn" type="button" onclick="openTemplateGuide('vod')">Template Guide</button>
|
|
</div>
|
|
<div class="form-row" style="gap: 8px; margin: 8px 0 6px;">
|
|
<button class="btn-secondary" id="templatePresetDefault" type="button" onclick="applyTemplatePreset('default')">Preset: Default</button>
|
|
<button class="btn-secondary" id="templatePresetArchive" type="button" onclick="applyTemplatePreset('archive')">Preset: Archive</button>
|
|
<button class="btn-secondary" id="templatePresetClipper" type="button" onclick="applyTemplatePreset('clipper')">Preset: Clipper</button>
|
|
</div>
|
|
<div style="display: grid; gap: 8px; margin-top: 8px;">
|
|
<label id="vodTemplateLabel" style="font-size: 13px; color: var(--text-secondary);">VOD Template</label>
|
|
<input type="text" id="vodFilenameTemplate" placeholder="{title}.mp4" style="font-family: monospace;" oninput="validateFilenameTemplates()">
|
|
|
|
<label id="partsTemplateLabel" style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;">VOD Part Template</label>
|
|
<input type="text" id="partsFilenameTemplate" placeholder="{date}_Part{part_padded}.mp4" style="font-family: monospace;" oninput="validateFilenameTemplates()">
|
|
|
|
<label id="defaultClipTemplateLabel" style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;">Clip Template</label>
|
|
<input type="text" id="defaultClipFilenameTemplate" placeholder="{date}_{part}.mp4" style="font-family: monospace;" oninput="validateFilenameTemplates()">
|
|
</div>
|
|
<div id="filenameTemplateHint" style="color: #888; font-size: 12px; margin-top: 8px;">Platzhalter: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}</div>
|
|
<div id="filenameTemplateLint" style="font-size: 12px; margin-top: 6px; color: #8bc34a;">Template-Check: OK</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-card">
|
|
<h3 id="updateTitle">Updates</h3>
|
|
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.1.13</p>
|
|
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
|
</div>
|
|
|
|
<div class="settings-card">
|
|
<div class="form-row" style="align-items:center; justify-content:space-between; margin-bottom: 10px;">
|
|
<h3 id="preflightTitle" style="margin: 0;">System-Check</h3>
|
|
<span class="health-badge unknown" id="healthBadge">System: Unbekannt</span>
|
|
</div>
|
|
<div class="form-row" style="margin-bottom: 10px;">
|
|
<button class="btn-secondary" id="btnPreflightRun" onclick="runPreflight(false)">Check ausfuhren</button>
|
|
<button class="btn-secondary" id="btnPreflightFix" onclick="runPreflight(true)">Auto-Fix Tools</button>
|
|
</div>
|
|
<pre id="preflightResult" class="log-panel">Noch kein Check ausgefuhrt.</pre>
|
|
</div>
|
|
|
|
<div class="settings-card">
|
|
<h3 id="debugLogTitle">Live Debug-Log</h3>
|
|
<div class="form-row" style="margin-bottom: 10px; align-items: center;">
|
|
<button class="btn-secondary" id="btnRefreshLog" onclick="refreshDebugLog()">Aktualisieren</button>
|
|
<button class="btn-secondary" id="btnOpenDebugLogFile" onclick="openDebugLogFile()">Log-Datei oeffnen</button>
|
|
<label style="display:flex; align-items:center; gap:6px; font-size:13px; color: var(--text-secondary);">
|
|
<input type="checkbox" id="debugAutoRefresh" onchange="toggleDebugAutoRefresh(this.checked)">
|
|
<span id="autoRefreshText">Auto-Refresh</span>
|
|
</label>
|
|
</div>
|
|
<pre id="debugLogOutput" class="log-panel">Lade...</pre>
|
|
</div>
|
|
|
|
<div class="settings-card">
|
|
<h3 id="runtimeMetricsTitle">Runtime Metrics</h3>
|
|
<div class="form-row" style="margin-bottom: 10px; align-items: center;">
|
|
<button class="btn-secondary" id="btnRefreshMetrics" onclick="refreshRuntimeMetrics()">Aktualisieren</button>
|
|
<button class="btn-secondary" id="btnExportMetrics" onclick="exportRuntimeMetrics()">Export JSON</button>
|
|
<label style="display:flex; align-items:center; gap:6px; font-size:13px; color: var(--text-secondary);">
|
|
<input type="checkbox" id="runtimeMetricsAutoRefresh" onchange="toggleRuntimeMetricsAutoRefresh(this.checked)">
|
|
<span id="runtimeMetricsAutoRefreshText">Auto-Refresh</span>
|
|
</label>
|
|
</div>
|
|
<pre id="runtimeMetricsOutput" class="log-panel">Lade...</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-bar">
|
|
<div class="status-indicator">
|
|
<div class="status-dot" id="statusDot"></div>
|
|
<span id="statusText">Nicht verbunden</span>
|
|
</div>
|
|
<span id="statusBarQueueSummary" style="color: var(--text-secondary); font-size:12px; margin-left:auto; padding-right:12px;"></span>
|
|
<span id="versionText">v4.1.13</span>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script src="../dist/renderer-locale-de.js"></script>
|
|
<script src="../dist/renderer-locale-en.js"></script>
|
|
<script src="../dist/renderer-texts.js"></script>
|
|
<script src="../dist/renderer-shared.js"></script>
|
|
<script src="../dist/renderer-settings.js"></script>
|
|
<script src="../dist/renderer-streamers.js"></script>
|
|
<script src="../dist/renderer-queue.js"></script>
|
|
<script src="../dist/renderer-updates.js"></script>
|
|
<script src="../dist/renderer.js"></script>
|
|
</body>
|
|
</html>
|