diff --git a/src/App.tsx b/src/App.tsx index 4426ebd..5868f96 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,8 @@ import type { RenderingEvent } from './core/audio-interface/KGOfflineRenderer'; import { KGCore } from './core/KGCore'; import { ConfigManager } from './core/config/ConfigManager'; import { validateFunctionalChordsJSON } from './util/scaleUtil'; +import { KGProjectStorage } from './core/io/KGProjectStorage'; +import { RESERVED_PROJECT_NAME } from './util/projectNameUtil'; function App() { // Enable global keyboard handler for copy/paste and undo/redo @@ -42,6 +44,16 @@ function App() { hasInitialized.current = true; const initializeApp = async () => { + // Wipe the reserved "Untitled Project" OPFS folder on every startup so it stays ephemeral + try { + const storage = KGProjectStorage.getInstance(); + if (await storage.exists(RESERVED_PROJECT_NAME)) { + await storage.delete(RESERVED_PROJECT_NAME); + } + } catch (error) { + console.warn('Could not clear Untitled Project folder on startup:', error); + } + // Load the current project from KGCore loadProject(null); diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 7374105..8963b18 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -2,7 +2,7 @@ import React from 'react'; import './Toolbar.css'; import { saveProject } from '../util/saveUtil'; import { KGProjectStorage } from '../core/io/KGProjectStorage'; -import { isValidProjectName } from '../util/projectNameUtil'; +import { isValidProjectName, isReservedProjectName, RESERVED_PROJECT_NAME } from '../util/projectNameUtil'; import { KGCore } from '../core/KGCore'; import { useProjectStore } from '../stores/projectStore'; import { DEBUG_MODE } from '../constants/uiConstants'; @@ -86,15 +86,43 @@ const Toolbar: React.FC = () => { // Export options const exportOptions = ["Export to KGStudio file", "Export to MIDI file", "Export to WAV", "Export to MP3"]; - const handleProjectNameClick = () => { + const handleProjectNameClick = async () => { const newName = prompt("Enter project name:", projectName); - if (newName) { - if (!isValidProjectName(newName)) { - window.alert("Invalid project name. Only letters, numbers, spaces, hyphens, underscores, periods, and parentheses are allowed."); + if (!newName) return; + if (!isValidProjectName(newName)) { + window.alert("Invalid project name. Only letters, numbers, spaces, hyphens, underscores, periods, and parentheses are allowed."); + return; + } + if (isReservedProjectName(newName)) { + window.alert(`"${RESERVED_PROJECT_NAME}" is a reserved project name. Please choose a different name.`); + return; + } + + // Conflict check: only relevant when targeting a different OPFS folder + if (newName !== savedProjectName) { + const storage = KGProjectStorage.getInstance(); + const exists = await storage.exists(newName); + if (exists) { + const confirmed = window.confirm( + `Project "${newName}" already exists. Do you want to overwrite it?` + ); + if (!confirmed) return; + // Confirmed: update in-memory name then save immediately, overwriting the existing project + setProjectName(newName); + await saveProject(newName, savedProjectName, setStatus, (finalName) => { + setSavedProjectName(finalName); + if (finalName !== newName) setProjectName(finalName); + }, true /* forceOverwrite */); return; } - setProjectName(newName); } + + // Name is available — update and save immediately + setProjectName(newName); + await saveProject(newName, savedProjectName, setStatus, (finalName) => { + setSavedProjectName(finalName); + if (finalName !== newName) setProjectName(finalName); + }); }; // Common project loading logic extracted for reuse diff --git a/src/core/io/KGProjectStorage.ts b/src/core/io/KGProjectStorage.ts index 0ef41f1..2083a6d 100644 --- a/src/core/io/KGProjectStorage.ts +++ b/src/core/io/KGProjectStorage.ts @@ -351,11 +351,11 @@ export class KGProjectStorage { * Used when the user renames the project and saves. Handles the case where the * old folder doesn't exist yet (new project never saved). */ - public async saveWithRename(oldName: string, newName: string, data: KGProject): Promise { + public async saveWithRename(oldName: string, newName: string, data: KGProject, overwrite: boolean = false): Promise { this.ensureInitialized(); // Save project JSON to the new folder - await this.save(newName, data, false); + await this.save(newName, data, overwrite); // Migrate media files only if the old folder exists if (await this.exists(oldName)) { diff --git a/src/util/projectNameUtil.ts b/src/util/projectNameUtil.ts index 6f8739c..549ab98 100644 --- a/src/util/projectNameUtil.ts +++ b/src/util/projectNameUtil.ts @@ -9,6 +9,12 @@ const VALID_PROJECT_NAME_REGEX = /^[a-zA-Z0-9 \-_.()\u00C0-\u024F]+$/; */ const DISALLOWED_CHARS_REGEX = /[^a-zA-Z0-9 \-_.()\u00C0-\u024F]/g; +/** + * The reserved project name used for unsaved/new projects. + * Users cannot rename a project to this name, and its OPFS folder is wiped on every startup. + */ +export const RESERVED_PROJECT_NAME = "Untitled Project"; + /** * Validate whether a project name contains only allowed characters. * Does NOT check for empty string — caller should check that separately. @@ -19,6 +25,13 @@ export function isValidProjectName(name: string): boolean { return VALID_PROJECT_NAME_REGEX.test(name); } +/** + * Returns true if the given name matches the reserved "Untitled Project" name. + */ +export function isReservedProjectName(name: string): boolean { + return name.trim() === RESERVED_PROJECT_NAME; +} + /** * Sanitize a project name by replacing disallowed characters with underscores, * collapsing consecutive underscores/spaces, and trimming. @@ -37,7 +50,7 @@ export function sanitizeProjectName(name: string): string { // If everything was stripped, provide a fallback if (sanitized.length === 0) { - sanitized = 'Untitled Project'; + sanitized = RESERVED_PROJECT_NAME; } return sanitized; diff --git a/src/util/saveUtil.ts b/src/util/saveUtil.ts index a394744..b7075e8 100644 --- a/src/util/saveUtil.ts +++ b/src/util/saveUtil.ts @@ -1,5 +1,6 @@ import { KGProjectStorage, DuplicateEntryError } from '../core/io/KGProjectStorage'; import { KGCore } from '../core/KGCore'; +import { RESERVED_PROJECT_NAME } from './projectNameUtil'; /** * Save project utility function. @@ -17,14 +18,27 @@ export const saveProject = async ( savedProjectName: string, setStatus: (status: string) => void, onSaveSuccess: (finalName: string) => void, + forceOverwrite: boolean = false, ): Promise => { const storage = KGProjectStorage.getInstance(); + + // Auto-rename reserved "Untitled Project" to "Untitled Project (1)", "(2)", etc. + if (projectName === RESERVED_PROJECT_NAME) { + let counter = 1; + let autoName = `${RESERVED_PROJECT_NAME} (${counter})`; + while (await storage.exists(autoName)) { + counter++; + autoName = `${RESERVED_PROJECT_NAME} (${counter})`; + } + projectName = autoName; + } + const isRename = savedProjectName !== projectName; if (isRename) { - // Determine the target name, resolving conflicts automatically + // Determine the target name; skip conflict resolution when caller already confirmed overwrite let targetName = projectName; - if (await storage.exists(projectName)) { + if (!forceOverwrite && await storage.exists(projectName)) { targetName = await storage.resolveUniqueName(projectName); } @@ -33,6 +47,7 @@ export const saveProject = async ( savedProjectName, targetName, KGCore.instance().getCurrentProject(), + forceOverwrite, ); const statusMsg =