feat(kon): scaffold hybrid modular workspace
- Cargo workspace with 6 domain crates: core, audio, transcription, ai-formatting, storage, cloud-providers - Minimal Tauri shell (lib.rs + main.rs) with plugin registration - Svelte 5 frontend copied from Ramble v0.2 - All crates compile as empty stubs - App identifier: uk.co.corbel.kon Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
241
src/lib/pages/FilesPage.svelte
Normal file
241
src/lib/pages/FilesPage.svelte
Normal file
@@ -0,0 +1,241 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { settings, addToHistory } from "$lib/stores/page.svelte.js";
|
||||
import Card from "$lib/components/Card.svelte";
|
||||
import { exportTranscript } from "$lib/utils/export.js";
|
||||
|
||||
let fileTranscript = $state("");
|
||||
let segments = $state([]);
|
||||
let isDragOver = $state(false);
|
||||
let progress = $state(0);
|
||||
let progressText = $state("");
|
||||
let fileName = $state("");
|
||||
let error = $state("");
|
||||
let transcribing = $state(false);
|
||||
let showExportMenu = $state(false);
|
||||
|
||||
let unlistenDrop = null;
|
||||
let unlistenEnter = null;
|
||||
let unlistenLeave = null;
|
||||
|
||||
onMount(async () => {
|
||||
// Native Tauri file drop events
|
||||
unlistenDrop = await listen("tauri://drag-drop", async (event) => {
|
||||
isDragOver = false;
|
||||
if (event.payload?.paths?.length > 0) {
|
||||
await transcribeFiles(event.payload.paths);
|
||||
}
|
||||
});
|
||||
unlistenEnter = await listen("tauri://drag-enter", () => { isDragOver = true; });
|
||||
unlistenLeave = await listen("tauri://drag-leave", () => { isDragOver = false; });
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (unlistenDrop) unlistenDrop();
|
||||
if (unlistenEnter) unlistenEnter();
|
||||
if (unlistenLeave) unlistenLeave();
|
||||
});
|
||||
|
||||
async function handleBrowse() {
|
||||
try {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const paths = await open({
|
||||
multiple: true,
|
||||
filters: [{
|
||||
name: "Audio/Video",
|
||||
extensions: ["mp3", "wav", "m4a", "mp4", "flac", "ogg", "webm", "mov", "mkv"],
|
||||
}],
|
||||
});
|
||||
if (paths) {
|
||||
const pathList = Array.isArray(paths) ? paths : [paths];
|
||||
await transcribeFiles(pathList);
|
||||
}
|
||||
} catch (err) {
|
||||
error = typeof err === "string" ? err : err.message || "Failed to open file dialog";
|
||||
}
|
||||
}
|
||||
|
||||
async function transcribeFiles(paths) {
|
||||
transcribing = true;
|
||||
error = "";
|
||||
fileTranscript = "";
|
||||
segments = [];
|
||||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const path = paths[i];
|
||||
fileName = path.split(/[\\/]/).pop() || path;
|
||||
progress = (i / paths.length) * 100;
|
||||
progressText = `Transcribing ${i + 1}/${paths.length}...`;
|
||||
|
||||
try {
|
||||
const result = await invoke("transcribe_file", {
|
||||
path,
|
||||
language: settings.language,
|
||||
initialPrompt: "",
|
||||
removeFillers: settings.removeFillers,
|
||||
britishEnglish: settings.britishEnglish,
|
||||
antiHallucination: settings.antiHallucination,
|
||||
formatMode: settings.formatMode,
|
||||
});
|
||||
|
||||
const text = result.segments.map((s) => s.text).join(" ").trim();
|
||||
|
||||
if (paths.length > 1) {
|
||||
fileTranscript += `--- ${fileName} ---\n${text}\n\n`;
|
||||
} else {
|
||||
fileTranscript = text;
|
||||
}
|
||||
|
||||
segments = [...segments, ...result.segments];
|
||||
|
||||
addToHistory({
|
||||
id: crypto.randomUUID(),
|
||||
date: new Date().toLocaleString("en-GB"),
|
||||
source: fileName,
|
||||
preview: text.slice(0, 120),
|
||||
text,
|
||||
segments: result.segments,
|
||||
duration: result.duration,
|
||||
language: result.language,
|
||||
});
|
||||
} catch (err) {
|
||||
error = `Failed: ${fileName} — ${typeof err === "string" ? err : err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
progress = 100;
|
||||
progressText = "Done";
|
||||
transcribing = false;
|
||||
|
||||
setTimeout(() => {
|
||||
progress = 0;
|
||||
progressText = "";
|
||||
fileName = "";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function copyAll() {
|
||||
if (fileTranscript) navigator.clipboard.writeText(fileTranscript);
|
||||
}
|
||||
|
||||
function handleExport(format) {
|
||||
if (!fileTranscript) return;
|
||||
showExportMenu = false;
|
||||
const content = exportTranscript(fileTranscript, segments, format);
|
||||
const ext = format === "vtt" ? "vtt" : format === "srt" ? "srt" : format === "md" ? "md" : "txt";
|
||||
const blob = new Blob([content], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `ramble-file-${new Date().toISOString().slice(0, 10)}.${ext}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full overflow-y-auto animate-fade-in">
|
||||
<!-- Title -->
|
||||
<h2 class="font-display text-[24px] italic text-text px-7 pt-6 pb-4">File Transcription</h2>
|
||||
|
||||
<!-- Drop zone -->
|
||||
<div class="px-7 pb-3">
|
||||
<Card>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-10 px-6 rounded-xl border-2 border-dashed m-3
|
||||
transition-all duration-200
|
||||
{isDragOver
|
||||
? 'border-accent bg-accent-subtle scale-[1.01]'
|
||||
: 'border-border hover:border-text-tertiary'}"
|
||||
role="region"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-full bg-bg-elevated flex items-center justify-center mb-3">
|
||||
<svg class="w-5 h-5 text-text-tertiary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-[13px] text-text-secondary text-center">
|
||||
Drop audio or video files here
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary text-center mt-1.5">
|
||||
MP3, WAV, M4A, MP4, FLAC, OGG
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Browse buttons + progress -->
|
||||
<div class="flex items-center gap-2 px-7 pb-3">
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg text-[13px] font-medium text-white bg-accent hover:bg-accent-hover
|
||||
shadow-[0_2px_8px_rgba(232,168,124,0.2)] active:scale-[0.97] transition-all duration-150"
|
||||
onclick={handleBrowse}
|
||||
disabled={transcribing}
|
||||
>
|
||||
Browse Files
|
||||
</button>
|
||||
<div class="flex-1"></div>
|
||||
{#if progressText}
|
||||
<span class="text-[12px] text-text-secondary animate-fade-in">{progressText}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
{#if error}
|
||||
<div class="px-7 pb-2 animate-fade-in">
|
||||
<div class="px-4 py-2 rounded-lg bg-danger/10 border border-danger/20 text-[12px] text-danger">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Progress bar -->
|
||||
{#if progress > 0}
|
||||
<div class="px-8 pb-3 animate-fade-in">
|
||||
<div class="h-[3px] bg-bg-elevated rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-accent rounded-full transition-all duration-500 shadow-[0_0_8px_rgba(232,168,124,0.4)]"
|
||||
style="width: {progress}%"
|
||||
></div>
|
||||
</div>
|
||||
{#if fileName}
|
||||
<p class="text-[11px] text-text-tertiary mt-1.5">{fileName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- File transcript -->
|
||||
<div class="flex-1 px-7 pb-3 min-h-0">
|
||||
<Card classes="h-full flex flex-col">
|
||||
<textarea
|
||||
class="font-transcript flex-1 w-full bg-transparent text-text p-6
|
||||
resize-none focus:outline-none placeholder:text-text-tertiary"
|
||||
placeholder="Transcribed text will appear here..."
|
||||
bind:value={fileTranscript}
|
||||
data-no-transition
|
||||
></textarea>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Bottom actions -->
|
||||
<div class="flex gap-0.5 px-7 pb-4">
|
||||
<button class="px-3 py-1.5 rounded-lg text-[12px] text-text-secondary hover:bg-hover hover:text-text" onclick={copyAll}>Copy</button>
|
||||
<div class="relative">
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-lg text-[12px] text-text-secondary hover:bg-hover hover:text-text"
|
||||
onclick={() => showExportMenu = !showExportMenu}
|
||||
>Export</button>
|
||||
{#if showExportMenu}
|
||||
<div class="absolute left-0 bottom-full mb-1 bg-bg-card border border-border rounded-lg shadow-lg py-1 z-10 animate-fade-in min-w-[120px]">
|
||||
{#each [["txt", "Plain Text"], ["md", "Markdown"], ["srt", "SRT Subtitles"], ["vtt", "WebVTT"]] as [fmt, label]}
|
||||
<button
|
||||
class="w-full text-left px-3 py-1.5 text-[12px] text-text-secondary hover:bg-hover hover:text-text"
|
||||
onclick={() => handleExport(fmt)}
|
||||
>{label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user