159 lines
5.2 KiB
TypeScript
159 lines
5.2 KiB
TypeScript
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<string, number>()
|
|
|
|
const defaultProjects = (): ProjectMeta[] => {
|
|
return []
|
|
}
|
|
|
|
const sanitizeProject = (item: Partial<ProjectMeta> | 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<ProjectRegistryPayload>
|
|
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
|
|
}
|