feat(import): model selector + ffmpeg/yt-dlp download links in add-meeting dialog
This commit is contained in:
1 parent
98f6fcdc16
commit
d736b78ecb
1 file changed
+102
-21
@@ -1,22 +1,43 @@
|
||||
<script lang="ts">
|
||||
// Manually add a meeting from an existing recording (feature: "add a meeting
|
||||
// + upload a video URL or audio file"). Transcoding is done by the backend
|
||||
// via ffmpeg (+ yt-dlp for URLs) — both external, not bundled — so this is
|
||||
// just a small form: pick a local file or paste a URL, optional title, go.
|
||||
import { api, errorMessage } from "../api";
|
||||
// via ffmpeg (+ yt-dlp for URLs) — both external, not bundled — so this is a
|
||||
// small form: pick a file or URL, choose the transcription model, go. Import
|
||||
// runs in the background (import_media returns as soon as the meeting row
|
||||
// exists), so this closes immediately and the meetings list shows progress.
|
||||
import { api, errorMessage, type ModelInfo } from "../api";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { onMount } from "svelte";
|
||||
import { trapFocus } from "../actions/trapFocus";
|
||||
import { t } from "../i18n/index.svelte";
|
||||
import { X, FileUp, Link as LinkIcon } from "@lucide/svelte";
|
||||
import { X, FileUp, Download } from "@lucide/svelte";
|
||||
|
||||
let { onClose, onImported }: { onClose: () => void; onImported: (id: string) => void } = $props();
|
||||
|
||||
// External download pages for the two tools this feature shells out to.
|
||||
const FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/latest";
|
||||
const YTDLP_URL = "https://github.com/yt-dlp/yt-dlp/releases/latest";
|
||||
|
||||
// `source` is either a local file path (set via Browse) or a URL (typed).
|
||||
let source = $state("");
|
||||
let title = $state("");
|
||||
let model = $state("");
|
||||
let models = $state<ModelInfo[]>([]);
|
||||
let busy = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Only installed whisper models are selectable; default to the active one so
|
||||
// the pick matches the user's Settings default unless they change it here.
|
||||
onMount(async () => {
|
||||
try {
|
||||
const all = await api.listModels();
|
||||
models = all.filter((m) => m.installed);
|
||||
model = models.find((m) => m.active)?.id ?? models[0]?.id ?? "";
|
||||
} catch {
|
||||
models = [];
|
||||
}
|
||||
});
|
||||
|
||||
async function browse() {
|
||||
const path = await open({
|
||||
multiple: false,
|
||||
@@ -51,7 +72,7 @@
|
||||
busy = true;
|
||||
error = null;
|
||||
try {
|
||||
const id = await api.importMedia(source.trim(), title.trim() || undefined);
|
||||
const id = await api.importMedia(source.trim(), title.trim() || undefined, model || undefined);
|
||||
onImported(id);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
@@ -101,17 +122,32 @@
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="wide">
|
||||
{t("import.title_label")} <em>({t("import.optional")})</em>
|
||||
<input bind:value={title} placeholder={t("import.title_placeholder")} disabled={busy} />
|
||||
</label>
|
||||
<div class="grid">
|
||||
<label>
|
||||
{t("import.model_label")}
|
||||
<select bind:value={model} disabled={busy || models.length === 0}>
|
||||
{#each models as m (m.id)}
|
||||
<option value={m.id}>{m.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="hint">{t("import.model_hint")}</span>
|
||||
</label>
|
||||
|
||||
<p class="muted small">
|
||||
<LinkIcon size={12} aria-hidden="true" />
|
||||
{t("import.requires_1")} <code>ffmpeg</code>
|
||||
{t("import.requires_2")} <code>yt-dlp</code>
|
||||
{t("import.requires_3")}
|
||||
</p>
|
||||
<label>
|
||||
{t("import.title_label")} <em>({t("import.optional")})</em>
|
||||
<input bind:value={title} placeholder={t("import.title_placeholder")} disabled={busy} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tools">
|
||||
<span class="muted small">{t("import.requires")}</span>
|
||||
<button class="tool" type="button" onclick={() => api.openUrl(FFMPEG_URL)}>
|
||||
<Download size={12} aria-hidden="true" /> ffmpeg
|
||||
</button>
|
||||
<button class="tool" type="button" onclick={() => api.openUrl(YTDLP_URL)}>
|
||||
<Download size={12} aria-hidden="true" /> yt-dlp
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="error">{error}</p>
|
||||
@@ -121,6 +157,7 @@
|
||||
<button class="primary" onclick={doImport} disabled={!source.trim() || busy}>
|
||||
{busy ? t("import.importing") : t("import.import")}
|
||||
</button>
|
||||
<span class="muted small note">{t("import.background_note")}</span>
|
||||
<button class="link" onclick={onClose} disabled={busy}>{t("import.cancel")}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +180,7 @@
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem;
|
||||
width: min(520px, 100%);
|
||||
width: min(540px, 100%);
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
||||
@@ -179,7 +216,8 @@
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
}
|
||||
input {
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.4rem 0.55rem;
|
||||
@@ -189,10 +227,18 @@
|
||||
color: var(--fg);
|
||||
font: inherit;
|
||||
}
|
||||
input:focus-visible {
|
||||
input:focus-visible,
|
||||
select:focus-visible {
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
}
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
@@ -213,18 +259,49 @@
|
||||
padding: 0.4rem 0.6rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Model + title side by side on wide panels, stacked when cramped. */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 0.75rem;
|
||||
}
|
||||
@media (max-width: 460px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
.tool {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: var(--bg-hover, transparent);
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
.small {
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.error {
|
||||
color: var(--danger, #d33);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
@@ -232,6 +309,10 @@
|
||||
gap: 0.6rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.actions .note {
|
||||
flex: 1;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.actions .primary {
|
||||
background: var(--accent);
|
||||
color: var(--accent-fg, #fff);
|
||||
|
||||
Reference in new issue
Block a user