Improve live download status and add debug logging (v3.7.8)
Keep queue visibility synced while downloads run, show richer per-item progress/error states in the sidebar, and write backend downloader diagnostics to ProgramData debug.log so instant start/stop failures can be traced on user systems.
This commit is contained in:
parent
7f208cf369
commit
78378b9812
@ -54,3 +54,11 @@ Release must include all of:
|
|||||||
- `Twitch-VOD-Manager-Setup-<version>.exe.blockmap`
|
- `Twitch-VOD-Manager-Setup-<version>.exe.blockmap`
|
||||||
|
|
||||||
Tag version must match app version (example: `v3.7.6`).
|
Tag version must match app version (example: `v3.7.6`).
|
||||||
|
|
||||||
|
## Debug log for failed downloads
|
||||||
|
|
||||||
|
From `v3.7.8`, detailed downloader logs are written to:
|
||||||
|
|
||||||
|
`C:\ProgramData\Twitch_VOD_Manager\debug.log`
|
||||||
|
|
||||||
|
If a download instantly switches from `Stoppen` back to `Start`, check the latest lines in that file for streamlink exit reasons.
|
||||||
|
|||||||
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.7",
|
"version": "3.7.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "3.7.7",
|
"version": "3.7.8",
|
||||||
"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.7",
|
"version": "3.7.8",
|
||||||
"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.7</p>
|
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.7.8</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,7 +346,7 @@
|
|||||||
<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.7</span>
|
<span id="versionText">v3.7.8</span>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,13 +8,14 @@ import { autoUpdater } from 'electron-updater';
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// CONFIG & CONSTANTS
|
// CONFIG & CONSTANTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const APP_VERSION = '3.7.7';
|
const APP_VERSION = '3.7.8';
|
||||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
const APPDATA_DIR = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Twitch_VOD_Manager');
|
const APPDATA_DIR = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Twitch_VOD_Manager');
|
||||||
const CONFIG_FILE = path.join(APPDATA_DIR, 'config.json');
|
const CONFIG_FILE = path.join(APPDATA_DIR, 'config.json');
|
||||||
const QUEUE_FILE = path.join(APPDATA_DIR, 'download_queue.json');
|
const QUEUE_FILE = path.join(APPDATA_DIR, 'download_queue.json');
|
||||||
|
const DEBUG_LOG_FILE = path.join(APPDATA_DIR, 'debug.log');
|
||||||
const DEFAULT_DOWNLOAD_PATH = path.join(app.getPath('desktop'), 'Twitch_VODs');
|
const DEFAULT_DOWNLOAD_PATH = path.join(app.getPath('desktop'), 'Twitch_VODs');
|
||||||
|
|
||||||
// Timeouts
|
// Timeouts
|
||||||
@ -74,9 +75,15 @@ interface QueueItem {
|
|||||||
eta?: string;
|
eta?: string;
|
||||||
downloadedBytes?: number;
|
downloadedBytes?: number;
|
||||||
totalBytes?: number;
|
totalBytes?: number;
|
||||||
|
last_error?: string;
|
||||||
customClip?: CustomClip;
|
customClip?: CustomClip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DownloadResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface DownloadProgress {
|
interface DownloadProgress {
|
||||||
id: string;
|
id: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
@ -225,6 +232,18 @@ function getFFprobePath(): string {
|
|||||||
return path.join(path.dirname(ffmpegPath), ffprobeExe);
|
return path.join(path.dirname(ffmpegPath), ffprobeExe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendDebugLog(message: string, details?: unknown): void {
|
||||||
|
try {
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
const payload = details === undefined
|
||||||
|
? ''
|
||||||
|
: ` | ${typeof details === 'string' ? details : JSON.stringify(details)}`;
|
||||||
|
fs.appendFileSync(DEBUG_LOG_FILE, `[${ts}] ${message}${payload}\n`);
|
||||||
|
} catch {
|
||||||
|
// ignore debug log errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// DURATION HELPERS
|
// DURATION HELPERS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -756,10 +775,11 @@ function downloadVODPart(
|
|||||||
itemId: string,
|
itemId: string,
|
||||||
partNum: number,
|
partNum: number,
|
||||||
totalParts: number
|
totalParts: number
|
||||||
): Promise<boolean> {
|
): Promise<DownloadResult> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const streamlinkPath = getStreamlinkPath();
|
const streamlinkPath = getStreamlinkPath();
|
||||||
const args = [url, 'best', '-o', filename, '--force'];
|
const args = [url, 'best', '-o', filename, '--force'];
|
||||||
|
let lastErrorLine = '';
|
||||||
|
|
||||||
if (startTime) {
|
if (startTime) {
|
||||||
args.push('--hls-start-offset', startTime);
|
args.push('--hls-start-offset', startTime);
|
||||||
@ -769,6 +789,7 @@ function downloadVODPart(
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Starting download:', streamlinkPath, args);
|
console.log('Starting download:', streamlinkPath, args);
|
||||||
|
appendDebugLog('download-part-start', { itemId, streamlinkPath, filename, args });
|
||||||
|
|
||||||
const proc = spawn(streamlinkPath, args, { windowsHide: true });
|
const proc = spawn(streamlinkPath, args, { windowsHide: true });
|
||||||
currentProcess = proc;
|
currentProcess = proc;
|
||||||
@ -828,7 +849,12 @@ function downloadVODPart(
|
|||||||
});
|
});
|
||||||
|
|
||||||
proc.stderr?.on('data', (data: Buffer) => {
|
proc.stderr?.on('data', (data: Buffer) => {
|
||||||
console.error('Streamlink error:', data.toString());
|
const message = data.toString().trim();
|
||||||
|
if (message) {
|
||||||
|
lastErrorLine = message.split('\n').pop() || message;
|
||||||
|
appendDebugLog('download-part-stderr', { itemId, message: lastErrorLine });
|
||||||
|
console.error('Streamlink error:', message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.on('close', (code) => {
|
proc.on('close', (code) => {
|
||||||
@ -836,26 +862,36 @@ function downloadVODPart(
|
|||||||
currentProcess = null;
|
currentProcess = null;
|
||||||
|
|
||||||
if (currentDownloadCancelled) {
|
if (currentDownloadCancelled) {
|
||||||
resolve(false);
|
appendDebugLog('download-part-cancelled', { itemId, filename });
|
||||||
|
resolve({ success: false, error: 'Download wurde abgebrochen.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 0 && fs.existsSync(filename)) {
|
if (code === 0 && fs.existsSync(filename)) {
|
||||||
const stats = fs.statSync(filename);
|
const stats = fs.statSync(filename);
|
||||||
if (stats.size > 1024 * 1024) {
|
if (stats.size > 1024 * 1024) {
|
||||||
resolve(true);
|
appendDebugLog('download-part-success', { itemId, filename, bytes: stats.size });
|
||||||
|
resolve({ success: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tooSmall = `Datei zu klein (${stats.size} Bytes)`;
|
||||||
|
appendDebugLog('download-part-failed-small-file', { itemId, filename, bytes: stats.size });
|
||||||
|
resolve({ success: false, error: tooSmall });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(false);
|
const genericError = lastErrorLine || `Streamlink Exit-Code ${code ?? -1}`;
|
||||||
|
appendDebugLog('download-part-failed', { itemId, filename, code, error: genericError });
|
||||||
|
resolve({ success: false, error: genericError });
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.on('error', (err) => {
|
proc.on('error', (err) => {
|
||||||
clearInterval(progressInterval);
|
clearInterval(progressInterval);
|
||||||
console.error('Process error:', err);
|
console.error('Process error:', err);
|
||||||
currentProcess = null;
|
currentProcess = null;
|
||||||
resolve(false);
|
appendDebugLog('download-part-process-error', { itemId, error: String(err) });
|
||||||
|
resolve({ success: false, error: String(err) });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -863,7 +899,7 @@ function downloadVODPart(
|
|||||||
async function downloadVOD(
|
async function downloadVOD(
|
||||||
item: QueueItem,
|
item: QueueItem,
|
||||||
onProgress: (progress: DownloadProgress) => void
|
onProgress: (progress: DownloadProgress) => void
|
||||||
): Promise<boolean> {
|
): Promise<DownloadResult> {
|
||||||
const streamer = item.streamer.replace(/[^a-zA-Z0-9_-]/g, '');
|
const streamer = item.streamer.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||||
const date = new Date(item.date);
|
const date = new Date(item.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()}`;
|
||||||
@ -907,7 +943,7 @@ async function downloadVOD(
|
|||||||
|
|
||||||
const partFilename = makeClipFilename(partNum, startOffset);
|
const partFilename = makeClipFilename(partNum, startOffset);
|
||||||
|
|
||||||
const success = await downloadVODPart(
|
const result = await downloadVODPart(
|
||||||
item.url,
|
item.url,
|
||||||
partFilename,
|
partFilename,
|
||||||
formatDuration(startOffset),
|
formatDuration(startOffset),
|
||||||
@ -918,11 +954,14 @@ async function downloadVOD(
|
|||||||
numParts
|
numParts
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!success) return false;
|
if (!result.success) return result;
|
||||||
downloadedFiles.push(partFilename);
|
downloadedFiles.push(partFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
return downloadedFiles.length === numParts;
|
return {
|
||||||
|
success: downloadedFiles.length === numParts,
|
||||||
|
error: downloadedFiles.length === numParts ? undefined : 'Nicht alle Clip-Teile konnten heruntergeladen werden.'
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
// Single clip file
|
// Single clip file
|
||||||
const filename = makeClipFilename(clip.startPart, clip.startSec);
|
const filename = makeClipFilename(clip.startPart, clip.startSec);
|
||||||
@ -959,7 +998,7 @@ async function downloadVOD(
|
|||||||
|
|
||||||
const partFilename = path.join(folder, `${dateStr}_Part${(i + 1).toString().padStart(2, '0')}.mp4`);
|
const partFilename = path.join(folder, `${dateStr}_Part${(i + 1).toString().padStart(2, '0')}.mp4`);
|
||||||
|
|
||||||
const success = await downloadVODPart(
|
const result = await downloadVODPart(
|
||||||
item.url,
|
item.url,
|
||||||
partFilename,
|
partFilename,
|
||||||
formatDuration(startSec),
|
formatDuration(startSec),
|
||||||
@ -970,20 +1009,24 @@ async function downloadVOD(
|
|||||||
numParts
|
numParts
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!success) {
|
if (!result.success) {
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadedFiles.push(partFilename);
|
downloadedFiles.push(partFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
return downloadedFiles.length === numParts;
|
return {
|
||||||
|
success: downloadedFiles.length === numParts,
|
||||||
|
error: downloadedFiles.length === numParts ? undefined : 'Nicht alle Teile konnten heruntergeladen werden.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processQueue(): Promise<void> {
|
async function processQueue(): Promise<void> {
|
||||||
if (isDownloading || downloadQueue.length === 0) return;
|
if (isDownloading || downloadQueue.length === 0) return;
|
||||||
|
|
||||||
|
appendDebugLog('queue-start', { items: downloadQueue.length });
|
||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
mainWindow?.webContents.send('download-started');
|
mainWindow?.webContents.send('download-started');
|
||||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
@ -992,17 +1035,27 @@ async function processQueue(): Promise<void> {
|
|||||||
if (!isDownloading) break;
|
if (!isDownloading) break;
|
||||||
if (item.status === 'completed') continue;
|
if (item.status === 'completed') continue;
|
||||||
|
|
||||||
|
appendDebugLog('queue-item-start', { itemId: item.id, title: item.title, url: item.url });
|
||||||
|
|
||||||
currentDownloadCancelled = false;
|
currentDownloadCancelled = false;
|
||||||
item.status = 'downloading';
|
item.status = 'downloading';
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
|
|
||||||
const success = await downloadVOD(item, (progress) => {
|
item.last_error = '';
|
||||||
|
|
||||||
|
const result = await downloadVOD(item, (progress) => {
|
||||||
mainWindow?.webContents.send('download-progress', progress);
|
mainWindow?.webContents.send('download-progress', progress);
|
||||||
});
|
});
|
||||||
|
|
||||||
item.status = success ? 'completed' : 'error';
|
item.status = result.success ? 'completed' : 'error';
|
||||||
item.progress = success ? 100 : 0;
|
item.progress = result.success ? 100 : 0;
|
||||||
|
item.last_error = result.success ? '' : (result.error || 'Unbekannter Fehler beim Download');
|
||||||
|
appendDebugLog('queue-item-finished', {
|
||||||
|
itemId: item.id,
|
||||||
|
status: item.status,
|
||||||
|
error: item.last_error
|
||||||
|
});
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
}
|
}
|
||||||
@ -1011,6 +1064,7 @@ async function processQueue(): Promise<void> {
|
|||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
mainWindow?.webContents.send('download-finished');
|
mainWindow?.webContents.send('download-finished');
|
||||||
|
appendDebugLog('queue-finished', { items: downloadQueue.length });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
2
typescript-version/src/renderer-globals.d.ts
vendored
2
typescript-version/src/renderer-globals.d.ts
vendored
@ -42,6 +42,8 @@ interface QueueItem {
|
|||||||
eta?: string;
|
eta?: string;
|
||||||
downloadedBytes?: number;
|
downloadedBytes?: number;
|
||||||
totalBytes?: number;
|
totalBytes?: number;
|
||||||
|
progressStatus?: string;
|
||||||
|
last_error?: string;
|
||||||
customClip?: CustomClip;
|
customClip?: CustomClip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,59 @@ async function clearCompleted(): Promise<void> {
|
|||||||
renderQueue();
|
renderQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getQueueStatusLabel(item: QueueItem): string {
|
||||||
|
if (item.status === 'completed') return 'Abgeschlossen';
|
||||||
|
if (item.status === 'error') return 'Fehlgeschlagen';
|
||||||
|
if (item.status === 'downloading') return 'Lauft';
|
||||||
|
return 'Wartet';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQueueProgressText(item: QueueItem): string {
|
||||||
|
if (item.status === 'completed') return '100%';
|
||||||
|
if (item.status === 'error') return 'Fehler';
|
||||||
|
if (item.status === 'pending') return 'Bereit';
|
||||||
|
if (item.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`;
|
||||||
|
return item.progressStatus || 'Lade...';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQueueMetaText(item: QueueItem): string {
|
||||||
|
if (item.status === 'error' && item.last_error) {
|
||||||
|
return item.last_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (item.currentPart && item.totalParts) {
|
||||||
|
parts.push(`Teil ${item.currentPart}/${item.totalParts}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.speed) {
|
||||||
|
parts.push(item.speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.eta) {
|
||||||
|
parts.push(`ETA ${item.eta}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parts.length && item.status === 'pending') {
|
||||||
|
parts.push('Bereit zum Download');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parts.length && item.status === 'downloading') {
|
||||||
|
parts.push(item.progressStatus || 'Download gestartet');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parts.length && item.status === 'completed') {
|
||||||
|
parts.push('Fertig');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parts.length && item.status === 'error') {
|
||||||
|
parts.push('Download fehlgeschlagen');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
function renderQueue(): void {
|
function renderQueue(): void {
|
||||||
if (!Array.isArray(queue)) {
|
if (!Array.isArray(queue)) {
|
||||||
queue = [];
|
queue = [];
|
||||||
@ -34,11 +87,30 @@ function renderQueue(): void {
|
|||||||
|
|
||||||
list.innerHTML = queue.map((item: QueueItem) => {
|
list.innerHTML = queue.map((item: QueueItem) => {
|
||||||
const safeTitle = escapeHtml(item.title || 'Untitled');
|
const safeTitle = escapeHtml(item.title || 'Untitled');
|
||||||
|
const safeStatusLabel = escapeHtml(getQueueStatusLabel(item));
|
||||||
|
const safeProgressText = escapeHtml(getQueueProgressText(item));
|
||||||
|
const safeMeta = escapeHtml(getQueueMetaText(item));
|
||||||
const isClip = item.customClip ? '* ' : '';
|
const isClip = item.customClip ? '* ' : '';
|
||||||
|
const hasDeterminateProgress = item.progress > 0 && item.progress <= 100;
|
||||||
|
const progressValue = item.status === 'completed'
|
||||||
|
? 100
|
||||||
|
: (hasDeterminateProgress ? Math.max(0, Math.min(100, item.progress)) : 0);
|
||||||
|
const progressClass = item.status === 'downloading' && !hasDeterminateProgress ? ' indeterminate' : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="queue-item">
|
<div class="queue-item">
|
||||||
<div class="status ${item.status}"></div>
|
<div class="status ${item.status}"></div>
|
||||||
|
<div class="queue-main">
|
||||||
|
<div class="queue-title-row">
|
||||||
<div class="title" title="${safeTitle}">${isClip}${safeTitle}</div>
|
<div class="title" title="${safeTitle}">${isClip}${safeTitle}</div>
|
||||||
|
<div class="queue-status-label">${safeStatusLabel}</div>
|
||||||
|
</div>
|
||||||
|
<div class="queue-meta">${safeMeta}</div>
|
||||||
|
<div class="queue-progress-wrap">
|
||||||
|
<div class="queue-progress-bar${progressClass}" style="width: ${progressValue}%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="queue-progress-text">${safeProgressText}</div>
|
||||||
|
</div>
|
||||||
<span class="remove" onclick="removeFromQueue('${item.id}')">x</span>
|
<span class="remove" onclick="removeFromQueue('${item.id}')">x</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -31,7 +31,15 @@ async function init(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.status = 'downloading';
|
||||||
item.progress = progress.progress;
|
item.progress = progress.progress;
|
||||||
|
item.speed = progress.speed;
|
||||||
|
item.eta = progress.eta;
|
||||||
|
item.currentPart = progress.currentPart;
|
||||||
|
item.totalParts = progress.totalParts;
|
||||||
|
item.downloadedBytes = progress.downloadedBytes;
|
||||||
|
item.totalBytes = progress.totalBytes;
|
||||||
|
item.progressStatus = progress.status;
|
||||||
renderQueue();
|
renderQueue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -195,13 +195,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.queue-list {
|
.queue-list {
|
||||||
max-height: 150px;
|
max-height: 210px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-item {
|
.queue-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
@ -234,6 +234,63 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.queue-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-status-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-meta {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-progress-wrap {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255,255,255,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
background: var(--accent);
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-progress-bar.indeterminate {
|
||||||
|
width: 35% !important;
|
||||||
|
animation: queue-indeterminate 1.3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-progress-text {
|
||||||
|
margin-top: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes queue-indeterminate {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(280%); }
|
||||||
|
}
|
||||||
|
|
||||||
.queue-item .remove {
|
.queue-item .remove {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user