import { DEFAULT_PROJECT_ID, QUICK_PROJECT_ID, normalizeProjectId } from '@/lib/workspace' import { i18n } from '@/i18n' const PROJECT_REGISTRY_KEY = 'jgjs-project-registry-v1' export interface ProjectMeta { id: string name: string createdAt: string updatedAt: string lastOpenedAt: string } type ProjectRegistryPayload = { projects: ProjectMeta[] } const nowIso = () => new Date().toISOString() const lastEditedTouchAt = new Map() const defaultProjects = (): ProjectMeta[] => { return [] } const sanitizeProject = (item: Partial | null | undefined): ProjectMeta | null => { if (!item || typeof item !== 'object') return null const id = normalizeProjectId(item.id) const name = String(item.name || '').trim() || ( id === QUICK_PROJECT_ID ? i18n.global.t('quickCalc.projectName') : i18n.global.t('tab.messages.projectNamePrefix', { id }) ) const createdAt = typeof item.createdAt === 'string' && item.createdAt ? item.createdAt : nowIso() const updatedAt = typeof item.updatedAt === 'string' && item.updatedAt ? item.updatedAt : createdAt const lastOpenedAt = typeof item.lastOpenedAt === 'string' && item.lastOpenedAt ? item.lastOpenedAt : updatedAt return { id, name, createdAt, updatedAt, lastOpenedAt } } const readPayload = (): ProjectRegistryPayload => { try { const raw = localStorage.getItem(PROJECT_REGISTRY_KEY) if (!raw) return { projects: defaultProjects() } const parsed = JSON.parse(raw) as Partial const projects = Array.isArray(parsed?.projects) ? parsed.projects.map(item => sanitizeProject(item)).filter((item): item is ProjectMeta => Boolean(item)) : [] if (projects.length === 0) return { projects: defaultProjects() } return { projects } } catch { return { projects: defaultProjects() } } } const writePayload = (payload: ProjectRegistryPayload) => { localStorage.setItem(PROJECT_REGISTRY_KEY, JSON.stringify(payload)) } export const listProjects = () => { const payload = readPayload() return payload.projects .filter(item => item.id !== QUICK_PROJECT_ID && item.id !== DEFAULT_PROJECT_ID) .slice() .sort((a, b) => new Date(b.lastOpenedAt).getTime() - new Date(a.lastOpenedAt).getTime()) } export const upsertProject = ( projectIdRaw: string, nameRaw?: string, options?: { touchUpdatedAt?: boolean touchLastOpenedAt?: boolean } ) => { const id = normalizeProjectId(projectIdRaw) if (id === QUICK_PROJECT_ID || id === DEFAULT_PROJECT_ID) return const name = String(nameRaw || '').trim() const payload = readPayload() const now = nowIso() const touchUpdatedAt = options?.touchUpdatedAt === true const touchLastOpenedAt = options?.touchLastOpenedAt !== false const index = payload.projects.findIndex(item => item.id === id) if (index < 0) { payload.projects.push({ id, name: name || ( id === QUICK_PROJECT_ID ? i18n.global.t('quickCalc.projectName') : i18n.global.t('tab.messages.projectNamePrefix', { id }) ), createdAt: now, updatedAt: now, lastOpenedAt: now }) } else { const current = payload.projects[index] payload.projects[index] = { ...current, name: name || current.name, updatedAt: touchUpdatedAt ? now : current.updatedAt, lastOpenedAt: touchLastOpenedAt ? now : current.lastOpenedAt } } writePayload(payload) } export const touchProjectEdited = ( projectIdRaw: string, options?: { throttleMs?: number } ) => { const id = normalizeProjectId(projectIdRaw) if (id === QUICK_PROJECT_ID || id === DEFAULT_PROJECT_ID) return false const throttleMs = Math.max(0, Number(options?.throttleMs ?? 5000)) const nowMs = Date.now() const lastTouch = lastEditedTouchAt.get(id) ?? 0 if (throttleMs > 0 && nowMs - lastTouch < throttleMs) return false const payload = readPayload() const index = payload.projects.findIndex(item => item.id === id) if (index < 0) return false const now = nowIso() const current = payload.projects[index] payload.projects[index] = { ...current, updatedAt: now, lastOpenedAt: now } writePayload(payload) lastEditedTouchAt.set(id, nowMs) return true } export const createProject = (nameRaw?: string) => { const payload = readPayload() const now = nowIso() let id = '' do { id = normalizeProjectId(`p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`) } while (payload.projects.some(item => item.id === id)) const name = String(nameRaw || '').trim() || i18n.global.t('tab.messages.projectNamePrefix', { id: payload.projects.length + 1 }) const project: ProjectMeta = { id, name, createdAt: now, updatedAt: now, lastOpenedAt: now } payload.projects.push(project) writePayload(payload) return project } export const deleteProject = (projectIdRaw: string) => { const id = normalizeProjectId(projectIdRaw) if (id === QUICK_PROJECT_ID || id === DEFAULT_PROJECT_ID) return false const payload = readPayload() const nextProjects = payload.projects.filter(item => item.id !== id) if (nextProjects.length === payload.projects.length) return false payload.projects = nextProjects writePayload(payload) return true }