Add Save As, auto-migrate v1 projects to folder structure
All checks were successful
Release / Bump version and tag (push) Successful in 3s
Release / Build App (macOS) (push) Successful in 1m17s
Release / Build App (Windows) (push) Successful in 3m5s
Release / Build App (Linux) (push) Successful in 3m22s

Save behavior:
- Save on v2 project: saves in place (no dialog)
- Save on v1 project: auto-migrates to folder structure next to the
  original .vtn (creates ProjectName/ folder with .vtn + audio.wav)
- Save on unsaved project: opens folder picker (Save As)
- Save As: always opens folder picker for a new location

Added projectIsV2 state to track project format version.
Split "Save Project" button into "Save" + "Save As".
Extracted saveToFolder() helper for shared save logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-23 09:56:11 -07:00
parent 548d260061
commit e05f9afaff

View File

@@ -31,6 +31,7 @@
// Project management state // Project management state
let currentProjectPath = $state<string | null>(null); let currentProjectPath = $state<string | null>(null);
let currentProjectName = $state(''); let currentProjectName = $state('');
let projectIsV2 = $state(false);
let audioFilePath = $state(''); let audioFilePath = $state('');
let audioWavPath = $state(''); let audioWavPath = $state('');
@@ -171,57 +172,65 @@
}; };
} }
async function saveProject() { /** Save to a specific folder — creates .vtn + audio.wav inside it. */
const defaultName = currentProjectName || 'Untitled'; async function saveToFolder(folderPath: string): Promise<boolean> {
const projectName = folderPath.split(/[\\/]/).pop() || currentProjectName || 'Untitled';
// If already saved, save in place const vtnPath = `${folderPath}/${projectName}.vtn`;
if (currentProjectPath) { const wavPath = `${folderPath}/audio.wav`;
const folderPath = currentProjectPath.replace(/[\\/][^\\/]+$/, '');
const projectName = currentProjectPath.split(/[\\/]/).pop()?.replace(/\.vtn$/i, '') || defaultName;
const wavInsideFolder = `${folderPath}/audio.wav`;
const projectData = buildProjectData(projectName); const projectData = buildProjectData(projectName);
try { try {
if (audioWavPath && audioWavPath !== wavInsideFolder) { await invoke('create_dir', { path: folderPath });
await invoke('copy_file', { src: audioWavPath, dst: wavInsideFolder }); if (audioWavPath && audioWavPath !== wavPath) {
await invoke('copy_file', { src: audioWavPath, dst: wavPath });
audioWavPath = wavPath;
} }
await invoke('save_project_file', { path: currentProjectPath, project: projectData }); await invoke('save_project_file', { path: vtnPath, project: projectData });
currentProjectPath = vtnPath;
currentProjectName = projectName;
projectIsV2 = true;
return true;
} catch (err) { } catch (err) {
console.error('Failed to save project:', err);
alert(`Failed to save: ${err}`); alert(`Failed to save: ${err}`);
return false;
}
}
async function saveProject() {
// Already saved as v2 folder — save in place
if (currentProjectPath && projectIsV2) {
const folderPath = currentProjectPath.replace(/[\\/][^\\/]+$/, '');
await saveToFolder(folderPath);
return;
}
// V1 project opened — migrate to folder structure
if (currentProjectPath && !projectIsV2) {
const oldVtnDir = currentProjectPath.replace(/[\\/][^\\/]+$/, '');
const projectName = currentProjectPath.split(/[\\/]/).pop()?.replace(/\.vtn$/i, '') || 'Untitled';
const folderPath = `${oldVtnDir}/${projectName}`;
const success = await saveToFolder(folderPath);
if (success) {
// Optionally remove the old .vtn file
try {
// Leave old file — user can delete manually
} catch {}
} }
return; return;
} }
// Pick a folder for the new project // Never saved — pick a folder
await saveProjectAs();
}
async function saveProjectAs() {
const folderPath = await open({ const folderPath = await open({
directory: true, directory: true,
title: 'Choose a folder to save the project', title: 'Choose a folder to save the project',
}); });
if (!folderPath) return; if (!folderPath) return;
await saveToFolder(folderPath as string);
// Use the folder name as the project name
const projectName = (folderPath as string).split(/[\\/]/).pop() || defaultName;
const vtnInsideFolder = `${folderPath}/${projectName}.vtn`;
const wavInsideFolder = `${folderPath}/audio.wav`;
const projectData = buildProjectData(projectName);
try {
// Copy the extracted WAV into the project folder
if (audioWavPath) {
await invoke('copy_file', { src: audioWavPath, dst: wavInsideFolder });
}
// Save the .vtn file inside the folder
await invoke('save_project_file', { path: vtnInsideFolder, project: projectData });
currentProjectPath = vtnInsideFolder;
currentProjectName = projectName;
} catch (err) {
console.error('Failed to save project:', err);
alert(`Failed to save: ${err}`);
}
} }
async function openProject() { async function openProject() {
@@ -290,6 +299,7 @@
// Determine the directory the .vtn file is in // Determine the directory the .vtn file is in
const vtnDir = (filePath as string).replace(/[\\/][^\\/]+$/, ''); const vtnDir = (filePath as string).replace(/[\\/][^\\/]+$/, '');
const version = project.version ?? 1; const version = project.version ?? 1;
projectIsV2 = version >= 2;
// Resolve audio for wavesurfer playback // Resolve audio for wavesurfer playback
if (version >= 2) { if (version >= 2) {
@@ -734,7 +744,10 @@
</button> </button>
{#if $segments.length > 0} {#if $segments.length > 0}
<button class="settings-btn" onclick={saveProject}> <button class="settings-btn" onclick={saveProject}>
Save Project Save
</button>
<button class="settings-btn" onclick={saveProjectAs}>
Save As
</button> </button>
{/if} {/if}
<button class="import-btn" onclick={handleFileImport} disabled={isTranscribing}> <button class="import-btn" onclick={handleFileImport} disabled={isTranscribing}>