Split renderer into typed feature modules (v3.7.5)
Move streamer, queue, settings and update logic into dedicated renderer files, introduce shared type declarations, and remove ts-nocheck so the UI codebase can continue migrating toward a maintainable TypeScript-first structure.
This commit is contained in:
parent
e29403505f
commit
eaa6d637ff
4
typescript-version/package-lock.json
generated
4
typescript-version/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "3.7.4",
|
"version": "3.7.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "3.7.4",
|
"version": "3.7.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "3.7.4",
|
"version": "3.7.5",
|
||||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"author": "xRangerDE",
|
"author": "xRangerDE",
|
||||||
|
|||||||
@ -335,7 +335,7 @@
|
|||||||
|
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3>Updates</h3>
|
<h3>Updates</h3>
|
||||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.7.4</p>
|
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.7.5</p>
|
||||||
<button class="btn-secondary" onclick="checkUpdate()">Nach Updates suchen</button>
|
<button class="btn-secondary" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -346,11 +346,16 @@
|
|||||||
<div class="status-dot" id="statusDot"></div>
|
<div class="status-dot" id="statusDot"></div>
|
||||||
<span id="statusText">Nicht verbunden</span>
|
<span id="statusText">Nicht verbunden</span>
|
||||||
</div>
|
</div>
|
||||||
<span id="versionText">v3.7.4</span>
|
<span id="versionText">v3.7.5</span>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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>
|
<script src="../dist/renderer.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// CONFIG & CONSTANTS
|
// CONFIG & CONSTANTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const APP_VERSION = '3.7.4';
|
const APP_VERSION = '3.7.5';
|
||||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
|
|||||||
128
typescript-version/src/renderer-globals.d.ts
vendored
Normal file
128
typescript-version/src/renderer-globals.d.ts
vendored
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
interface AppConfig {
|
||||||
|
client_id?: string;
|
||||||
|
client_secret?: string;
|
||||||
|
download_path?: string;
|
||||||
|
streamers?: string[];
|
||||||
|
theme?: string;
|
||||||
|
download_mode?: 'parts' | 'full';
|
||||||
|
part_minutes?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VOD {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
created_at: string;
|
||||||
|
duration: string;
|
||||||
|
thumbnail_url: string;
|
||||||
|
url: string;
|
||||||
|
view_count: number;
|
||||||
|
stream_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomClip {
|
||||||
|
startSec: number;
|
||||||
|
durationSec: number;
|
||||||
|
startPart: number;
|
||||||
|
filenameFormat: 'simple' | 'timestamp';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
date: string;
|
||||||
|
streamer: string;
|
||||||
|
duration_str: string;
|
||||||
|
status: 'pending' | 'downloading' | 'completed' | 'error';
|
||||||
|
progress: number;
|
||||||
|
currentPart?: number;
|
||||||
|
totalParts?: number;
|
||||||
|
speed?: string;
|
||||||
|
eta?: string;
|
||||||
|
downloadedBytes?: number;
|
||||||
|
totalBytes?: number;
|
||||||
|
customClip?: CustomClip;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadProgress {
|
||||||
|
id: string;
|
||||||
|
progress: number;
|
||||||
|
speed: string;
|
||||||
|
eta: string;
|
||||||
|
status: string;
|
||||||
|
currentPart?: number;
|
||||||
|
totalParts?: number;
|
||||||
|
downloadedBytes?: number;
|
||||||
|
totalBytes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoInfo {
|
||||||
|
duration: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
fps: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClipDialogData {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
date: string;
|
||||||
|
streamer: string;
|
||||||
|
duration: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateInfo {
|
||||||
|
version: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateDownloadProgress {
|
||||||
|
percent: number;
|
||||||
|
bytesPerSecond: number;
|
||||||
|
transferred: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiBridge {
|
||||||
|
getConfig(): Promise<AppConfig>;
|
||||||
|
saveConfig(config: Partial<AppConfig>): Promise<AppConfig>;
|
||||||
|
login(): Promise<boolean>;
|
||||||
|
getUserId(username: string): Promise<string | null>;
|
||||||
|
getVODs(userId: string): Promise<VOD[]>;
|
||||||
|
getQueue(): Promise<QueueItem[]>;
|
||||||
|
addToQueue(item: Omit<QueueItem, 'id' | 'status' | 'progress'>): Promise<QueueItem[]>;
|
||||||
|
removeFromQueue(id: string): Promise<QueueItem[]>;
|
||||||
|
clearCompleted(): Promise<QueueItem[]>;
|
||||||
|
startDownload(): Promise<boolean>;
|
||||||
|
cancelDownload(): Promise<boolean>;
|
||||||
|
isDownloading(): Promise<boolean>;
|
||||||
|
downloadClip(url: string): Promise<{ success: boolean; error?: string }>;
|
||||||
|
selectFolder(): Promise<string | null>;
|
||||||
|
selectVideoFile(): Promise<string | null>;
|
||||||
|
selectMultipleVideos(): Promise<string[] | null>;
|
||||||
|
saveVideoDialog(defaultName: string): Promise<string | null>;
|
||||||
|
openFolder(path: string): Promise<void>;
|
||||||
|
getVideoInfo(filePath: string): Promise<VideoInfo | null>;
|
||||||
|
extractFrame(filePath: string, timeSeconds: number): Promise<string | null>;
|
||||||
|
cutVideo(inputFile: string, startTime: number, endTime: number): Promise<{ success: boolean; outputFile: string | null }>;
|
||||||
|
mergeVideos(inputFiles: string[], outputFile: string): Promise<{ success: boolean; outputFile: string | null }>;
|
||||||
|
getVersion(): Promise<string>;
|
||||||
|
checkUpdate(): Promise<{ checking?: boolean; error?: boolean }>;
|
||||||
|
downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean }>;
|
||||||
|
installUpdate(): Promise<void>;
|
||||||
|
openExternal(url: string): Promise<void>;
|
||||||
|
onDownloadProgress(callback: (progress: DownloadProgress) => void): void;
|
||||||
|
onQueueUpdated(callback: (queue: QueueItem[]) => void): void;
|
||||||
|
onDownloadStarted(callback: () => void): void;
|
||||||
|
onDownloadFinished(callback: () => void): void;
|
||||||
|
onCutProgress(callback: (percent: number) => void): void;
|
||||||
|
onMergeProgress(callback: (percent: number) => void): void;
|
||||||
|
onUpdateAvailable(callback: (info: UpdateInfo) => void): void;
|
||||||
|
onUpdateDownloadProgress(callback: (progress: UpdateDownloadProgress) => void): void;
|
||||||
|
onUpdateDownloaded(callback: (info: UpdateInfo) => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
api: ApiBridge;
|
||||||
|
}
|
||||||
50
typescript-version/src/renderer-queue.ts
Normal file
50
typescript-version/src/renderer-queue.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
async function addToQueue(url: string, title: string, date: string, streamer: string, duration: string): Promise<void> {
|
||||||
|
queue = await window.api.addToQueue({
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
date,
|
||||||
|
streamer,
|
||||||
|
duration_str: duration
|
||||||
|
});
|
||||||
|
renderQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeFromQueue(id: string): Promise<void> {
|
||||||
|
queue = await window.api.removeFromQueue(id);
|
||||||
|
renderQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearCompleted(): Promise<void> {
|
||||||
|
queue = await window.api.clearCompleted();
|
||||||
|
renderQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQueue(): void {
|
||||||
|
const list = byId('queueList');
|
||||||
|
byId('queueCount').textContent = queue.length;
|
||||||
|
|
||||||
|
if (queue.length === 0) {
|
||||||
|
list.innerHTML = '<div style="color: var(--text-secondary); font-size: 12px; text-align: center; padding: 15px;">Keine Downloads in der Warteschlange</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = queue.map((item: QueueItem) => {
|
||||||
|
const isClip = item.customClip ? '* ' : '';
|
||||||
|
return `
|
||||||
|
<div class="queue-item">
|
||||||
|
<div class="status ${item.status}"></div>
|
||||||
|
<div class="title" title="${item.title}">${isClip}${item.title}</div>
|
||||||
|
<span class="remove" onclick="removeFromQueue('${item.id}')">x</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleDownload(): Promise<void> {
|
||||||
|
if (downloading) {
|
||||||
|
await window.api.cancelDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.api.startDownload();
|
||||||
|
}
|
||||||
55
typescript-version/src/renderer-settings.ts
Normal file
55
typescript-version/src/renderer-settings.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
async function connect(): Promise<void> {
|
||||||
|
updateStatus('Verbinde...', false);
|
||||||
|
const success = await window.api.login();
|
||||||
|
isConnected = success;
|
||||||
|
updateStatus(success ? 'Verbunden' : 'Verbindung fehlgeschlagen', success);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(text: string, connected: boolean): void {
|
||||||
|
byId('statusText').textContent = text;
|
||||||
|
const dot = byId('statusDot');
|
||||||
|
dot.classList.remove('connected', 'error');
|
||||||
|
dot.classList.add(connected ? 'connected' : 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettings(): Promise<void> {
|
||||||
|
const clientId = byId<HTMLInputElement>('clientId').value.trim();
|
||||||
|
const clientSecret = byId<HTMLInputElement>('clientSecret').value.trim();
|
||||||
|
const downloadPath = byId<HTMLInputElement>('downloadPath').value;
|
||||||
|
const downloadMode = byId<HTMLSelectElement>('downloadMode').value as 'parts' | 'full';
|
||||||
|
const partMinutes = parseInt(byId<HTMLInputElement>('partMinutes').value, 10) || 120;
|
||||||
|
|
||||||
|
config = await window.api.saveConfig({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
download_path: downloadPath,
|
||||||
|
download_mode: downloadMode,
|
||||||
|
part_minutes: partMinutes
|
||||||
|
});
|
||||||
|
|
||||||
|
await connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectFolder(): Promise<void> {
|
||||||
|
const folder = await window.api.selectFolder();
|
||||||
|
if (!folder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byId<HTMLInputElement>('downloadPath').value = folder;
|
||||||
|
config = await window.api.saveConfig({ download_path: folder });
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFolder(): void {
|
||||||
|
const folder = config.download_path;
|
||||||
|
if (!folder || typeof folder !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void window.api.openFolder(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTheme(theme: string): void {
|
||||||
|
document.body.className = `theme-${theme}`;
|
||||||
|
void window.api.saveConfig({ theme });
|
||||||
|
}
|
||||||
31
typescript-version/src/renderer-shared.ts
Normal file
31
typescript-version/src/renderer-shared.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
function byId<T = any>(id: string): T {
|
||||||
|
return document.getElementById(id) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function query<T = any>(selector: string): T {
|
||||||
|
return document.querySelector(selector) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryAll<T = any>(selector: string): T[] {
|
||||||
|
return Array.from(document.querySelectorAll(selector)) as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: AppConfig = {};
|
||||||
|
let currentStreamer: string | null = null;
|
||||||
|
let isConnected = false;
|
||||||
|
let downloading = false;
|
||||||
|
let queue: QueueItem[] = [];
|
||||||
|
|
||||||
|
let cutterFile: string | null = null;
|
||||||
|
let cutterVideoInfo: VideoInfo | null = null;
|
||||||
|
let cutterStartTime = 0;
|
||||||
|
let cutterEndTime = 0;
|
||||||
|
let isCutting = false;
|
||||||
|
|
||||||
|
let mergeFiles: string[] = [];
|
||||||
|
let isMerging = false;
|
||||||
|
|
||||||
|
let clipDialogData: ClipDialogData | null = null;
|
||||||
|
let clipTotalSeconds = 0;
|
||||||
|
|
||||||
|
let updateReady = false;
|
||||||
116
typescript-version/src/renderer-streamers.ts
Normal file
116
typescript-version/src/renderer-streamers.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
function renderStreamers(): void {
|
||||||
|
const list = byId('streamerList');
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
(config.streamers ?? []).forEach((streamer: string) => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'streamer-item' + (currentStreamer === streamer ? ' active' : '');
|
||||||
|
item.innerHTML = `
|
||||||
|
<span>${streamer}</span>
|
||||||
|
<span class="remove" onclick="event.stopPropagation(); removeStreamer('${streamer}')">x</span>
|
||||||
|
`;
|
||||||
|
item.onclick = () => {
|
||||||
|
void selectStreamer(streamer);
|
||||||
|
};
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addStreamer(): Promise<void> {
|
||||||
|
const input = byId<HTMLInputElement>('newStreamer');
|
||||||
|
const name = input.value.trim().toLowerCase();
|
||||||
|
if (!name || (config.streamers ?? []).includes(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.streamers = [...(config.streamers ?? []), name];
|
||||||
|
config = await window.api.saveConfig({ streamers: config.streamers });
|
||||||
|
input.value = '';
|
||||||
|
renderStreamers();
|
||||||
|
await selectStreamer(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeStreamer(name: string): Promise<void> {
|
||||||
|
config.streamers = (config.streamers ?? []).filter((s: string) => s !== name);
|
||||||
|
config = await window.api.saveConfig({ streamers: config.streamers });
|
||||||
|
renderStreamers();
|
||||||
|
|
||||||
|
if (currentStreamer !== name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStreamer = null;
|
||||||
|
byId('vodGrid').innerHTML = `
|
||||||
|
<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.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectStreamer(name: string): Promise<void> {
|
||||||
|
currentStreamer = name;
|
||||||
|
renderStreamers();
|
||||||
|
byId('pageTitle').textContent = name;
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
await connect();
|
||||||
|
if (!isConnected) {
|
||||||
|
byId('vodGrid').innerHTML = '<div class="empty-state"><h3>Nicht verbunden</h3><p>Bitte Twitch API Daten in den Einstellungen prufen.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byId('vodGrid').innerHTML = '<div class="empty-state"><p>Lade VODs...</p></div>';
|
||||||
|
|
||||||
|
const userId = await window.api.getUserId(name);
|
||||||
|
if (!userId) {
|
||||||
|
byId('vodGrid').innerHTML = '<div class="empty-state"><h3>Streamer nicht gefunden</h3></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vods = await window.api.getVODs(userId);
|
||||||
|
renderVODs(vods, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVODs(vods: VOD[] | null | undefined, streamer: string): void {
|
||||||
|
const grid = byId('vodGrid');
|
||||||
|
|
||||||
|
if (!vods || vods.length === 0) {
|
||||||
|
grid.innerHTML = '<div class="empty-state"><h3>Keine VODs gefunden</h3><p>Dieser Streamer hat keine VODs.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = vods.map((vod: VOD) => {
|
||||||
|
const thumb = vod.thumbnail_url.replace('%{width}', '320').replace('%{height}', '180');
|
||||||
|
const date = new Date(vod.created_at).toLocaleDateString('de-DE');
|
||||||
|
const escapedTitle = vod.title.replace(/'/g, "\\'").replace(/\"/g, '"');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="vod-card">
|
||||||
|
<img class="vod-thumbnail" src="${thumb}" alt="" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 320 180%22><rect fill=%22%23333%22 width=%22320%22 height=%22180%22/></svg>'">
|
||||||
|
<div class="vod-info">
|
||||||
|
<div class="vod-title">${vod.title}</div>
|
||||||
|
<div class="vod-meta">
|
||||||
|
<span>${date}</span>
|
||||||
|
<span>${vod.duration}</span>
|
||||||
|
<span>${vod.view_count.toLocaleString()} Views</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vod-actions">
|
||||||
|
<button class="vod-btn secondary" onclick="openClipDialog('${vod.url}', '${escapedTitle}', '${vod.created_at}', '${streamer}', '${vod.duration}')">Clip</button>
|
||||||
|
<button class="vod-btn primary" onclick="addToQueue('${vod.url}', '${escapedTitle}', '${vod.created_at}', '${streamer}', '${vod.duration}')">+ Queue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshVODs(): Promise<void> {
|
||||||
|
if (!currentStreamer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await selectStreamer(currentStreamer);
|
||||||
|
}
|
||||||
54
typescript-version/src/renderer-updates.ts
Normal file
54
typescript-version/src/renderer-updates.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
async function checkUpdateSilent(): Promise<void> {
|
||||||
|
await window.api.checkUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkUpdate(): Promise<void> {
|
||||||
|
await window.api.checkUpdate();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (byId('updateBanner').style.display !== 'flex') {
|
||||||
|
alert('Du hast die neueste Version!');
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadUpdate(): void {
|
||||||
|
if (updateReady) {
|
||||||
|
void window.api.installUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byId('updateButton').textContent = 'Wird heruntergeladen...';
|
||||||
|
byId('updateButton').disabled = true;
|
||||||
|
byId('updateProgress').style.display = 'block';
|
||||||
|
byId('updateProgressBar').classList.add('downloading');
|
||||||
|
void window.api.downloadUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.api.onUpdateAvailable((info: UpdateInfo) => {
|
||||||
|
byId('updateBanner').style.display = 'flex';
|
||||||
|
byId('updateText').textContent = `Version ${info.version} verfügbar!`;
|
||||||
|
byId('updateButton').textContent = 'Jetzt herunterladen';
|
||||||
|
});
|
||||||
|
|
||||||
|
window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
|
||||||
|
const bar = byId('updateProgressBar');
|
||||||
|
bar.classList.remove('downloading');
|
||||||
|
bar.style.width = progress.percent + '%';
|
||||||
|
|
||||||
|
const mb = (progress.transferred / 1024 / 1024).toFixed(1);
|
||||||
|
const totalMb = (progress.total / 1024 / 1024).toFixed(1);
|
||||||
|
byId('updateText').textContent = `Download: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
||||||
|
updateReady = true;
|
||||||
|
|
||||||
|
const bar = byId('updateProgressBar');
|
||||||
|
bar.classList.remove('downloading');
|
||||||
|
bar.style.width = '100%';
|
||||||
|
|
||||||
|
byId('updateText').textContent = `Version ${info.version} bereit zur Installation!`;
|
||||||
|
byId('updateButton').textContent = 'Jetzt installieren';
|
||||||
|
byId('updateButton').disabled = false;
|
||||||
|
});
|
||||||
@ -1,369 +1,181 @@
|
|||||||
// @ts-nocheck
|
async function init(): Promise<void> {
|
||||||
|
|
||||||
// State
|
|
||||||
let config = {};
|
|
||||||
let currentStreamer = null;
|
|
||||||
let isConnected = false;
|
|
||||||
let downloading = false;
|
|
||||||
let queue = [];
|
|
||||||
|
|
||||||
// Cutter State
|
|
||||||
let cutterFile = null;
|
|
||||||
let cutterVideoInfo = null;
|
|
||||||
let cutterStartTime = 0;
|
|
||||||
let cutterEndTime = 0;
|
|
||||||
let isCutting = false;
|
|
||||||
|
|
||||||
// Merge State
|
|
||||||
let mergeFiles = [];
|
|
||||||
let isMerging = false;
|
|
||||||
|
|
||||||
// Init
|
|
||||||
async function init() {
|
|
||||||
config = await window.api.getConfig();
|
config = await window.api.getConfig();
|
||||||
queue = await window.api.getQueue();
|
queue = await window.api.getQueue();
|
||||||
const version = await window.api.getVersion();
|
const version = await window.api.getVersion();
|
||||||
|
|
||||||
document.getElementById('versionText').textContent = `v${version}`;
|
byId('versionText').textContent = `v${version}`;
|
||||||
document.getElementById('versionInfo').textContent = `Version: v${version}`;
|
byId('versionInfo').textContent = `Version: v${version}`;
|
||||||
document.title = `Twitch VOD Manager v${version}`;
|
document.title = `Twitch VOD Manager v${version}`;
|
||||||
document.getElementById('clientId').value = config.client_id || '';
|
|
||||||
document.getElementById('clientSecret').value = config.client_secret || '';
|
|
||||||
document.getElementById('downloadPath').value = config.download_path || '';
|
|
||||||
document.getElementById('themeSelect').value = config.theme || 'twitch';
|
|
||||||
document.getElementById('downloadMode').value = config.download_mode || 'full';
|
|
||||||
document.getElementById('partMinutes').value = config.part_minutes || 120;
|
|
||||||
|
|
||||||
changeTheme(config.theme || 'twitch');
|
byId<HTMLInputElement>('clientId').value = config.client_id ?? '';
|
||||||
|
byId<HTMLInputElement>('clientSecret').value = config.client_secret ?? '';
|
||||||
|
byId<HTMLInputElement>('downloadPath').value = config.download_path ?? '';
|
||||||
|
byId<HTMLSelectElement>('themeSelect').value = config.theme ?? 'twitch';
|
||||||
|
byId<HTMLSelectElement>('downloadMode').value = config.download_mode ?? 'full';
|
||||||
|
byId<HTMLInputElement>('partMinutes').value = String(config.part_minutes ?? 120);
|
||||||
|
|
||||||
|
changeTheme(config.theme ?? 'twitch');
|
||||||
renderStreamers();
|
renderStreamers();
|
||||||
renderQueue();
|
renderQueue();
|
||||||
|
|
||||||
if (config.client_id && config.client_secret) {
|
if (config.client_id && config.client_secret) {
|
||||||
await connect();
|
await connect();
|
||||||
// Auto-select first streamer if available
|
|
||||||
if (config.streamers && config.streamers.length > 0) {
|
if (config.streamers && config.streamers.length > 0) {
|
||||||
selectStreamer(config.streamers[0]);
|
await selectStreamer(config.streamers[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event listeners
|
window.api.onQueueUpdated((q: QueueItem[]) => {
|
||||||
window.api.onQueueUpdated((q) => {
|
|
||||||
queue = q;
|
queue = q;
|
||||||
renderQueue();
|
renderQueue();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onDownloadProgress((progress) => {
|
window.api.onDownloadProgress((progress: DownloadProgress) => {
|
||||||
const item = queue.find(i => i.id === progress.id);
|
const item = queue.find((i: QueueItem) => i.id === progress.id);
|
||||||
if (item) {
|
if (!item) {
|
||||||
item.progress = progress.progress;
|
return;
|
||||||
renderQueue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.progress = progress.progress;
|
||||||
|
renderQueue();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onDownloadStarted(() => {
|
window.api.onDownloadStarted(() => {
|
||||||
downloading = true;
|
downloading = true;
|
||||||
document.getElementById('btnStart').textContent = 'Stoppen';
|
byId('btnStart').textContent = 'Stoppen';
|
||||||
document.getElementById('btnStart').classList.add('downloading');
|
byId('btnStart').classList.add('downloading');
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onDownloadFinished(() => {
|
window.api.onDownloadFinished(() => {
|
||||||
downloading = false;
|
downloading = false;
|
||||||
document.getElementById('btnStart').textContent = 'Start';
|
byId('btnStart').textContent = 'Start';
|
||||||
document.getElementById('btnStart').classList.remove('downloading');
|
byId('btnStart').classList.remove('downloading');
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onCutProgress((percent) => {
|
window.api.onCutProgress((percent: number) => {
|
||||||
document.getElementById('cutProgressBar').style.width = percent + '%';
|
byId('cutProgressBar').style.width = percent + '%';
|
||||||
document.getElementById('cutProgressText').textContent = Math.round(percent) + '%';
|
byId('cutProgressText').textContent = Math.round(percent) + '%';
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onMergeProgress((percent) => {
|
window.api.onMergeProgress((percent: number) => {
|
||||||
document.getElementById('mergeProgressBar').style.width = percent + '%';
|
byId('mergeProgressBar').style.width = percent + '%';
|
||||||
document.getElementById('mergeProgressText').textContent = Math.round(percent) + '%';
|
byId('mergeProgressText').textContent = Math.round(percent) + '%';
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(checkUpdateSilent, 3000);
|
setTimeout(() => {
|
||||||
|
void checkUpdateSilent();
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connect() {
|
function showTab(tab: string): void {
|
||||||
updateStatus('Verbinde...', false);
|
queryAll('.nav-item').forEach((i) => i.classList.remove('active'));
|
||||||
const success = await window.api.login();
|
queryAll('.tab-content').forEach((c) => c.classList.remove('active'));
|
||||||
isConnected = success;
|
|
||||||
updateStatus(success ? 'Verbunden' : 'Verbindung fehlgeschlagen', success);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateStatus(text, connected) {
|
query(`.nav-item[data-tab="${tab}"]`).classList.add('active');
|
||||||
document.getElementById('statusText').textContent = text;
|
byId(tab + 'Tab').classList.add('active');
|
||||||
const dot = document.getElementById('statusDot');
|
|
||||||
dot.classList.remove('connected', 'error');
|
|
||||||
dot.classList.add(connected ? 'connected' : 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation
|
const titles: Record<string, string> = {
|
||||||
function showTab(tab) {
|
|
||||||
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
|
||||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
||||||
|
|
||||||
document.querySelector(`.nav-item[data-tab="${tab}"]`).classList.add('active');
|
|
||||||
document.getElementById(tab + 'Tab').classList.add('active');
|
|
||||||
|
|
||||||
const titles = {
|
|
||||||
vods: 'VODs',
|
vods: 'VODs',
|
||||||
clips: 'Clips',
|
clips: 'Clips',
|
||||||
cutter: 'Video Cutter',
|
cutter: 'Video Cutter',
|
||||||
merge: 'Videos Zusammenfugen',
|
merge: 'Videos Zusammenfugen',
|
||||||
settings: 'Einstellungen'
|
settings: 'Einstellungen'
|
||||||
};
|
};
|
||||||
document.getElementById('pageTitle').textContent = currentStreamer || titles[tab];
|
|
||||||
|
byId('pageTitle').textContent = currentStreamer || titles[tab] || 'Twitch VOD Manager';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Streamers
|
function parseDurationToSeconds(durStr: string): number {
|
||||||
function renderStreamers() {
|
|
||||||
const list = document.getElementById('streamerList');
|
|
||||||
list.innerHTML = '';
|
|
||||||
|
|
||||||
(config.streamers || []).forEach(streamer => {
|
|
||||||
const item = document.createElement('div');
|
|
||||||
item.className = 'streamer-item' + (currentStreamer === streamer ? ' active' : '');
|
|
||||||
item.innerHTML = `
|
|
||||||
<span>${streamer}</span>
|
|
||||||
<span class="remove" onclick="event.stopPropagation(); removeStreamer('${streamer}')">x</span>
|
|
||||||
`;
|
|
||||||
item.onclick = () => selectStreamer(streamer);
|
|
||||||
list.appendChild(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addStreamer() {
|
|
||||||
const input = document.getElementById('newStreamer');
|
|
||||||
const name = input.value.trim().toLowerCase();
|
|
||||||
if (!name || (config.streamers || []).includes(name)) return;
|
|
||||||
|
|
||||||
config.streamers = [...(config.streamers || []), name];
|
|
||||||
config = await window.api.saveConfig({ streamers: config.streamers });
|
|
||||||
input.value = '';
|
|
||||||
renderStreamers();
|
|
||||||
selectStreamer(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeStreamer(name) {
|
|
||||||
config.streamers = (config.streamers || []).filter(s => s !== name);
|
|
||||||
config = await window.api.saveConfig({ streamers: config.streamers });
|
|
||||||
renderStreamers();
|
|
||||||
if (currentStreamer === name) {
|
|
||||||
currentStreamer = null;
|
|
||||||
document.getElementById('vodGrid').innerHTML = `
|
|
||||||
<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.</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectStreamer(name) {
|
|
||||||
currentStreamer = name;
|
|
||||||
renderStreamers();
|
|
||||||
document.getElementById('pageTitle').textContent = name;
|
|
||||||
|
|
||||||
if (!isConnected) {
|
|
||||||
await connect();
|
|
||||||
if (!isConnected) {
|
|
||||||
document.getElementById('vodGrid').innerHTML = '<div class="empty-state"><h3>Nicht verbunden</h3><p>Bitte Twitch API Daten in den Einstellungen prufen.</p></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('vodGrid').innerHTML = '<div class="empty-state"><p>Lade VODs...</p></div>';
|
|
||||||
|
|
||||||
const userId = await window.api.getUserId(name);
|
|
||||||
if (!userId) {
|
|
||||||
document.getElementById('vodGrid').innerHTML = '<div class="empty-state"><h3>Streamer nicht gefunden</h3></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vods = await window.api.getVODs(userId);
|
|
||||||
renderVODs(vods, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderVODs(vods, streamer) {
|
|
||||||
const grid = document.getElementById('vodGrid');
|
|
||||||
|
|
||||||
if (!vods || vods.length === 0) {
|
|
||||||
grid.innerHTML = '<div class="empty-state"><h3>Keine VODs gefunden</h3><p>Dieser Streamer hat keine VODs.</p></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
grid.innerHTML = vods.map(vod => {
|
|
||||||
const thumb = vod.thumbnail_url.replace('%{width}', '320').replace('%{height}', '180');
|
|
||||||
const date = new Date(vod.created_at).toLocaleDateString('de-DE');
|
|
||||||
const escapedTitle = vod.title.replace(/'/g, "\\'").replace(/"/g, """);
|
|
||||||
return `
|
|
||||||
<div class="vod-card">
|
|
||||||
<img class="vod-thumbnail" src="${thumb}" alt="" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 320 180%22><rect fill=%22%23333%22 width=%22320%22 height=%22180%22/></svg>'">
|
|
||||||
<div class="vod-info">
|
|
||||||
<div class="vod-title">${vod.title}</div>
|
|
||||||
<div class="vod-meta">
|
|
||||||
<span>${date}</span>
|
|
||||||
<span>${vod.duration}</span>
|
|
||||||
<span>${vod.view_count.toLocaleString()} Views</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="vod-actions">
|
|
||||||
<button class="vod-btn secondary" onclick="openClipDialog('${vod.url}', '${escapedTitle}', '${vod.created_at}', '${streamer}', '${vod.duration}')">Clip</button>
|
|
||||||
<button class="vod-btn primary" onclick="addToQueue('${vod.url}', '${escapedTitle}', '${vod.created_at}', '${streamer}', '${vod.duration}')">+ Queue</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshVODs() {
|
|
||||||
if (currentStreamer) {
|
|
||||||
await selectStreamer(currentStreamer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue
|
|
||||||
async function addToQueue(url, title, date, streamer, duration) {
|
|
||||||
queue = await window.api.addToQueue({ url, title, date, streamer, duration_str: duration });
|
|
||||||
renderQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeFromQueue(id) {
|
|
||||||
queue = await window.api.removeFromQueue(id);
|
|
||||||
renderQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearCompleted() {
|
|
||||||
queue = await window.api.clearCompleted();
|
|
||||||
renderQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderQueue() {
|
|
||||||
const list = document.getElementById('queueList');
|
|
||||||
document.getElementById('queueCount').textContent = queue.length;
|
|
||||||
|
|
||||||
if (queue.length === 0) {
|
|
||||||
list.innerHTML = '<div style="color: var(--text-secondary); font-size: 12px; text-align: center; padding: 15px;">Keine Downloads in der Warteschlange</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.innerHTML = queue.map(item => {
|
|
||||||
const isClip = item.customClip ? '* ' : '';
|
|
||||||
return `
|
|
||||||
<div class="queue-item">
|
|
||||||
<div class="status ${item.status}"></div>
|
|
||||||
<div class="title" title="${item.title}">${isClip}${item.title}</div>
|
|
||||||
<span class="remove" onclick="removeFromQueue('${item.id}')">x</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleDownload() {
|
|
||||||
if (downloading) {
|
|
||||||
await window.api.cancelDownload();
|
|
||||||
} else {
|
|
||||||
await window.api.startDownload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clip Dialog
|
|
||||||
let clipDialogData = null;
|
|
||||||
let clipTotalSeconds = 0;
|
|
||||||
|
|
||||||
function parseDurationToSeconds(durStr) {
|
|
||||||
let seconds = 0;
|
let seconds = 0;
|
||||||
const hours = durStr.match(/(\d+)h/);
|
const hours = durStr.match(/(\d+)h/);
|
||||||
const minutes = durStr.match(/(\d+)m/);
|
const minutes = durStr.match(/(\d+)m/);
|
||||||
const secs = durStr.match(/(\d+)s/);
|
const secs = durStr.match(/(\d+)s/);
|
||||||
if (hours) seconds += parseInt(hours[1]) * 3600;
|
|
||||||
if (minutes) seconds += parseInt(minutes[1]) * 60;
|
if (hours) seconds += parseInt(hours[1], 10) * 3600;
|
||||||
if (secs) seconds += parseInt(secs[1]);
|
if (minutes) seconds += parseInt(minutes[1], 10) * 60;
|
||||||
|
if (secs) seconds += parseInt(secs[1], 10);
|
||||||
|
|
||||||
return seconds;
|
return seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSecondsToTime(seconds) {
|
function formatSecondsToTime(seconds: number): string {
|
||||||
const h = Math.floor(seconds / 3600);
|
const h = Math.floor(seconds / 3600);
|
||||||
const m = Math.floor((seconds % 3600) / 60);
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
const s = Math.floor(seconds % 60);
|
const s = Math.floor(seconds % 60);
|
||||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSecondsToTimeDashed(seconds) {
|
function formatSecondsToTimeDashed(seconds: number): string {
|
||||||
const h = Math.floor(seconds / 3600);
|
const h = Math.floor(seconds / 3600);
|
||||||
const m = Math.floor((seconds % 3600) / 60);
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
const s = Math.floor(seconds % 60);
|
const s = Math.floor(seconds % 60);
|
||||||
return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`;
|
return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTimeToSeconds(timeStr) {
|
function parseTimeToSeconds(timeStr: string): number {
|
||||||
const parts = timeStr.split(':').map(p => parseInt(p) || 0);
|
const parts = timeStr.split(':').map((p: string) => parseInt(p, 10) || 0);
|
||||||
if (parts.length === 3) {
|
if (parts.length === 3) {
|
||||||
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openClipDialog(url, title, date, streamer, duration) {
|
function openClipDialog(url: string, title: string, date: string, streamer: string, duration: string): void {
|
||||||
clipDialogData = { url, title, date, streamer, duration };
|
clipDialogData = { url, title, date, streamer, duration };
|
||||||
clipTotalSeconds = parseDurationToSeconds(duration);
|
clipTotalSeconds = parseDurationToSeconds(duration);
|
||||||
|
|
||||||
document.getElementById('clipDialogTitle').textContent = 'Clip zuschneiden (' + duration + ')';
|
byId('clipDialogTitle').textContent = `Clip zuschneiden (${duration})`;
|
||||||
|
byId<HTMLInputElement>('clipStartSlider').max = String(clipTotalSeconds);
|
||||||
|
byId<HTMLInputElement>('clipEndSlider').max = String(clipTotalSeconds);
|
||||||
|
byId<HTMLInputElement>('clipStartSlider').value = '0';
|
||||||
|
byId<HTMLInputElement>('clipEndSlider').value = String(Math.min(60, clipTotalSeconds));
|
||||||
|
|
||||||
// Setup sliders
|
byId<HTMLInputElement>('clipStartTime').value = '00:00:00';
|
||||||
document.getElementById('clipStartSlider').max = clipTotalSeconds;
|
byId<HTMLInputElement>('clipEndTime').value = formatSecondsToTime(Math.min(60, clipTotalSeconds));
|
||||||
document.getElementById('clipEndSlider').max = clipTotalSeconds;
|
byId<HTMLInputElement>('clipStartPart').value = '';
|
||||||
document.getElementById('clipStartSlider').value = 0;
|
|
||||||
document.getElementById('clipEndSlider').value = Math.min(60, clipTotalSeconds);
|
|
||||||
|
|
||||||
document.getElementById('clipStartTime').value = '00:00:00';
|
|
||||||
document.getElementById('clipEndTime').value = formatSecondsToTime(Math.min(60, clipTotalSeconds));
|
|
||||||
document.getElementById('clipStartPart').value = '';
|
|
||||||
|
|
||||||
updateClipDuration();
|
updateClipDuration();
|
||||||
updateFilenameExamples();
|
updateFilenameExamples();
|
||||||
document.getElementById('clipModal').classList.add('show');
|
byId('clipModal').classList.add('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeClipDialog() {
|
function closeClipDialog(): void {
|
||||||
document.getElementById('clipModal').classList.remove('show');
|
byId('clipModal').classList.remove('show');
|
||||||
clipDialogData = null;
|
clipDialogData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFromSlider(which) {
|
function updateFromSlider(which: string): void {
|
||||||
const startSlider = document.getElementById('clipStartSlider');
|
const startSlider = byId<HTMLInputElement>('clipStartSlider');
|
||||||
const endSlider = document.getElementById('clipEndSlider');
|
const endSlider = byId<HTMLInputElement>('clipEndSlider');
|
||||||
|
|
||||||
if (which === 'start') {
|
if (which === 'start') {
|
||||||
document.getElementById('clipStartTime').value = formatSecondsToTime(parseInt(startSlider.value));
|
byId<HTMLInputElement>('clipStartTime').value = formatSecondsToTime(parseInt(startSlider.value, 10));
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('clipEndTime').value = formatSecondsToTime(parseInt(endSlider.value));
|
byId<HTMLInputElement>('clipEndTime').value = formatSecondsToTime(parseInt(endSlider.value, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClipDuration();
|
updateClipDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFromInput(which) {
|
function updateFromInput(which: string): void {
|
||||||
const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value);
|
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
||||||
const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value);
|
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
||||||
|
|
||||||
if (which === 'start') {
|
if (which === 'start') {
|
||||||
document.getElementById('clipStartSlider').value = Math.max(0, Math.min(startSec, clipTotalSeconds));
|
byId<HTMLInputElement>('clipStartSlider').value = String(Math.max(0, Math.min(startSec, clipTotalSeconds)));
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('clipEndSlider').value = Math.max(0, Math.min(endSec, clipTotalSeconds));
|
byId<HTMLInputElement>('clipEndSlider').value = String(Math.max(0, Math.min(endSec, clipTotalSeconds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClipDuration();
|
updateClipDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateClipDuration() {
|
function updateClipDuration(): void {
|
||||||
const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value);
|
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
||||||
const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value);
|
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
||||||
const duration = endSec - startSec;
|
const duration = endSec - startSec;
|
||||||
const durationDisplay = document.getElementById('clipDurationDisplay');
|
const durationDisplay = byId('clipDurationDisplay');
|
||||||
|
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
durationDisplay.textContent = formatSecondsToTime(duration);
|
durationDisplay.textContent = formatSecondsToTime(duration);
|
||||||
@ -376,27 +188,31 @@ function updateClipDuration() {
|
|||||||
updateFilenameExamples();
|
updateFilenameExamples();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFilenameExamples() {
|
function updateFilenameExamples(): void {
|
||||||
if (!clipDialogData) return;
|
if (!clipDialogData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const date = new Date(clipDialogData.date);
|
const date = new Date(clipDialogData.date);
|
||||||
const dateStr = `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getFullYear()}`;
|
const dateStr = `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getFullYear()}`;
|
||||||
const partNum = document.getElementById('clipStartPart').value || '1';
|
const partNum = byId<HTMLInputElement>('clipStartPart').value || '1';
|
||||||
const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value);
|
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
||||||
const timeStr = formatSecondsToTimeDashed(startSec);
|
const timeStr = formatSecondsToTimeDashed(startSec);
|
||||||
|
|
||||||
document.getElementById('formatSimple').textContent = `${dateStr}_${partNum}.mp4 (Standard)`;
|
byId('formatSimple').textContent = `${dateStr}_${partNum}.mp4 (Standard)`;
|
||||||
document.getElementById('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 (mit Zeitstempel)`;
|
byId('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 (mit Zeitstempel)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmClipDialog() {
|
async function confirmClipDialog(): Promise<void> {
|
||||||
if (!clipDialogData) return;
|
if (!clipDialogData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value);
|
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
||||||
const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value);
|
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
||||||
const startPartStr = document.getElementById('clipStartPart').value.trim();
|
const startPartStr = byId<HTMLInputElement>('clipStartPart').value.trim();
|
||||||
const startPart = startPartStr ? parseInt(startPartStr) : 1;
|
const startPart = startPartStr ? parseInt(startPartStr, 10) : 1;
|
||||||
const filenameFormat = document.querySelector('input[name="filenameFormat"]:checked').value;
|
const filenameFormat = query<HTMLInputElement>('input[name="filenameFormat"]:checked').value as 'simple' | 'timestamp';
|
||||||
|
|
||||||
if (endSec <= startSec) {
|
if (endSec <= startSec) {
|
||||||
alert('Endzeit muss grosser als Startzeit sein!');
|
alert('Endzeit muss grosser als Startzeit sein!');
|
||||||
@ -417,10 +233,10 @@ async function confirmClipDialog() {
|
|||||||
streamer: clipDialogData.streamer,
|
streamer: clipDialogData.streamer,
|
||||||
duration_str: clipDialogData.duration,
|
duration_str: clipDialogData.duration,
|
||||||
customClip: {
|
customClip: {
|
||||||
startSec: startSec,
|
startSec,
|
||||||
durationSec: durationSec,
|
durationSec,
|
||||||
startPart: startPart,
|
startPart,
|
||||||
filenameFormat: filenameFormat
|
filenameFormat
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -428,47 +244,10 @@ async function confirmClipDialog() {
|
|||||||
closeClipDialog();
|
closeClipDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings
|
async function downloadClip(): Promise<void> {
|
||||||
async function saveSettings() {
|
const url = byId<HTMLInputElement>('clipUrl').value.trim();
|
||||||
const clientId = document.getElementById('clientId').value.trim();
|
const status = byId('clipStatus');
|
||||||
const clientSecret = document.getElementById('clientSecret').value.trim();
|
const btn = byId('btnClip');
|
||||||
const downloadPath = document.getElementById('downloadPath').value;
|
|
||||||
const downloadMode = document.getElementById('downloadMode').value;
|
|
||||||
const partMinutes = parseInt(document.getElementById('partMinutes').value);
|
|
||||||
|
|
||||||
config = await window.api.saveConfig({
|
|
||||||
client_id: clientId,
|
|
||||||
client_secret: clientSecret,
|
|
||||||
download_path: downloadPath,
|
|
||||||
download_mode: downloadMode,
|
|
||||||
part_minutes: partMinutes
|
|
||||||
});
|
|
||||||
|
|
||||||
await connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectFolder() {
|
|
||||||
const folder = await window.api.selectFolder();
|
|
||||||
if (folder) {
|
|
||||||
document.getElementById('downloadPath').value = folder;
|
|
||||||
config = await window.api.saveConfig({ download_path: folder });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFolder() {
|
|
||||||
window.api.openFolder(config.download_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeTheme(theme) {
|
|
||||||
document.body.className = `theme-${theme}`;
|
|
||||||
window.api.saveConfig({ theme });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clips
|
|
||||||
async function downloadClip() {
|
|
||||||
const url = document.getElementById('clipUrl').value.trim();
|
|
||||||
const status = document.getElementById('clipStatus');
|
|
||||||
const btn = document.getElementById('btnClip');
|
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
status.textContent = 'Bitte URL eingeben';
|
status.textContent = 'Bitte URL eingeben';
|
||||||
@ -489,19 +268,21 @@ async function downloadClip() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
status.textContent = 'Download erfolgreich!';
|
status.textContent = 'Download erfolgreich!';
|
||||||
status.className = 'clip-status success';
|
status.className = 'clip-status success';
|
||||||
} else {
|
return;
|
||||||
status.textContent = 'Fehler: ' + result.error;
|
|
||||||
status.className = 'clip-status error';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status.textContent = 'Fehler: ' + (result.error || 'Unbekannter Fehler');
|
||||||
|
status.className = 'clip-status error';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video Cutter
|
async function selectCutterVideo(): Promise<void> {
|
||||||
async function selectCutterVideo() {
|
|
||||||
const filePath = await window.api.selectVideoFile();
|
const filePath = await window.api.selectVideoFile();
|
||||||
if (!filePath) return;
|
if (!filePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cutterFile = filePath;
|
cutterFile = filePath;
|
||||||
document.getElementById('cutterFilePath').value = filePath;
|
byId<HTMLInputElement>('cutterFilePath').value = filePath;
|
||||||
|
|
||||||
const info = await window.api.getVideoInfo(filePath);
|
const info = await window.api.getVideoInfo(filePath);
|
||||||
if (!info) {
|
if (!info) {
|
||||||
@ -513,41 +294,44 @@ async function selectCutterVideo() {
|
|||||||
cutterStartTime = 0;
|
cutterStartTime = 0;
|
||||||
cutterEndTime = info.duration;
|
cutterEndTime = info.duration;
|
||||||
|
|
||||||
document.getElementById('cutterInfo').style.display = 'flex';
|
byId('cutterInfo').style.display = 'flex';
|
||||||
document.getElementById('timelineContainer').style.display = 'block';
|
byId('timelineContainer').style.display = 'block';
|
||||||
document.getElementById('btnCut').disabled = false;
|
byId('btnCut').disabled = false;
|
||||||
|
|
||||||
document.getElementById('infoDuration').textContent = formatTime(info.duration);
|
byId('infoDuration').textContent = formatTime(info.duration);
|
||||||
document.getElementById('infoResolution').textContent = `${info.width}x${info.height}`;
|
byId('infoResolution').textContent = `${info.width}x${info.height}`;
|
||||||
document.getElementById('infoFps').textContent = Math.round(info.fps);
|
byId('infoFps').textContent = Math.round(info.fps);
|
||||||
document.getElementById('infoSelection').textContent = formatTime(info.duration);
|
byId('infoSelection').textContent = formatTime(info.duration);
|
||||||
|
|
||||||
document.getElementById('startTime').value = '00:00:00';
|
byId<HTMLInputElement>('startTime').value = '00:00:00';
|
||||||
document.getElementById('endTime').value = formatTime(info.duration);
|
byId<HTMLInputElement>('endTime').value = formatTime(info.duration);
|
||||||
|
|
||||||
updateTimeline();
|
updateTimeline();
|
||||||
await updatePreview(0);
|
await updatePreview(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(seconds) {
|
function formatTime(seconds: number): string {
|
||||||
const h = Math.floor(seconds / 3600);
|
const h = Math.floor(seconds / 3600);
|
||||||
const m = Math.floor((seconds % 3600) / 60);
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
const s = Math.floor(seconds % 60);
|
const s = Math.floor(seconds % 60);
|
||||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTime(timeStr) {
|
function parseTime(timeStr: string): number {
|
||||||
const parts = timeStr.split(':').map(p => parseInt(p) || 0);
|
const parts = timeStr.split(':').map((p: string) => parseInt(p, 10) || 0);
|
||||||
if (parts.length === 3) {
|
if (parts.length === 3) {
|
||||||
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTimeline() {
|
function updateTimeline(): void {
|
||||||
if (!cutterVideoInfo) return;
|
if (!cutterVideoInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const selection = document.getElementById('timelineSelection');
|
const selection = byId('timelineSelection');
|
||||||
const startPercent = (cutterStartTime / cutterVideoInfo.duration) * 100;
|
const startPercent = (cutterStartTime / cutterVideoInfo.duration) * 100;
|
||||||
const endPercent = (cutterEndTime / cutterVideoInfo.duration) * 100;
|
const endPercent = (cutterEndTime / cutterVideoInfo.duration) * 100;
|
||||||
|
|
||||||
@ -555,12 +339,12 @@ function updateTimeline() {
|
|||||||
selection.style.width = (endPercent - startPercent) + '%';
|
selection.style.width = (endPercent - startPercent) + '%';
|
||||||
|
|
||||||
const duration = cutterEndTime - cutterStartTime;
|
const duration = cutterEndTime - cutterStartTime;
|
||||||
document.getElementById('infoSelection').textContent = formatTime(duration);
|
byId('infoSelection').textContent = formatTime(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTimeFromInput() {
|
function updateTimeFromInput(): void {
|
||||||
const startStr = document.getElementById('startTime').value;
|
const startStr = byId<HTMLInputElement>('startTime').value;
|
||||||
const endStr = document.getElementById('endTime').value;
|
const endStr = byId<HTMLInputElement>('endTime').value;
|
||||||
|
|
||||||
cutterStartTime = Math.max(0, parseTime(startStr));
|
cutterStartTime = Math.max(0, parseTime(startStr));
|
||||||
cutterEndTime = Math.min(cutterVideoInfo?.duration || 0, parseTime(endStr));
|
cutterEndTime = Math.min(cutterVideoInfo?.duration || 0, parseTime(endStr));
|
||||||
@ -572,66 +356,75 @@ function updateTimeFromInput() {
|
|||||||
updateTimeline();
|
updateTimeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seekTimeline(event) {
|
async function seekTimeline(event: MouseEvent): Promise<void> {
|
||||||
if (!cutterVideoInfo) return;
|
if (!cutterVideoInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const timeline = document.getElementById('timeline');
|
const timeline = byId<HTMLElement>('timeline');
|
||||||
const rect = timeline.getBoundingClientRect();
|
const rect = timeline.getBoundingClientRect();
|
||||||
const percent = (event.clientX - rect.left) / rect.width;
|
const percent = (event.clientX - rect.left) / rect.width;
|
||||||
const time = percent * cutterVideoInfo.duration;
|
const time = percent * cutterVideoInfo.duration;
|
||||||
|
|
||||||
document.getElementById('timelineCurrent').style.left = (percent * 100) + '%';
|
byId('timelineCurrent').style.left = (percent * 100) + '%';
|
||||||
await updatePreview(time);
|
await updatePreview(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updatePreview(time) {
|
async function updatePreview(time: number): Promise<void> {
|
||||||
if (!cutterFile) return;
|
if (!cutterFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const preview = document.getElementById('cutterPreview');
|
const preview = byId('cutterPreview');
|
||||||
preview.innerHTML = '<div class="placeholder"><p>Lade Vorschau...</p></div>';
|
preview.innerHTML = '<div class="placeholder"><p>Lade Vorschau...</p></div>';
|
||||||
|
|
||||||
const frame = await window.api.extractFrame(cutterFile, time);
|
const frame = await window.api.extractFrame(cutterFile, time);
|
||||||
if (frame) {
|
if (frame) {
|
||||||
preview.innerHTML = `<img src="${frame}" alt="Preview">`;
|
preview.innerHTML = `<img src="${frame}" alt="Preview">`;
|
||||||
} else {
|
return;
|
||||||
preview.innerHTML = '<div class="placeholder"><p>Vorschau nicht verfugbar</p></div>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preview.innerHTML = '<div class="placeholder"><p>Vorschau nicht verfugbar</p></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startCutting() {
|
async function startCutting(): Promise<void> {
|
||||||
if (!cutterFile || isCutting) return;
|
if (!cutterFile || isCutting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isCutting = true;
|
isCutting = true;
|
||||||
document.getElementById('btnCut').disabled = true;
|
byId('btnCut').disabled = true;
|
||||||
document.getElementById('btnCut').textContent = 'Schneidet...';
|
byId('btnCut').textContent = 'Schneidet...';
|
||||||
document.getElementById('cutProgress').classList.add('show');
|
byId('cutProgress').classList.add('show');
|
||||||
|
|
||||||
const result = await window.api.cutVideo(cutterFile, cutterStartTime, cutterEndTime);
|
const result = await window.api.cutVideo(cutterFile, cutterStartTime, cutterEndTime);
|
||||||
|
|
||||||
isCutting = false;
|
isCutting = false;
|
||||||
document.getElementById('btnCut').disabled = false;
|
byId('btnCut').disabled = false;
|
||||||
document.getElementById('btnCut').textContent = 'Schneiden';
|
byId('btnCut').textContent = 'Schneiden';
|
||||||
document.getElementById('cutProgress').classList.remove('show');
|
byId('cutProgress').classList.remove('show');
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert('Video erfolgreich geschnitten!\n\n' + result.outputFile);
|
alert('Video erfolgreich geschnitten!\n\n' + result.outputFile);
|
||||||
} else {
|
return;
|
||||||
alert('Fehler beim Schneiden des Videos.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alert('Fehler beim Schneiden des Videos.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge Videos
|
async function addMergeFiles(): Promise<void> {
|
||||||
async function addMergeFiles() {
|
|
||||||
const files = await window.api.selectMultipleVideos();
|
const files = await window.api.selectMultipleVideos();
|
||||||
if (files && files.length > 0) {
|
if (!files || files.length === 0) {
|
||||||
mergeFiles = [...mergeFiles, ...files];
|
return;
|
||||||
renderMergeFiles();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mergeFiles = [...mergeFiles, ...files];
|
||||||
|
renderMergeFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMergeFiles() {
|
function renderMergeFiles(): void {
|
||||||
const list = document.getElementById('mergeFileList');
|
const list = byId('mergeFileList');
|
||||||
document.getElementById('btnMerge').disabled = mergeFiles.length < 2;
|
byId('btnMerge').disabled = mergeFiles.length < 2;
|
||||||
|
|
||||||
if (mergeFiles.length === 0) {
|
if (mergeFiles.length === 0) {
|
||||||
list.innerHTML = `
|
list.innerHTML = `
|
||||||
@ -643,7 +436,7 @@ function renderMergeFiles() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.innerHTML = mergeFiles.map((file, index) => {
|
list.innerHTML = mergeFiles.map((file: string, index: number) => {
|
||||||
const name = file.split(/[/\\]/).pop();
|
const name = file.split(/[/\\]/).pop();
|
||||||
return `
|
return `
|
||||||
<div class="file-item" draggable="true" data-index="${index}">
|
<div class="file-item" draggable="true" data-index="${index}">
|
||||||
@ -659,9 +452,11 @@ function renderMergeFiles() {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveMergeFile(index, direction) {
|
function moveMergeFile(index: number, direction: number): void {
|
||||||
const newIndex = index + direction;
|
const newIndex = index + direction;
|
||||||
if (newIndex < 0 || newIndex >= mergeFiles.length) return;
|
if (newIndex < 0 || newIndex >= mergeFiles.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const temp = mergeFiles[index];
|
const temp = mergeFiles[index];
|
||||||
mergeFiles[index] = mergeFiles[newIndex];
|
mergeFiles[index] = mergeFiles[newIndex];
|
||||||
@ -669,97 +464,41 @@ function moveMergeFile(index, direction) {
|
|||||||
renderMergeFiles();
|
renderMergeFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeMergeFile(index) {
|
function removeMergeFile(index: number): void {
|
||||||
mergeFiles.splice(index, 1);
|
mergeFiles.splice(index, 1);
|
||||||
renderMergeFiles();
|
renderMergeFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startMerging() {
|
async function startMerging(): Promise<void> {
|
||||||
if (mergeFiles.length < 2 || isMerging) return;
|
if (mergeFiles.length < 2 || isMerging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const outputFile = await window.api.saveVideoDialog('merged_video.mp4');
|
const outputFile = await window.api.saveVideoDialog('merged_video.mp4');
|
||||||
if (!outputFile) return;
|
if (!outputFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isMerging = true;
|
isMerging = true;
|
||||||
document.getElementById('btnMerge').disabled = true;
|
byId('btnMerge').disabled = true;
|
||||||
document.getElementById('btnMerge').textContent = 'Zusammenfugen...';
|
byId('btnMerge').textContent = 'Zusammenfugen...';
|
||||||
document.getElementById('mergeProgress').classList.add('show');
|
byId('mergeProgress').classList.add('show');
|
||||||
|
|
||||||
const result = await window.api.mergeVideos(mergeFiles, outputFile);
|
const result = await window.api.mergeVideos(mergeFiles, outputFile);
|
||||||
|
|
||||||
isMerging = false;
|
isMerging = false;
|
||||||
document.getElementById('btnMerge').disabled = false;
|
byId('btnMerge').disabled = false;
|
||||||
document.getElementById('btnMerge').textContent = 'Zusammenfugen';
|
byId('btnMerge').textContent = 'Zusammenfugen';
|
||||||
document.getElementById('mergeProgress').classList.remove('show');
|
byId('mergeProgress').classList.remove('show');
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert('Videos erfolgreich zusammengefugt!\n\n' + result.outputFile);
|
alert('Videos erfolgreich zusammengefugt!\n\n' + result.outputFile);
|
||||||
mergeFiles = [];
|
mergeFiles = [];
|
||||||
renderMergeFiles();
|
renderMergeFiles();
|
||||||
} else {
|
return;
|
||||||
alert('Fehler beim Zusammenfugen der Videos.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alert('Fehler beim Zusammenfugen der Videos.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates - wird jetzt automatisch vom main process via Events gesteuert
|
void init();
|
||||||
async function checkUpdateSilent() {
|
|
||||||
// Auto-Updater läuft automatisch beim App-Start
|
|
||||||
await window.api.checkUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkUpdate() {
|
|
||||||
// Manueller Check - zeigt Info wenn kein Update
|
|
||||||
await window.api.checkUpdate();
|
|
||||||
// Wenn kein Update, kommt kein Event - kurz warten dann Info zeigen
|
|
||||||
setTimeout(() => {
|
|
||||||
if (document.getElementById('updateBanner').style.display !== 'flex') {
|
|
||||||
alert('Du hast die neueste Version!');
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
let updateReady = false;
|
|
||||||
|
|
||||||
function downloadUpdate() {
|
|
||||||
if (updateReady) {
|
|
||||||
// Update ist heruntergeladen - installieren
|
|
||||||
window.api.installUpdate();
|
|
||||||
} else {
|
|
||||||
// Update herunterladen
|
|
||||||
document.getElementById('updateButton').textContent = 'Wird heruntergeladen...';
|
|
||||||
document.getElementById('updateButton').disabled = true;
|
|
||||||
document.getElementById('updateProgress').style.display = 'block';
|
|
||||||
// Start animated progress bar
|
|
||||||
document.getElementById('updateProgressBar').classList.add('downloading');
|
|
||||||
window.api.downloadUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-Update Event Listeners
|
|
||||||
window.api.onUpdateAvailable((info) => {
|
|
||||||
document.getElementById('updateBanner').style.display = 'flex';
|
|
||||||
document.getElementById('updateText').textContent = `Version ${info.version} verfügbar!`;
|
|
||||||
document.getElementById('updateButton').textContent = 'Jetzt herunterladen';
|
|
||||||
});
|
|
||||||
|
|
||||||
window.api.onUpdateDownloadProgress((progress) => {
|
|
||||||
const bar = document.getElementById('updateProgressBar');
|
|
||||||
bar.classList.remove('downloading');
|
|
||||||
bar.style.width = progress.percent + '%';
|
|
||||||
const mb = (progress.transferred / 1024 / 1024).toFixed(1);
|
|
||||||
const totalMb = (progress.total / 1024 / 1024).toFixed(1);
|
|
||||||
document.getElementById('updateText').textContent = `Download: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.api.onUpdateDownloaded((info) => {
|
|
||||||
updateReady = true;
|
|
||||||
const bar = document.getElementById('updateProgressBar');
|
|
||||||
bar.classList.remove('downloading');
|
|
||||||
bar.style.width = '100%';
|
|
||||||
document.getElementById('updateText').textContent = `Version ${info.version} bereit zur Installation!`;
|
|
||||||
document.getElementById('updateButton').textContent = 'Jetzt installieren';
|
|
||||||
document.getElementById('updateButton').disabled = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start
|
|
||||||
init();
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user