Files
misode.github.io/src/app/contexts/Project.tsx
2024-11-21 01:30:42 +01:00

144 lines
3.7 KiB
TypeScript

import type { ComponentChildren } from 'preact'
import { createContext } from 'preact'
import { useCallback, useContext, useEffect, useMemo, useState } from 'preact/hooks'
import type { VersionId } from '../services/index.js'
import { DRAFTS_URI, SpyglassClient } from '../services/Spyglass.js'
import { Store } from '../Store.js'
export type ProjectMeta = {
name: string,
namespace?: string,
version?: VersionId,
storage?: ProjectStorage,
/** @deprecated */
files?: ProjectFile[],
/** @deprecated */
unknownFiles?: UnknownFile[],
}
export type ProjectStorage = {
type: 'indexeddb',
rootUri: string,
}
type ProjectFile = {
type: string,
id: string,
data: any,
}
type UnknownFile = {
path: string,
data: string,
}
export const DRAFT_PROJECT: ProjectMeta = {
name: 'Drafts',
namespace: 'draft',
storage: {
type: 'indexeddb',
rootUri: DRAFTS_URI,
},
}
interface ProjectContext {
projects: ProjectMeta[],
project: ProjectMeta,
projectUri: string | undefined,
setProjectUri: (uri: string | undefined) => void,
createProject: (project: ProjectMeta) => void,
deleteProject: (name: string) => void,
changeProject: (name: string) => void,
updateProject: (project: Partial<ProjectMeta>) => void,
}
const Project = createContext<ProjectContext | undefined>(undefined)
export function useProject() {
const context = useContext(Project)
if (context === undefined) {
throw new Error('Cannot use project outside of provider')
}
return context
}
export function ProjectProvider({ children }: { children: ComponentChildren }) {
const [projects, setProjects] = useState<ProjectMeta[]>(Store.getProjects())
const [openProject, setOpenProject] = useState<string>()
useEffect(() => {
const initialProject = Store.getOpenProject()
const project = projects.find(p => p.name === initialProject)
if (!project) {
return
}
if (project.storage !== undefined) {
setOpenProject(initialProject)
return
}
// TODO: Import legacy projects
}, [])
const project = useMemo(() => {
return projects.find(p => p.name === openProject) ?? DRAFT_PROJECT
}, [projects, openProject])
const [projectUri, setProjectUri] = useState<string>()
const changeProjects = useCallback((projects: ProjectMeta[]) => {
Store.setProjects(projects)
setProjects(projects)
}, [])
const createProject = useCallback((project: ProjectMeta) => {
changeProjects([...projects, project])
}, [projects])
const deleteProject = useCallback(async (name: string) => {
if (name === DRAFT_PROJECT.name) return
const project = projects.find(p => p.name === name)
if (project) {
const projectRoot = getProjectRoot(project)
const entries = await SpyglassClient.FS.readdir(projectRoot)
await Promise.all(entries.flatMap(async e => {
if (e.name.startsWith(projectRoot)) {
return [await SpyglassClient.FS.unlink(e.name)]
}
return []
}))
}
changeProjects(projects.filter(p => p.name !== name))
setOpenProject(undefined)
}, [projects])
const changeProject = useCallback((name: string) => {
Store.setOpenProject(name)
setOpenProject(name)
}, [])
const updateProject = useCallback((edits: Partial<ProjectMeta>) => {
changeProjects(projects.map(p => p.name === openProject ? { ...p, ...edits } : p))
}, [projects, openProject])
const value: ProjectContext = {
projects,
project,
projectUri,
setProjectUri,
createProject,
changeProject,
deleteProject,
updateProject,
}
return <Project.Provider value={value}>
{children}
</Project.Provider>
}
export function getProjectRoot(project: ProjectMeta) {
if (project.storage?.type === 'indexeddb') {
return project.storage.rootUri
}
throw new Error(`Unsupported project storage ${project.storage?.type}`)
}