228 lines
4.7 KiB
Svelte
228 lines
4.7 KiB
Svelte
|
|
<script lang="ts">
|
||
|
|
import {
|
||
|
|
checkDependencies,
|
||
|
|
installDependency,
|
||
|
|
type DependencyStatus,
|
||
|
|
} from '$lib/bindings/dependencies';
|
||
|
|
|
||
|
|
let {
|
||
|
|
onComplete,
|
||
|
|
}: {
|
||
|
|
onComplete: () => void;
|
||
|
|
} = $props();
|
||
|
|
|
||
|
|
let statuses = $state<DependencyStatus[]>([]);
|
||
|
|
let loading = $state(true);
|
||
|
|
let installingIndex = $state<number | null>(null);
|
||
|
|
let installLog = $state<string[]>([]);
|
||
|
|
let installError = $state<string | null>(null);
|
||
|
|
|
||
|
|
$effect(() => {
|
||
|
|
runChecks();
|
||
|
|
});
|
||
|
|
|
||
|
|
async function runChecks() {
|
||
|
|
loading = true;
|
||
|
|
statuses = await checkDependencies();
|
||
|
|
loading = false;
|
||
|
|
|
||
|
|
const allMet = statuses.every((s) => s.installed);
|
||
|
|
if (allMet) {
|
||
|
|
onComplete();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function install(index: number) {
|
||
|
|
const dep = statuses[index];
|
||
|
|
installingIndex = index;
|
||
|
|
installLog = [];
|
||
|
|
installError = null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await installDependency(
|
||
|
|
dep.name,
|
||
|
|
(line) => {
|
||
|
|
installLog = [...installLog, line];
|
||
|
|
},
|
||
|
|
(success) => {
|
||
|
|
if (!success) {
|
||
|
|
installError = 'Installation failed. Check the log for details.';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|
||
|
|
await runChecks();
|
||
|
|
} catch (e) {
|
||
|
|
installError = String(e);
|
||
|
|
} finally {
|
||
|
|
installingIndex = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function skip() {
|
||
|
|
const criticalMet = statuses[0]?.installed && statuses[1]?.installed;
|
||
|
|
if (criticalMet) {
|
||
|
|
onComplete();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let criticalMet = $derived(statuses[0]?.installed && statuses[1]?.installed);
|
||
|
|
</script>
|
||
|
|
|
||
|
|
{#if loading}
|
||
|
|
<div class="wizard-overlay">
|
||
|
|
<div class="wizard">
|
||
|
|
<h2>Checking dependencies…</h2>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{:else}
|
||
|
|
<div class="wizard-overlay">
|
||
|
|
<div class="wizard">
|
||
|
|
<h2>Setup</h2>
|
||
|
|
<p class="subtitle">
|
||
|
|
Video Clipper requires a few tools to work. Let's make sure they're installed.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<ul class="dep-list">
|
||
|
|
{#each statuses as dep, i}
|
||
|
|
<li class="dep-row">
|
||
|
|
<span class="dep-status">
|
||
|
|
{#if dep.installed}
|
||
|
|
✅
|
||
|
|
{:else if installingIndex === i}
|
||
|
|
⏳
|
||
|
|
{:else}
|
||
|
|
❌
|
||
|
|
{/if}
|
||
|
|
</span>
|
||
|
|
<span class="dep-info">
|
||
|
|
<strong>{dep.name}</strong>
|
||
|
|
{#if dep.version}
|
||
|
|
<span class="version">{dep.version}</span>
|
||
|
|
{/if}
|
||
|
|
</span>
|
||
|
|
{#if !dep.installed && installingIndex === null}
|
||
|
|
<button onclick={() => install(i)}>Install</button>
|
||
|
|
{/if}
|
||
|
|
</li>
|
||
|
|
{/each}
|
||
|
|
</ul>
|
||
|
|
|
||
|
|
{#if installLog.length > 0}
|
||
|
|
<div class="log-area">
|
||
|
|
{#each installLog as line}
|
||
|
|
<div class="log-line">{line}</div>
|
||
|
|
{/each}
|
||
|
|
</div>
|
||
|
|
{/if}
|
||
|
|
|
||
|
|
{#if installError}
|
||
|
|
<div class="error">{installError}</div>
|
||
|
|
{/if}
|
||
|
|
|
||
|
|
<div class="wizard-actions">
|
||
|
|
{#if criticalMet}
|
||
|
|
<button class="primary" onclick={skip}>
|
||
|
|
{statuses.every((s) => s.installed) ? 'Continue' : 'Skip optional & Continue'}
|
||
|
|
</button>
|
||
|
|
{:else}
|
||
|
|
<p class="hint">ffmpeg and yt-dlp are required to continue.</p>
|
||
|
|
{/if}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{/if}
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.wizard-overlay {
|
||
|
|
position: fixed;
|
||
|
|
inset: 0;
|
||
|
|
background: rgba(0, 0, 0, 0.7);
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
z-index: 1000;
|
||
|
|
}
|
||
|
|
|
||
|
|
.wizard {
|
||
|
|
background: var(--bg-secondary);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 24px 32px;
|
||
|
|
max-width: 520px;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
h2 {
|
||
|
|
margin-bottom: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.subtitle {
|
||
|
|
color: var(--text-secondary);
|
||
|
|
margin-bottom: 16px;
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dep-list {
|
||
|
|
list-style: none;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dep-row {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
padding: 8px 0;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dep-info {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.version {
|
||
|
|
color: var(--text-muted);
|
||
|
|
font-size: 12px;
|
||
|
|
margin-left: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-area {
|
||
|
|
max-height: 120px;
|
||
|
|
overflow-y: auto;
|
||
|
|
background: var(--bg-primary);
|
||
|
|
border-radius: 4px;
|
||
|
|
padding: 8px;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 11px;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
margin-bottom: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-line {
|
||
|
|
white-space: pre-wrap;
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
|
||
|
|
.error {
|
||
|
|
color: var(--danger);
|
||
|
|
font-size: 13px;
|
||
|
|
margin-bottom: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.wizard-actions {
|
||
|
|
display: flex;
|
||
|
|
justify-content: flex-end;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.primary {
|
||
|
|
background: var(--accent);
|
||
|
|
color: var(--bg-primary);
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.hint {
|
||
|
|
color: var(--text-muted);
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
</style>
|