BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: improve settings feedback handling

+300 -137
+4 -1
src/components/search/EmbeddingsSettings.test.tsx
··· 20 20 }), 21 21 ); 22 22 23 - vi.mock("$/lib/api/settings", () => ({ getSettings: getSettingsMock, updateSetting: updateSettingMock })); 23 + vi.mock( 24 + "$/lib/api/settings", 25 + () => ({ SettingsController: { getSettings: getSettingsMock, updateSetting: updateSettingMock } }), 26 + ); 24 27 25 28 vi.mock("@tauri-apps/plugin-log", () => ({ info: vi.fn(), error: vi.fn(), warn: vi.fn() })); 26 29
+190 -67
src/components/settings/SettingsData.tsx
··· 1 - import { exportData } from "$/lib/api/settings"; 2 - import { formatBytes } from "$/lib/utils/text"; 1 + import { SettingsController } from "$/lib/api/settings"; 2 + import { formatBytes, normalizeError } from "$/lib/utils/text"; 3 + import { createSignal } from "solid-js"; 3 4 import { SettingsCard } from "./SettingsCard"; 5 + import { SettingsInlineFeedback, useTransientFeedback } from "./SettingsInlineFeedback"; 6 + 7 + type CacheScope = "feeds" | "embeddings" | "fts" | "all"; 8 + 9 + type PendingAction = CacheScope | "json" | "csv" | null; 4 10 5 11 type SettingsDataProps = { 6 12 cacheSize: { feedsBytes: number; embeddingsBytes: number; ftsBytes: number; totalBytes?: number } | null; 7 - handleClearCache: (scope: "feeds" | "embeddings" | "fts" | "all") => Promise<void>; 13 + handleClearCache: (scope: CacheScope) => Promise<void>; 8 14 openConfirmation: ( 9 15 options: { 10 16 title: string; ··· 15 21 }, 16 22 ) => void; 17 23 }; 24 + 25 + type PendingCheck = (action: Exclude<PendingAction, null>) => boolean; 26 + 18 27 export function SettingsData(props: SettingsDataProps) { 19 - const cacheSize = () => props.cacheSize; 28 + const [pendingAction, setPendingAction] = createSignal<PendingAction>(null); 29 + const { feedback, dismissFeedback, queueFeedback } = useTransientFeedback(); 30 + const pending = (action: Exclude<PendingAction, null>) => pendingAction() === action; 31 + const busy = () => pendingAction() !== null; 32 + 33 + async function runClearCache(scope: CacheScope) { 34 + if (busy()) { 35 + return; 36 + } 37 + 38 + setPendingAction(scope); 39 + dismissFeedback(); 40 + try { 41 + await props.handleClearCache(scope); 42 + queueFeedback({ kind: "success", message: toClearSuccessMessage(scope) }); 43 + } catch (error) { 44 + queueFeedback({ kind: "error", message: toClearErrorMessage(error) }); 45 + } finally { 46 + setPendingAction(null); 47 + } 48 + } 49 + 50 + async function runExport(format: "json" | "csv") { 51 + if (busy()) { 52 + return; 53 + } 54 + 55 + setPendingAction(format); 56 + dismissFeedback(); 57 + try { 58 + await SettingsController.exportData(format); 59 + queueFeedback({ kind: "success", message: toExportSuccessMessage(format) }); 60 + } catch (error) { 61 + queueFeedback({ kind: "error", message: toExportErrorMessage(error) }); 62 + } finally { 63 + setPendingAction(null); 64 + } 65 + } 20 66 21 67 return ( 22 68 <SettingsCard icon="db" title="Data"> 23 69 <div class="grid gap-4"> 24 - <div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4"> 25 - <div class="rounded-xl bg-black/30 p-4 text-center"> 26 - <p class="text-lg font-medium text-on-surface">{formatBytes(cacheSize()?.feedsBytes ?? 0)}</p> 27 - <p class="text-xs text-on-surface-variant">Feeds cache</p> 28 - </div> 29 - <div class="rounded-xl bg-black/30 p-4 text-center"> 30 - <p class="text-lg font-medium text-on-surface">{formatBytes(cacheSize()?.embeddingsBytes ?? 0)}</p> 31 - <p class="text-xs text-on-surface-variant">Embeddings</p> 32 - </div> 33 - <div class="rounded-xl bg-black/30 p-4 text-center"> 34 - <p class="text-lg font-medium text-on-surface">{formatBytes(cacheSize()?.ftsBytes ?? 0)}</p> 35 - <p class="text-xs text-on-surface-variant">Search index</p> 36 - </div> 37 - <div class="rounded-xl bg-black/30 p-4 text-center"> 38 - <p class="text-lg font-medium text-on-surface">{formatBytes(cacheSize()?.totalBytes ?? 0)}</p> 39 - <p class="text-xs text-on-surface-variant">Total local data</p> 40 - </div> 41 - </div> 42 - <div class="flex gap-4"> 43 - <button 44 - type="button" 45 - onClick={() => void props.handleClearCache("feeds")} 46 - class="flex-1 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5"> 47 - Clear feeds 48 - </button> 49 - <button 50 - type="button" 51 - onClick={() => void props.handleClearCache("embeddings")} 52 - class="flex-1 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5"> 53 - Clear embeddings 54 - </button> 55 - <button 56 - type="button" 57 - onClick={() => void props.handleClearCache("fts")} 58 - class="flex-1 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5"> 59 - Clear search index 60 - </button> 61 - <button 62 - type="button" 63 - onClick={() => 64 - props.openConfirmation({ 65 - title: "Clear All Cache", 66 - message: 67 - "This will delete all cached data including feeds, embeddings, and search index. This action cannot be undone.", 68 - type: "danger", 69 - onConfirm: () => void props.handleClearCache("all"), 70 - })} 71 - class="flex-1 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-2 text-sm font-medium text-red-400 transition hover:bg-red-500/20"> 72 - Clear all 73 - </button> 74 - </div> 75 - <ExportControl /> 70 + <CacheSizeGrid cacheSize={props.cacheSize} /> 71 + <CacheActions 72 + busy={busy()} 73 + pending={pending} 74 + openConfirmation={props.openConfirmation} 75 + onClear={runClearCache} /> 76 + <ExportActions busy={busy()} pending={pending} onExport={runExport} /> 77 + <SettingsInlineFeedback feedback={feedback()} /> 76 78 </div> 77 79 </SettingsCard> 78 80 ); 79 81 } 80 82 81 - function ExportControl() { 83 + function CacheSizeGrid(props: { cacheSize: SettingsDataProps["cacheSize"] }) { 84 + return ( 85 + <div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4"> 86 + <CacheTile label="Feeds cache" value={formatBytes(props.cacheSize?.feedsBytes ?? 0)} /> 87 + <CacheTile label="Embeddings" value={formatBytes(props.cacheSize?.embeddingsBytes ?? 0)} /> 88 + <CacheTile label="Search index" value={formatBytes(props.cacheSize?.ftsBytes ?? 0)} /> 89 + <CacheTile label="Total local data" value={formatBytes(props.cacheSize?.totalBytes ?? 0)} /> 90 + </div> 91 + ); 92 + } 93 + 94 + function CacheTile(props: { label: string; value: string }) { 95 + return ( 96 + <div class="rounded-xl bg-black/30 p-4 text-center"> 97 + <p class="text-lg font-medium text-on-surface">{props.value}</p> 98 + <p class="text-xs text-on-surface-variant">{props.label}</p> 99 + </div> 100 + ); 101 + } 102 + 103 + function CacheActions( 104 + props: { 105 + busy: boolean; 106 + pending: PendingCheck; 107 + onClear: (scope: CacheScope) => Promise<void>; 108 + openConfirmation: SettingsDataProps["openConfirmation"]; 109 + }, 110 + ) { 111 + return ( 112 + <div class="flex gap-4"> 113 + <button 114 + type="button" 115 + disabled={props.busy} 116 + onClick={() => void props.onClear("feeds")} 117 + class="flex-1 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5 disabled:cursor-wait disabled:opacity-60"> 118 + {props.pending("feeds") ? "Clearing..." : "Clear feeds"} 119 + </button> 120 + <button 121 + type="button" 122 + disabled={props.busy} 123 + onClick={() => void props.onClear("embeddings")} 124 + class="flex-1 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5 disabled:cursor-wait disabled:opacity-60"> 125 + {props.pending("embeddings") ? "Clearing..." : "Clear embeddings"} 126 + </button> 127 + <button 128 + type="button" 129 + disabled={props.busy} 130 + onClick={() => void props.onClear("fts")} 131 + class="flex-1 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5 disabled:cursor-wait disabled:opacity-60"> 132 + {props.pending("fts") ? "Clearing..." : "Clear search index"} 133 + </button> 134 + <button 135 + type="button" 136 + disabled={props.busy} 137 + onClick={() => 138 + props.openConfirmation({ 139 + title: "Clear All Cache", 140 + message: 141 + "This will delete all cached data including feeds, embeddings, and search index. This action cannot be undone.", 142 + type: "danger", 143 + onConfirm: () => void props.onClear("all"), 144 + })} 145 + class="flex-1 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-2 text-sm font-medium text-red-400 transition hover:bg-red-500/20 disabled:cursor-wait disabled:opacity-60"> 146 + {props.pending("all") ? "Clearing..." : "Clear all"} 147 + </button> 148 + </div> 149 + ); 150 + } 151 + 152 + function ExportActions( 153 + props: { busy: boolean; pending: PendingCheck; onExport: (format: "json" | "csv") => Promise<void> }, 154 + ) { 82 155 return ( 83 156 <div class="border-t border-white/10 pt-4"> 84 157 <div class="flex items-center justify-between"> 85 - <div> 86 - <p class="text-sm font-medium text-on-surface">Export your data</p> 87 - <p class="text-xs text-on-surface-variant">Download all your data as JSON or CSV</p> 88 - </div> 158 + <ExportDescription /> 89 159 <div class="flex gap-2"> 90 160 <button 91 161 type="button" 92 - onClick={() => void exportData("json")} 93 - class="rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5"> 94 - JSON 162 + disabled={props.busy} 163 + onClick={() => void props.onExport("json")} 164 + class="rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5 disabled:cursor-wait disabled:opacity-60"> 165 + {props.pending("json") ? "Exporting..." : "JSON"} 95 166 </button> 96 167 <button 97 168 type="button" 98 - onClick={() => void exportData("csv")} 99 - class="rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5"> 100 - CSV 169 + disabled={props.busy} 170 + onClick={() => void props.onExport("csv")} 171 + class="rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5 disabled:cursor-wait disabled:opacity-60"> 172 + {props.pending("csv") ? "Exporting..." : "CSV"} 101 173 </button> 102 174 </div> 103 175 </div> 104 176 </div> 105 177 ); 106 178 } 179 + 180 + function ExportDescription() { 181 + return ( 182 + <div> 183 + <p class="text-sm font-medium text-on-surface">Export your data</p> 184 + <p class="text-xs text-on-surface-variant">Download all your data as JSON or CSV</p> 185 + </div> 186 + ); 187 + } 188 + 189 + function toClearSuccessMessage(scope: CacheScope) { 190 + switch (scope) { 191 + case "feeds": { 192 + return "Cleared feeds cache."; 193 + } 194 + case "embeddings": { 195 + return "Cleared embeddings cache."; 196 + } 197 + case "fts": { 198 + return "Cleared search index cache."; 199 + } 200 + case "all": { 201 + return "Cleared all local cache."; 202 + } 203 + default: { 204 + return "Cleared cache."; 205 + } 206 + } 207 + } 208 + 209 + function toClearErrorMessage(error: unknown) { 210 + const message = normalizeError(error); 211 + if (/cache|clear/iu.test(message)) { 212 + return "Couldn't clear cache right now."; 213 + } 214 + 215 + return "Couldn't update local data right now."; 216 + } 217 + 218 + function toExportSuccessMessage(format: "json" | "csv") { 219 + return format === "json" ? "Exported data as JSON." : "Exported data as CSV."; 220 + } 221 + 222 + function toExportErrorMessage(error: unknown) { 223 + const message = normalizeError(error); 224 + if (/export|path|file|save|directory/iu.test(message)) { 225 + return "Couldn't export data — check that the destination path is valid."; 226 + } 227 + 228 + return "Couldn't export data right now."; 229 + }
+4 -44
src/components/settings/SettingsDownloads.tsx
··· 3 3 import { normalizeError } from "$/lib/utils/text"; 4 4 import { open } from "@tauri-apps/plugin-dialog"; 5 5 import * as logger from "@tauri-apps/plugin-log"; 6 - import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js"; 7 - import { Icon } from "../shared/Icon"; 6 + import { createEffect, createSignal, onMount } from "solid-js"; 8 7 import { SettingsCard } from "./SettingsCard"; 8 + import { SettingsInlineFeedback, useTransientFeedback } from "./SettingsInlineFeedback"; 9 9 10 10 type SettingsDownloadsProps = { settings: AppSettings | null }; 11 11 12 - type DirectoryFeedback = { kind: "error" | "success"; message: string }; 13 - 14 12 export function SettingsDownloads(props: SettingsDownloadsProps) { 15 13 const [directory, setDirectory] = createSignal(""); 16 14 const [pending, setPending] = createSignal(false); 17 - const [feedback, setFeedback] = createSignal<DirectoryFeedback | null>(null); 18 - let feedbackTimer: ReturnType<typeof setTimeout> | null = null; 15 + const { feedback, dismissFeedback, queueFeedback } = useTransientFeedback(); 19 16 20 17 createEffect(() => { 21 18 const currentDirectory = directory(); ··· 28 25 onMount(() => { 29 26 void refreshDirectory(); 30 27 }); 31 - 32 - onCleanup(() => { 33 - if (feedbackTimer !== null) { 34 - clearTimeout(feedbackTimer); 35 - } 36 - }); 37 - 38 - function dismissFeedback() { 39 - setFeedback(null); 40 - if (feedbackTimer !== null) { 41 - clearTimeout(feedbackTimer); 42 - feedbackTimer = null; 43 - } 44 - } 45 - 46 - function queueFeedback(nextFeedback: DirectoryFeedback) { 47 - dismissFeedback(); 48 - setFeedback(nextFeedback); 49 - feedbackTimer = setTimeout(() => { 50 - setFeedback(null); 51 - feedbackTimer = null; 52 - }, 5000); 53 - } 54 28 55 29 async function refreshDirectory() { 56 30 try { ··· 139 113 </button> 140 114 </div> 141 115 142 - <Show when={feedback()}> 143 - {(currentFeedback) => ( 144 - <div 145 - role={currentFeedback().kind === "error" ? "alert" : "status"} 146 - aria-live={currentFeedback().kind === "error" ? "assertive" : "polite"} 147 - class="inline-flex w-fit items-center gap-2 rounded-full bg-surface-container-high px-3 py-1.5 text-sm" 148 - classList={{ 149 - "text-emerald-300": currentFeedback().kind === "success", 150 - "text-red-300": currentFeedback().kind === "error", 151 - }}> 152 - <Icon kind={currentFeedback().kind === "success" ? "complete" : "danger"} aria-hidden="true" /> 153 - <span>{currentFeedback().message}</span> 154 - </div> 155 - )} 156 - </Show> 116 + <SettingsInlineFeedback feedback={feedback()} /> 157 117 </div> 158 118 </SettingsCard> 159 119 );
+55
src/components/settings/SettingsInlineFeedback.tsx
··· 1 + import { createSignal, onCleanup, Show } from "solid-js"; 2 + import { Icon } from "../shared/Icon"; 3 + 4 + export type SettingsFeedback = { kind: "error" | "success"; message: string }; 5 + 6 + export function useTransientFeedback(timeoutMs = 5000) { 7 + const [feedback, setFeedback] = createSignal<SettingsFeedback | null>(null); 8 + let timer: ReturnType<typeof setTimeout> | null = null; 9 + 10 + onCleanup(() => { 11 + if (timer !== null) { 12 + clearTimeout(timer); 13 + } 14 + }); 15 + 16 + function dismissFeedback() { 17 + setFeedback(null); 18 + if (timer !== null) { 19 + clearTimeout(timer); 20 + timer = null; 21 + } 22 + } 23 + 24 + function queueFeedback(nextFeedback: SettingsFeedback) { 25 + dismissFeedback(); 26 + setFeedback(nextFeedback); 27 + timer = setTimeout(() => { 28 + setFeedback(null); 29 + timer = null; 30 + }, timeoutMs); 31 + } 32 + 33 + return { feedback, dismissFeedback, queueFeedback }; 34 + } 35 + 36 + export function SettingsInlineFeedback(props: { feedback: SettingsFeedback | null }) { 37 + return ( 38 + <Show when={props.feedback}> 39 + {(currentFeedback) => { 40 + const message = currentFeedback().message; 41 + const kind = currentFeedback().kind; 42 + return ( 43 + <div 44 + role={kind === "error" ? "alert" : "status"} 45 + aria-live={kind === "error" ? "assertive" : "polite"} 46 + class="inline-flex w-fit items-center gap-2 rounded-full bg-surface-container-high px-3 py-1.5 text-sm" 47 + classList={{ "text-emerald-300": kind === "success", "text-red-300": kind === "error" }}> 48 + <Icon kind={kind === "success" ? "complete" : "danger"} aria-hidden="true" /> 49 + <span>{message}</span> 50 + </div> 51 + ); 52 + }} 53 + </Show> 54 + ); 55 + }
+25 -8
src/components/settings/SettingsPanel.test.tsx
··· 30 30 vi.mock( 31 31 "$/lib/api/settings", 32 32 () => ({ 33 - getSettings: getSettingsMock, 34 - updateSetting: updateSettingMock, 35 - getCacheSize: getCacheSizeMock, 36 - clearCache: clearCacheMock, 37 - exportData: exportDataMock, 38 - resetApp: resetAppMock, 39 - resetAndRestartApp: resetAndRestartAppMock, 40 - getLogEntries: getLogEntriesMock, 33 + SettingsController: { 34 + getSettings: getSettingsMock, 35 + updateSetting: updateSettingMock, 36 + getCacheSize: getCacheSizeMock, 37 + clearCache: clearCacheMock, 38 + exportData: exportDataMock, 39 + resetApp: resetAppMock, 40 + resetAndRestartApp: resetAndRestartAppMock, 41 + getLogEntries: getLogEntriesMock, 42 + }, 41 43 }), 42 44 ); 43 45 ··· 207 209 208 210 fireEvent.click(clearFeedsButton); 209 211 await waitFor(() => expect(clearCacheMock).toHaveBeenCalledWith("feeds")); 212 + expect(await screen.findByText("Cleared feeds cache.")).toBeInTheDocument(); 210 213 }); 211 214 212 215 it("shows confirmation modal before clearing all cache", async () => { ··· 227 230 228 231 fireEvent.click(jsonButton); 229 232 await waitFor(() => expect(exportDataMock).toHaveBeenCalledWith("json")); 233 + expect(await screen.findByText("Exported data as JSON.")).toBeInTheDocument(); 230 234 }); 231 235 232 236 it("allows exporting data as CSV", async () => { ··· 237 241 238 242 fireEvent.click(csvButton); 239 243 await waitFor(() => expect(exportDataMock).toHaveBeenCalledWith("csv")); 244 + expect(await screen.findByText("Exported data as CSV.")).toBeInTheDocument(); 245 + }); 246 + 247 + it("shows export errors inline in data settings", async () => { 248 + exportDataMock.mockRejectedValueOnce(new Error("export path is invalid")); 249 + renderSettingsPanel(); 250 + 251 + await screen.findByText("Settings"); 252 + const jsonButton = await screen.findByRole("button", { name: /json/i }); 253 + fireEvent.click(jsonButton); 254 + 255 + expect(await screen.findByText("Couldn't export data — check that the destination path is valid.")) 256 + .toBeInTheDocument(); 240 257 }); 241 258 242 259 it("allows selecting the download folder from the directory picker", async () => {
+6 -5
src/components/settings/SettingsPanel.tsx
··· 1 1 import { EmbeddingsSettings } from "$/components/search/EmbeddingsSettings"; 2 2 import { useAppPreferences } from "$/contexts/app-preferences"; 3 - import { clearCache, getCacheSize, getLogEntries, resetAndRestartApp } from "$/lib/api/settings"; 3 + import { SettingsController } from "$/lib/api/settings"; 4 4 import type { 5 5 AppSettings, 6 6 CacheClearScope, ··· 157 157 158 158 async function handleResetAndRestartApp() { 159 159 try { 160 - await resetAndRestartApp(); 160 + await SettingsController.resetAndRestartApp(); 161 161 } catch (err) { 162 162 logger.error("failed to reset and restart app", { keyValues: { error: normalizeError(err) } }); 163 163 } ··· 180 180 181 181 async function loadCacheSize() { 182 182 try { 183 - setPanel("cacheSize", await getCacheSize()); 183 + setPanel("cacheSize", await SettingsController.getCacheSize()); 184 184 } catch (err) { 185 185 logger.error("failed to load cache size", { keyValues: { error: normalizeError(err) } }); 186 186 } ··· 188 188 189 189 async function loadLogs(level = panel.logLevel) { 190 190 try { 191 - setPanel("logs", await getLogEntries(100, level)); 191 + setPanel("logs", await SettingsController.getLogEntries(100, level)); 192 192 } catch (err) { 193 193 logger.error("failed to load logs", { keyValues: { error: normalizeError(err) } }); 194 194 } ··· 200 200 201 201 async function handleClearCache(scope: CacheClearScope) { 202 202 try { 203 - await clearCache(scope); 203 + await SettingsController.clearCache(scope); 204 204 await loadCacheSize(); 205 205 } catch (err) { 206 206 logger.error("failed to clear cache", { keyValues: { scope, error: normalizeError(err) } }); 207 + throw err; 207 208 } 208 209 } 209 210
+3 -3
src/contexts/app-preferences.tsx
··· 5 5 setEmbeddingsPreflightSeen as setEmbeddingsPreflightSeenRequest, 6 6 } from "$/lib/api/search"; 7 7 import type { EmbeddingsConfig } from "$/lib/api/search"; 8 - import { getSettings, updateSetting as updateSettingRequest } from "$/lib/api/settings"; 8 + import { SettingsController } from "$/lib/api/settings"; 9 9 import type { AppSettings } from "$/lib/types"; 10 10 import * as logger from "@tauri-apps/plugin-log"; 11 11 import { createContext, onMount, type ParentProps, splitProps, untrack, useContext } from "solid-js"; ··· 46 46 setPreferences("settingsLoading", true); 47 47 48 48 try { 49 - setPreferences("settings", await getSettings()); 49 + setPreferences("settings", await SettingsController.getSettings()); 50 50 } catch (error) { 51 51 logger.error("failed to load settings", { keyValues: { error: String(error) } }); 52 52 } finally { ··· 58 58 const serialized = typeof value === "boolean" ? (value ? "1" : "0") : String(value); 59 59 60 60 try { 61 - await updateSettingRequest(key, serialized); 61 + await SettingsController.updateSetting(key, serialized); 62 62 63 63 setPreferences("settings", (current) => { 64 64 if (!current) {
+13 -9
src/lib/api/settings.ts
··· 1 1 import type { AppSettings, CacheClearScope, CacheSize, ExportFormat, LogEntry, LogLevelFilter } from "$/lib/types"; 2 2 import { invoke } from "@tauri-apps/api/core"; 3 - import * as logger from "@tauri-apps/plugin-log"; 4 - import { normalizeError } from "../utils/text"; 5 3 6 4 export function getSettings() { 7 5 return invoke<AppSettings>("get_settings"); ··· 19 17 return invoke("clear_cache", { scope }); 20 18 } 21 19 22 - export async function exportData(format: ExportFormat, path?: string) { 23 - try { 24 - const now = Date.now(); 25 - await invoke("export_data", { format, path: path ?? `lazurite_${now}_export.${format}` }); 26 - } catch (err) { 27 - logger.error("failed to export data", { keyValues: { error: normalizeError(err) } }); 28 - } 20 + export function exportData(format: ExportFormat, path?: string) { 21 + const now = Date.now(); 22 + return invoke("export_data", { format, path: path ?? `lazurite_${now}_export.${format}` }); 29 23 } 30 24 31 25 function resetApp() { ··· 48 42 globalThis.location.replace(url.toString()); 49 43 globalThis.setTimeout(() => globalThis.location.reload(), 0); 50 44 } 45 + 46 + export const SettingsController = { 47 + getSettings, 48 + updateSetting, 49 + getCacheSize, 50 + clearCache, 51 + exportData, 52 + resetAndRestartApp, 53 + getLogEntries, 54 + };