FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
assetLibrary.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4import { create } from 'zustand';
5import {
6 type AssetLibraryTemplate,
7 createAssetLibraryFile,
8 createTemplateFromScenarioItem,
9 parseAssetTemplates,
10 prepareTemplatesForCatalog,
11} from './assetTemplates';
12import { useScenarioStore } from './scenarioStore';
13
14const CATALOG_FILENAME = 'asset-library.json';
15const LOCAL_STORAGE_KEY = 'fers.assetLibrary.v1';
16
17type AssetLibraryState = {
18 templates: AssetLibraryTemplate[];
19 isLoading: boolean;
20 hasLoaded: boolean;
21 error: string | null;
22 loadCatalog: () => Promise<void>;
23 saveScenarioItem: (
24 itemId: string,
25 nameOverride?: string
26 ) => Promise<AssetLibraryTemplate | null>;
27 addTemplates: (templates: AssetLibraryTemplate[]) => Promise<number>;
28 updateTemplateName: (templateId: string, name: string) => Promise<void>;
29 deleteTemplate: (templateId: string) => Promise<void>;
30};
31
32function canUseLocalStorage(): boolean {
33 return typeof globalThis.localStorage !== 'undefined';
34}
35
36function readLocalCatalog(): AssetLibraryTemplate[] {
37 if (!canUseLocalStorage()) {
38 return [];
39 }
40
41 const raw = globalThis.localStorage.getItem(LOCAL_STORAGE_KEY);
42 if (!raw) {
43 return [];
44 }
45
46 return parseAssetTemplates(JSON.parse(raw));
47}
48
49function writeLocalCatalog(templates: AssetLibraryTemplate[]): void {
50 if (!canUseLocalStorage()) {
51 return;
52 }
53
54 globalThis.localStorage.setItem(
55 LOCAL_STORAGE_KEY,
56 JSON.stringify(createAssetLibraryFile(templates), null, 2)
57 );
58}
59
60function isMissingCatalogError(error: unknown): boolean {
61 const message = error instanceof Error ? error.message : String(error);
62 return (
63 message.includes('not found') ||
64 message.includes('No such file') ||
65 message.includes('os error 2') ||
66 message.includes('ENOENT')
67 );
68}
69
70async function getTauriCatalogLocation(): Promise<{
71 directory: string;
72 filePath: string;
73}> {
74 const { appDataDir, join } = await import('@tauri-apps/api/path');
75 const directory = await appDataDir();
76 return {
77 directory,
78 filePath: await join(directory, CATALOG_FILENAME),
79 };
80}
81
82async function readCatalog(): Promise<AssetLibraryTemplate[]> {
83 try {
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));
88 } catch (error) {
89 if (isMissingCatalogError(error)) {
90 return [];
91 }
92 return readLocalCatalog();
93 }
94}
95
96async function writeCatalog(templates: AssetLibraryTemplate[]): Promise<void> {
97 try {
98 const { mkdir, writeTextFile } = await import('@tauri-apps/plugin-fs');
99 const { directory, filePath } = await getTauriCatalogLocation();
100 await mkdir(directory, { recursive: true });
101 await writeTextFile(
102 filePath,
103 JSON.stringify(createAssetLibraryFile(templates), null, 2)
104 );
105 } catch (_error) {
106 writeLocalCatalog(templates);
107 }
108}
109
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) {
116 return kindCompare;
117 }
118 return a.name.localeCompare(b.name);
119 });
120}
121
122export const useAssetLibraryStore = create<AssetLibraryState>()((set, get) => ({
123 templates: [],
124 isLoading: false,
125 hasLoaded: false,
126 error: null,
127 loadCatalog: async () => {
128 set({ isLoading: true, error: null });
129 try {
130 const templates = await readCatalog();
131 set({
132 templates: sortTemplates(templates),
133 isLoading: false,
134 hasLoaded: true,
135 error: null,
136 });
137 } catch (error) {
138 const message =
139 error instanceof Error ? error.message : String(error);
140 set({ isLoading: false, error: message });
141 }
142 },
143 saveScenarioItem: async (itemId, nameOverride) => {
144 if (!get().hasLoaded) {
145 const templates = await readCatalog();
146 set({ templates: sortTemplates(templates), hasLoaded: true });
147 }
148
149 const scenarioState = useScenarioStore.getState();
150 const template = createTemplateFromScenarioItem(scenarioState, itemId);
151 if (!template) {
152 return null;
153 }
154 const trimmedName = nameOverride?.trim();
155 const templateToSave = trimmedName
156 ? { ...template, name: trimmedName }
157 : template;
158
159 const templates = sortTemplates([...get().templates, templateToSave]);
160 set({ templates, error: null });
161 await writeCatalog(templates);
162 return templateToSave;
163 },
164 addTemplates: async (incomingTemplates) => {
165 if (!get().hasLoaded) {
166 const templates = await readCatalog();
167 set({ templates: sortTemplates(templates), hasLoaded: true });
168 }
169
170 const templatesForCatalog =
171 prepareTemplatesForCatalog(incomingTemplates);
172 const templates = sortTemplates([
173 ...get().templates,
174 ...templatesForCatalog,
175 ]);
176 set({ templates, error: null });
177 await writeCatalog(templates);
178 return templatesForCatalog.length;
179 },
180 updateTemplateName: async (templateId, name) => {
181 if (!get().hasLoaded) {
182 const templates = await readCatalog();
183 set({ templates: sortTemplates(templates), hasLoaded: true });
184 }
185
186 const trimmedName = name.trim();
187 if (!trimmedName) {
188 return;
189 }
190
191 const updatedAt = new Date().toISOString();
192 const templates = sortTemplates(
193 get().templates.map((template) =>
194 template.id === templateId
195 ? { ...template, name: trimmedName, updatedAt }
196 : template
197 )
198 );
199 set({ templates, error: null });
200 await writeCatalog(templates);
201 },
202 deleteTemplate: async (templateId) => {
203 if (!get().hasLoaded) {
204 const templates = await readCatalog();
205 set({ templates: sortTemplates(templates), hasLoaded: true });
206 }
207
208 const templates = get().templates.filter(
209 (template) => template.id !== templateId
210 );
211 set({ templates, error: null });
212 await writeCatalog(templates);
213 },
214}));
215
216export { createAssetLibraryFile, parseAssetTemplates };