/ concept-collection / numbl-web-ide
Sign in
concept-collection / numbl-web-ide
numbl-web-ide / src / projects.ts
123 lines · 3.6 KBBlameHistoryRaw
1import { createIndexedDBFileSystem, type WorkspaceFileSystem } from 'minwebide';
3// The project registry: a small localStorage index of projects, each backed
4// by its own IndexedDB database (its own workspace file system).
6export interface ProjectInfo {
7 readonly id: string;
8 name: string;
9 createdAt: number;
10 lastOpenedAt: number;
13const REGISTRY_KEY = 'numbl-web-ide.projects';
15function readRegistry(): ProjectInfo[] {
16 try {
17 const raw = localStorage.getItem(REGISTRY_KEY);
18 const parsed = raw ? JSON.parse(raw) : [];
19 return Array.isArray(parsed) ? parsed : [];
20 } catch {
21 return [];
22 }
25function writeRegistry(projects: ProjectInfo[]): void {
26 localStorage.setItem(REGISTRY_KEY, JSON.stringify(projects));
29export function projectDbName(id: string): string {
30 return `numbl-web-ide-project-${id}`;
33export function listProjects(): ProjectInfo[] {
34 return readRegistry().sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
37export function getProject(id: string): ProjectInfo | undefined {
38 return readRegistry().find(p => p.id === id);
41/** Picks 'untitled', 'untitled-2', ... skipping names already in use. */
42export function nextUntitledName(base = 'untitled'): string {
43 const names = new Set(readRegistry().map(p => p.name));
44 if (!names.has(base)) {
45 return base;
46 }
47 for (let i = 2; ; i++) {
48 if (!names.has(`${base}-${i}`)) {
49 return `${base}-${i}`;
50 }
51 }
54export function createProject(name: string): ProjectInfo {
55 const project: ProjectInfo = {
56 id: Math.random().toString(36).slice(2, 10),
57 name,
58 createdAt: Date.now(),
59 lastOpenedAt: Date.now(),
60 };
61 writeRegistry([...readRegistry(), project]);
62 return project;
65export function renameProject(id: string, name: string): void {
66 const projects = readRegistry();
67 const project = projects.find(p => p.id === id);
68 if (project && name.trim()) {
69 project.name = name.trim();
70 writeRegistry(projects);
71 }
74export function touchProject(id: string): void {
75 const projects = readRegistry();
76 const project = projects.find(p => p.id === id);
77 if (project) {
78 project.lastOpenedAt = Date.now();
79 writeRegistry(projects);
80 }
83export async function deleteProject(id: string): Promise<void> {
84 writeRegistry(readRegistry().filter(p => p.id !== id));
85 await new Promise<void>((resolve) => {
86 const request = indexedDB.deleteDatabase(projectDbName(id));
87 request.onsuccess = request.onerror = request.onblocked = () => resolve();
88 });
91export async function openProjectFileSystem(id: string): Promise<WorkspaceFileSystem> {
92 return createIndexedDBFileSystem({ dbName: projectDbName(id) });
95/** Copies all files of one project into a brand-new project. */
96export async function duplicateProject(id: string): Promise<ProjectInfo | undefined> {
97 const source = getProject(id);
98 if (!source) {
99 return undefined;
100 }
101 const copy = createProject(nextUntitledName(`${source.name}-copy`));
102 const sourceFs = await openProjectFileSystem(source.id);
103 const targetFs = await openProjectFileSystem(copy.id);
104 try {
105 const copyTree = async (path: string): Promise<void> => {
106 const stat = await sourceFs.fileService.resolve(sourceFs.root.with({ path }));
107 for (const child of stat.children ?? []) {
108 if (child.isDirectory) {
109 await targetFs.fileService.createFolder(targetFs.root.with({ path: child.resource.path }));
110 await copyTree(child.resource.path);
111 } else {
112 const content = await sourceFs.fileService.readFile(child.resource);
113 await targetFs.fileService.writeFile(targetFs.root.with({ path: child.resource.path }), content.value);
114 }
115 }
116 };
117 await copyTree('/');
118 } finally {
119 sourceFs.dispose();
120 targetFs.dispose();
121 }
122 return copy;
moveopenescclose