feat(frontend): use Wails bindings for file I/O
Replace hidden file inputs and anchor downloads with native open/save dialogs via a thin platform layer. WAV export sends base64 audio data to the Go backend for writing to the user-chosen path. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+17
-21
@@ -10,6 +10,7 @@
|
||||
shouldRecordMarker,
|
||||
} from './lib/keyboard';
|
||||
import { downloadProject, exportAudio, loadProject } from './lib/project';
|
||||
import { openProject, openVideo } from './lib/platform/files';
|
||||
import {
|
||||
addMarker,
|
||||
app,
|
||||
@@ -28,8 +29,6 @@
|
||||
import { attachVideoPlayer, type VideoController } from './lib/video/player';
|
||||
|
||||
let videoEl: HTMLVideoElement | undefined = $state();
|
||||
let videoInput: HTMLInputElement | undefined = $state();
|
||||
let projectInput: HTMLInputElement | undefined = $state();
|
||||
let timeline: Timeline | undefined = $state();
|
||||
let controller: VideoController | null = null;
|
||||
let audioEngine = createAudioEngine();
|
||||
@@ -160,28 +159,27 @@
|
||||
audioEngine.destroy();
|
||||
});
|
||||
|
||||
async function handleVideoFile(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
setVideoUrl(URL.createObjectURL(file), file.name);
|
||||
statusMessage = `Loaded video: ${file.name}`;
|
||||
input.value = '';
|
||||
async function handleOpenVideo() {
|
||||
try {
|
||||
const result = await openVideo();
|
||||
if (!result) return;
|
||||
setVideoUrl(result.url, result.name);
|
||||
statusMessage = `Loaded video: ${result.name}`;
|
||||
} catch (error) {
|
||||
statusMessage = error instanceof Error ? error.message : 'Failed to open video';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleProjectFile(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
async function handleOpenProject() {
|
||||
try {
|
||||
const text = await file.text();
|
||||
const text = await openProject();
|
||||
if (!text) return;
|
||||
loadProject(JSON.parse(text));
|
||||
statusMessage = `Loaded project: ${file.name}`;
|
||||
statusMessage = 'Loaded project';
|
||||
rescheduleAudio();
|
||||
} catch (error) {
|
||||
statusMessage = error instanceof Error ? error.message : 'Failed to load project';
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
async function handleExportAudio() {
|
||||
@@ -202,9 +200,9 @@
|
||||
<div class="app">
|
||||
<header class="toolbar">
|
||||
<div class="file-actions">
|
||||
<button type="button" onclick={() => videoInput?.click()}>Open Video</button>
|
||||
<button type="button" onclick={() => projectInput?.click()}>Open Project</button>
|
||||
<button type="button" onclick={() => downloadProject()}>Save Project</button>
|
||||
<button type="button" onclick={handleOpenVideo}>Open Video</button>
|
||||
<button type="button" onclick={handleOpenProject}>Open Project</button>
|
||||
<button type="button" onclick={() => void downloadProject()}>Save Project</button>
|
||||
<button type="button" onclick={handleExportAudio}>Export Audio</button>
|
||||
</div>
|
||||
<div class="meta">
|
||||
@@ -230,8 +228,6 @@
|
||||
<span class="status">{statusMessage}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<input bind:this={videoInput} type="file" accept="video/mp4,video/*" hidden onchange={handleVideoFile} />
|
||||
<input bind:this={projectInput} type="file" accept="application/json,.json" hidden onchange={handleProjectFile} />
|
||||
</header>
|
||||
|
||||
<section class="player-section">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as FileService from '../../../bindings/sfxkeeb/fileservice';
|
||||
|
||||
export type VideoOpenResult = {
|
||||
url: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export async function openVideo(): Promise<VideoOpenResult | null> {
|
||||
const result = await FileService.OpenVideo();
|
||||
if (!result.url) return null;
|
||||
return { url: result.url, name: result.name };
|
||||
}
|
||||
|
||||
export async function openProject(): Promise<string | null> {
|
||||
const text = await FileService.OpenProject();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
export async function saveProject(content: string, defaultName = 'project.json'): Promise<boolean> {
|
||||
await FileService.SaveProject(content, defaultName);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function blobToBase64(blob: Blob): Promise<string> {
|
||||
const buffer = await blob.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i]!);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
export async function saveAudio(blob: Blob, defaultName = 'keyboard_track.wav'): Promise<boolean> {
|
||||
const dataBase64 = await blobToBase64(blob);
|
||||
await FileService.SaveAudio(dataBase64, defaultName);
|
||||
return true;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { renderKeyboardTrack } from './audio/export';
|
||||
import type { SampleCache } from './audio/sampleCache';
|
||||
import { saveAudio, saveProject } from './platform/files';
|
||||
import type { LegacySwitchType, Marker, Project, SwitchType } from './types';
|
||||
import { app, setActiveSwitch, setMarkers, setSelectedIds } from './store.svelte';
|
||||
|
||||
@@ -32,15 +33,10 @@ export function buildProject(): Project {
|
||||
};
|
||||
}
|
||||
|
||||
export function downloadProject(filename = 'project.json') {
|
||||
export async function downloadProject(filename = 'project.json') {
|
||||
const project = buildProject();
|
||||
const blob = new Blob([JSON.stringify(project, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
const content = JSON.stringify(project, null, 2);
|
||||
await saveProject(content, filename);
|
||||
}
|
||||
|
||||
export function loadProject(data: Project & { version?: number; switch?: string }) {
|
||||
@@ -65,10 +61,5 @@ export async function exportAudio(duration: number, cache?: SampleCache) {
|
||||
cache,
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = 'keyboard_track.wav';
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
await saveAudio(blob, 'keyboard_track.wav');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user