1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4import { create } from 'zustand';
6 type AssetLibraryTemplate,
7 createAssetLibraryFile,
8 createTemplateFromScenarioItem,
10 prepareTemplatesForCatalog,
11} from './assetTemplates';
12import { useScenarioStore } from './scenarioStore';
14const CATALOG_FILENAME = 'asset-library.json';
15const LOCAL_STORAGE_KEY = 'fers.assetLibrary.v1';
17type AssetLibraryState = {
18 templates: AssetLibraryTemplate[];
22 loadCatalog: () => Promise<void>;
26 ) => Promise<AssetLibraryTemplate | null>;
27 addTemplates: (templates: AssetLibraryTemplate[]) => Promise<number>;
28 updateTemplateName: (templateId: string, name: string) => Promise<void>;
29 deleteTemplate: (templateId: string) => Promise<void>;
32function canUseLocalStorage(): boolean {
33 return typeof globalThis.localStorage !== 'undefined';
36function readLocalCatalog(): AssetLibraryTemplate[] {
37 if (!canUseLocalStorage()) {
41 const raw = globalThis.localStorage.getItem(LOCAL_STORAGE_KEY);
46 return parseAssetTemplates(JSON.parse(raw));
49function writeLocalCatalog(templates: AssetLibraryTemplate[]): void {
50 if (!canUseLocalStorage()) {
54 globalThis.localStorage.setItem(
56 JSON.stringify(createAssetLibraryFile(templates), null, 2)
60function isMissingCatalogError(error: unknown): boolean {
61 const message = error instanceof Error ? error.message : String(error);
63 message.includes('not found') ||
64 message.includes('No such file') ||
65 message.includes('os error 2') ||
66 message.includes('ENOENT')
70async function getTauriCatalogLocation(): Promise<{
74 const { appDataDir, join } = await import('@tauri-apps/api/path');
75 const directory = await appDataDir();
78 filePath: await join(directory, CATALOG_FILENAME),
82async function readCatalog(): Promise<AssetLibraryTemplate[]> {
84 const { readTextFile } = await import('@tauri-apps/plugin-fs');
85 const { filePath } = await getTauriCatalogLocation();
86 const raw = await readTextFile(filePath);
87 return parseAssetTemplates(JSON.parse(raw));
89 if (isMissingCatalogError(error)) {
92 return readLocalCatalog();
96async function writeCatalog(templates: AssetLibraryTemplate[]): Promise<void> {
98 const { mkdir, writeTextFile } = await import('@tauri-apps/plugin-fs');
99 const { directory, filePath } = await getTauriCatalogLocation();
100 await mkdir(directory, { recursive: true });
103 JSON.stringify(createAssetLibraryFile(templates), null, 2)
106 writeLocalCatalog(templates);
110function sortTemplates(
111 templates: AssetLibraryTemplate[]
112): AssetLibraryTemplate[] {
113 return [...templates].sort((a, b) => {
114 const kindCompare = a.kind.localeCompare(b.kind);
115 if (kindCompare !== 0) {
118 return a.name.localeCompare(b.name);
122export const useAssetLibraryStore = create<AssetLibraryState>()((set, get) => ({
127 loadCatalog: async () => {
128 set({ isLoading: true, error: null });
130 const templates = await readCatalog();
132 templates: sortTemplates(templates),
139 error instanceof Error ? error.message : String(error);
140 set({ isLoading: false, error: message });
143 saveScenarioItem: async (itemId, nameOverride) => {
144 if (!get().hasLoaded) {
145 const templates = await readCatalog();
146 set({ templates: sortTemplates(templates), hasLoaded: true });
149 const scenarioState = useScenarioStore.getState();
150 const template = createTemplateFromScenarioItem(scenarioState, itemId);
154 const trimmedName = nameOverride?.trim();
155 const templateToSave = trimmedName
156 ? { ...template, name: trimmedName }
159 const templates = sortTemplates([...get().templates, templateToSave]);
160 set({ templates, error: null });
161 await writeCatalog(templates);
162 return templateToSave;
164 addTemplates: async (incomingTemplates) => {
165 if (!get().hasLoaded) {
166 const templates = await readCatalog();
167 set({ templates: sortTemplates(templates), hasLoaded: true });
170 const templatesForCatalog =
171 prepareTemplatesForCatalog(incomingTemplates);
172 const templates = sortTemplates([
174 ...templatesForCatalog,
176 set({ templates, error: null });
177 await writeCatalog(templates);
178 return templatesForCatalog.length;
180 updateTemplateName: async (templateId, name) => {
181 if (!get().hasLoaded) {
182 const templates = await readCatalog();
183 set({ templates: sortTemplates(templates), hasLoaded: true });
186 const trimmedName = name.trim();
191 const updatedAt = new Date().toISOString();
192 const templates = sortTemplates(
193 get().templates.map((template) =>
194 template.id === templateId
195 ? { ...template, name: trimmedName, updatedAt }
199 set({ templates, error: null });
200 await writeCatalog(templates);
202 deleteTemplate: async (templateId) => {
203 if (!get().hasLoaded) {
204 const templates = await readCatalog();
205 set({ templates: sortTemplates(templates), hasLoaded: true });
208 const templates = get().templates.filter(
209 (template) => template.id !== templateId
211 set({ templates, error: null });
212 await writeCatalog(templates);
216export { createAssetLibraryFile, parseAssetTemplates };