/** * Loading, creating, switching and deleting projects, plus the two toolbar menus that trigger * it. Talks only to the store and the editor state – no widgets of its own beyond the menus. */ import { SetActiveSceneCommand } from '../core/commands/assetCommands'; import { listScenes } from '../core/model/Project'; import type { ProjectStore } from '../core/store/ProjectStore'; import { Dialog, Menu, type MenuItem, type Widget } from '../ui'; import type { EditorState } from './state'; export class ProjectSession { /** * `beforeSwitch` runs before a different project is loaded or the current one is deleted – * a run in progress has to end first, since it holds a copy of the project being replaced. */ constructor( private readonly state: EditorState, private readonly store: ProjectStore, private readonly beforeSwitch: () => void, ) {} /** Opens the project named in the URL, else the first one on the server, else a new one. */ async boot(): Promise { const wanted = new URLSearchParams(location.search).get('project'); try { if (wanted) { await this.open(wanted); return; } await this.openFirstOrCreate(); } catch (error) { this.state.setNotice( `Could not load project: ${error instanceof Error ? error.message : String(error)}`, ); } } private async open(id: string): Promise { this.beforeSwitch(); const loaded = await this.store.load(id); this.state.loadProject(loaded.project, loaded.revision); const url = new URL(location.href); url.searchParams.set('project', id); history.replaceState(null, '', url); } private async openFirstOrCreate(): Promise { const list = await this.store.list(); if (list[0]) await this.open(list[0].id); else await this.create('My first project'); } private async create(name: string): Promise { const created = await this.store.create(name); await this.open(created.project.id); } private async deleteCurrent(): Promise { this.beforeSwitch(); await this.store.delete(this.state.project.id); await this.openFirstOrCreate(); } openProjectMenu(anchor: Widget): void { const open = async () => { const list = await this.store.list(); const items: MenuItem[] = [ { label: 'New project', onSelect: () => void this.create(`Project ${list.length + 1}`) }, ...list.map((p) => ({ label: p.id === this.state.project.id ? `● ${p.name}` : p.name, onSelect: () => void this.open(p.id), disabled: p.id === this.state.project.id, })), { label: 'Delete this project…', danger: true, onSelect: () => new Dialog( `Delete project "${this.state.project.name}"?`, 'All assets and scenes are removed permanently.', [ { label: 'Cancel' }, { label: 'Delete', danger: true, onClick: () => void this.deleteCurrent() }, ], ).openCentered(), }, ]; new Menu(items).openBelow(anchor); }; void open().catch((error) => this.state.setNotice(String(error))); } openSceneMenu(anchor: Widget): void { const items: MenuItem[] = listScenes(this.state.project).map((scene) => ({ label: scene.id === this.state.project.activeSceneId ? `● ${scene.name}` : scene.name, onSelect: () => this.state.execute(new SetActiveSceneCommand(scene.id)), })); if (items.length === 0) items.push({ label: 'No scenes – create one in Assets', onSelect: () => {}, disabled: true }); new Menu(items).openBelow(anchor); } }