feat: add background download manager with progress reporting
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { invoke, Channel } from '@tauri-apps/api/core';
|
||||
import { preferences } from '$lib/stores/preferences.svelte';
|
||||
|
||||
export interface VideoMetadata {
|
||||
@@ -10,9 +10,50 @@ export interface VideoMetadata {
|
||||
streamUrl: string;
|
||||
}
|
||||
|
||||
type DownloadEvent =
|
||||
| { event: 'progress'; data: { percent: number } }
|
||||
| { event: 'filePath'; data: { path: string } }
|
||||
| { event: 'finished'; data: { success: boolean; path: string } }
|
||||
| { event: 'error'; data: { message: string } };
|
||||
|
||||
export async function resolveUrl(url: string): Promise<VideoMetadata> {
|
||||
return invoke<VideoMetadata>('resolve_url', {
|
||||
url,
|
||||
cookieSource: preferences.cookieSource,
|
||||
});
|
||||
}
|
||||
|
||||
export async function startDownload(
|
||||
url: string,
|
||||
onProgress: (percent: number) => void,
|
||||
onFilePath: (path: string) => void,
|
||||
onFinished: (success: boolean, path: string) => void,
|
||||
onError: (message: string) => void
|
||||
): Promise<void> {
|
||||
const onEvent = new Channel<DownloadEvent>();
|
||||
onEvent.onmessage = (message) => {
|
||||
switch (message.event) {
|
||||
case 'progress':
|
||||
onProgress(message.data.percent);
|
||||
break;
|
||||
case 'filePath':
|
||||
onFilePath(message.data.path);
|
||||
break;
|
||||
case 'finished':
|
||||
onFinished(message.data.success, message.data.path);
|
||||
break;
|
||||
case 'error':
|
||||
onError(message.data.message);
|
||||
break;
|
||||
default: {
|
||||
const _exhaustive: never = message;
|
||||
throw new Error(`Unhandled download event: ${(_exhaustive as DownloadEvent).event}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
await invoke('start_download', {
|
||||
url,
|
||||
cookieSource: preferences.cookieSource,
|
||||
onEvent,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { session } from '$lib/stores/videoSession.svelte';
|
||||
|
||||
let progressPercent = $derived(Math.round(session.downloadProgress * 100));
|
||||
let isDownloading = $derived(
|
||||
session.status === 'ready' &&
|
||||
session.downloadStatus === 'downloading' &&
|
||||
session.downloadProgress > 0 &&
|
||||
session.downloadProgress < 1
|
||||
);
|
||||
let isDownloaded = $derived(
|
||||
session.localFilePath !== null &&
|
||||
(session.downloadStatus === 'complete' || session.downloadProgress >= 1)
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="status-bar">
|
||||
<span class="status-text">Ready</span>
|
||||
{#if isDownloading}
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {progressPercent}%"></div>
|
||||
</div>
|
||||
<span class="status-text">Downloading: {progressPercent}%</span>
|
||||
{:else if isDownloaded}
|
||||
<span class="status-text success">✓ Downloaded: {session.title}</span>
|
||||
{:else if session.status === 'resolving'}
|
||||
<span class="status-text">Resolving URL…</span>
|
||||
{:else if session.status === 'ready' && session.downloadStatus === 'downloading'}
|
||||
<span class="status-text">Starting download…</span>
|
||||
{:else if session.downloadStatus === 'failed'}
|
||||
<span class="status-text">Download failed</span>
|
||||
{:else}
|
||||
<span class="status-text">Ready</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -12,5 +43,25 @@
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
min-height: 24px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--success);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { resolveUrl } from '$lib/bindings/video';
|
||||
import { session, setMetadata, setError, setResolving } from '$lib/stores/videoSession.svelte';
|
||||
import {
|
||||
session,
|
||||
setMetadata,
|
||||
setError,
|
||||
setResolving,
|
||||
beginDownload,
|
||||
} from '$lib/stores/videoSession.svelte';
|
||||
|
||||
let inputValue = $state('');
|
||||
|
||||
@@ -12,6 +18,7 @@
|
||||
try {
|
||||
const meta = await resolveUrl(trimmed);
|
||||
setMetadata(meta);
|
||||
void beginDownload();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { VideoMetadata } from '$lib/bindings/video';
|
||||
import { startDownload } from '$lib/bindings/video';
|
||||
import type { ThumbnailSpritesheet } from '$lib/bindings/mediaAnalysis';
|
||||
|
||||
export type SessionStatus = 'idle' | 'resolving' | 'ready' | 'error';
|
||||
export type DownloadStatus = 'idle' | 'downloading' | 'complete' | 'failed';
|
||||
|
||||
export const session = $state({
|
||||
status: 'idle' as SessionStatus,
|
||||
@@ -16,6 +18,7 @@ export const session = $state({
|
||||
isPlaying: false,
|
||||
localFilePath: null as string | null,
|
||||
downloadProgress: 0,
|
||||
downloadStatus: 'idle' as DownloadStatus,
|
||||
keyframePositions: [] as number[],
|
||||
waveformPeaks: [] as number[],
|
||||
thumbnailSpritesheets: [] as ThumbnailSpritesheet[],
|
||||
@@ -33,6 +36,7 @@ export function setMetadata(meta: VideoMetadata) {
|
||||
|
||||
session.localFilePath = null;
|
||||
session.downloadProgress = 0;
|
||||
session.downloadStatus = 'idle';
|
||||
session.keyframePositions = [];
|
||||
session.waveformPeaks = [];
|
||||
session.thumbnailSpritesheets = [];
|
||||
@@ -49,6 +53,7 @@ function clearMediaFields() {
|
||||
session.isPlaying = false;
|
||||
session.localFilePath = null;
|
||||
session.downloadProgress = 0;
|
||||
session.downloadStatus = 'idle';
|
||||
session.keyframePositions = [];
|
||||
session.waveformPeaks = [];
|
||||
session.thumbnailSpritesheets = [];
|
||||
@@ -71,3 +76,39 @@ export function reset() {
|
||||
session.status = 'idle';
|
||||
session.error = null;
|
||||
}
|
||||
|
||||
export async function beginDownload() {
|
||||
if (!session.url) return;
|
||||
|
||||
session.downloadStatus = 'downloading';
|
||||
session.downloadProgress = 0;
|
||||
session.localFilePath = null;
|
||||
|
||||
try {
|
||||
await startDownload(
|
||||
session.url,
|
||||
(percent) => {
|
||||
session.downloadProgress = percent;
|
||||
},
|
||||
(path) => {
|
||||
session.localFilePath = path;
|
||||
},
|
||||
(success, path) => {
|
||||
if (success) {
|
||||
session.downloadProgress = 1.0;
|
||||
session.localFilePath = path;
|
||||
session.downloadStatus = 'complete';
|
||||
} else {
|
||||
session.downloadStatus = 'failed';
|
||||
}
|
||||
},
|
||||
(message) => {
|
||||
session.downloadStatus = 'failed';
|
||||
console.error('Download error:', message);
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
session.downloadStatus = 'failed';
|
||||
console.error('Download failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user