From afac4b9870bd6743f624ea650d6b7cee9bf5cde6 Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Tue, 21 Apr 2026 23:25:34 +0800 Subject: [PATCH 1/2] fix: wire up Default Tool View save in General settings The with value/onChange - Add Save Settings button mirroring SystemSection's pattern - Redirect home page to /fullscreen when defaultToolView is "fullscreen" Closes #75 --- .../components/settings/settings-dialog.tsx | 80 ++++++++++++++++--- apps/web/src/pages/home-page.tsx | 8 +- apps/web/src/stores/settings-store.ts | 4 +- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/settings/settings-dialog.tsx b/apps/web/src/components/settings/settings-dialog.tsx index a1950fad..4d098616 100644 --- a/apps/web/src/components/settings/settings-dialog.tsx +++ b/apps/web/src/components/settings/settings-dialog.tsx @@ -27,6 +27,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useAuth } from "@/hooks/use-auth"; import { apiDelete, apiGet, apiPost, apiPut, clearToken, formatHeaders } from "@/lib/api"; import { cn, copyToClipboard } from "@/lib/utils"; +import { useSettingsStore } from "@/stores/settings-store"; import { GemLogo } from "../common/gem-logo"; import { AiFeaturesSection } from "./ai-features-section"; @@ -179,19 +180,29 @@ interface TeamEntry { function GeneralSection() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); + const [defaultToolView, setDefaultToolView] = useState("sidebar"); + const [saving, setSaving] = useState(false); + const [saveMsg, setSaveMsg] = useState(null); useEffect(() => { - apiGet<{ user: SessionUser }>("/auth/session") - .then((data) => setUser(data.user)) - .catch(() => { - // Fallback to localStorage if session endpoint fails - setUser({ - id: 0, - username: localStorage.getItem("ashim-username") || "", - role: "unknown", - }); - }) - .finally(() => setLoading(false)); + Promise.all([ + apiGet<{ user: SessionUser }>("/auth/session") + .then((data) => setUser(data.user)) + .catch(() => { + setUser({ + id: 0, + username: localStorage.getItem("ashim-username") || "", + role: "unknown", + }); + }), + apiGet<{ settings: Record }>("/v1/settings") + .then((data) => { + if (data.settings.defaultToolView) { + setDefaultToolView(data.settings.defaultToolView); + } + }) + .catch(() => {}), + ]).finally(() => setLoading(false)); }, []); const handleLogout = () => { @@ -200,6 +211,23 @@ function GeneralSection() { window.location.href = "/login"; }; + const handleSave = useCallback(async () => { + setSaving(true); + setSaveMsg(null); + try { + await apiPut("/v1/settings", { defaultToolView }); + setSaveMsg("Settings saved."); + useSettingsStore.setState({ + defaultToolView: defaultToolView as "sidebar" | "fullscreen", + }); + } catch { + setSaveMsg("Failed to save settings."); + } finally { + setSaving(false); + setTimeout(() => setSaveMsg(null), 3000); + } + }, [defaultToolView]); + const username = user?.username || "admin"; const role = user?.role || "unknown"; @@ -237,7 +265,11 @@ function GeneralSection() { {/* Default view */} - setDefaultToolView(e.target.value)} + className="px-3 py-1.5 rounded-lg border border-border bg-background text-sm text-foreground" + > @@ -247,6 +279,30 @@ function GeneralSection() { {APP_VERSION} + +
+ + {saveMsg && ( + + {saveMsg} + + )} +
); } diff --git a/apps/web/src/pages/home-page.tsx b/apps/web/src/pages/home-page.tsx index 4e479bb1..313b5cec 100644 --- a/apps/web/src/pages/home-page.tsx +++ b/apps/web/src/pages/home-page.tsx @@ -24,7 +24,7 @@ export function HomePage() { currentEntry, } = useFileStore(); const navigate = useNavigate(); - const { fetch: fetchSettings } = useSettingsStore(); + const { fetch: fetchSettings, defaultToolView, loaded: settingsLoaded } = useSettingsStore(); const { fetch: fetchFeatures, isToolInstalled } = useFeaturesStore(); useEffect(() => { @@ -32,6 +32,12 @@ export function HomePage() { fetchFeatures(); }, [fetchSettings, fetchFeatures]); + useEffect(() => { + if (settingsLoaded && defaultToolView === "fullscreen" && files.length === 0) { + navigate("/fullscreen", { replace: true }); + } + }, [settingsLoaded, defaultToolView, files.length, navigate]); + const handleFiles = useCallback( (newFiles: File[]) => { reset(); diff --git a/apps/web/src/stores/settings-store.ts b/apps/web/src/stores/settings-store.ts index 34a34d86..0fbbc5af 100644 --- a/apps/web/src/stores/settings-store.ts +++ b/apps/web/src/stores/settings-store.ts @@ -4,6 +4,7 @@ import { apiGet } from "@/lib/api"; interface SettingsState { disabledTools: string[]; experimentalEnabled: boolean; + defaultToolView: "sidebar" | "fullscreen"; loaded: boolean; fetch: () => Promise; } @@ -11,6 +12,7 @@ interface SettingsState { export const useSettingsStore = create((set, get) => ({ disabledTools: [], experimentalEnabled: false, + defaultToolView: "sidebar", loaded: false, fetch: async () => { @@ -23,10 +25,10 @@ export const useSettingsStore = create((set, get) => ({ set({ disabledTools: data.settings.disabledTools ? JSON.parse(data.settings.disabledTools) : [], experimentalEnabled: data.settings.enableExperimentalTools === "true", + defaultToolView: data.settings.defaultToolView === "fullscreen" ? "fullscreen" : "sidebar", loaded: true, }); } catch { - // Settings fetch failed - default to no disabled tools set({ loaded: true }); } }, From 7920fbfd2093fc1260a33056625d04fc05d0d516 Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Tue, 21 Apr 2026 23:25:41 +0800 Subject: [PATCH 2/2] chore: fix pre-existing biome formatting issues --- apps/api/src/lib/format-decoders.ts | 8 +++---- apps/web/src/lib/image-preview.ts | 23 ++++++++++++------- apps/web/src/pages/tool-page.tsx | 3 ++- .../image-engine/src/operations/compress.ts | 6 ++--- packages/image-engine/src/types.ts | 11 ++++++++- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/apps/api/src/lib/format-decoders.ts b/apps/api/src/lib/format-decoders.ts index dbd48b8b..da0d5230 100644 --- a/apps/api/src/lib/format-decoders.ts +++ b/apps/api/src/lib/format-decoders.ts @@ -70,11 +70,9 @@ async function decodeIco(buffer: Buffer): Promise { try { await writeFile(inputPath, buffer); // ICO contains multiple sizes; extract the largest by sorting - await execFileAsync( - cmd, - magickArgs(cmd, [`${inputPath}[-1]`, `png:${outputPath}`]), - { timeout: 120_000 }, - ); + await execFileAsync(cmd, magickArgs(cmd, [`${inputPath}[-1]`, `png:${outputPath}`]), { + timeout: 120_000, + }); return await readFile(outputPath); } finally { await rm(inputPath, { force: true }).catch(() => {}); diff --git a/apps/web/src/lib/image-preview.ts b/apps/web/src/lib/image-preview.ts index 3e9474db..d6d29275 100644 --- a/apps/web/src/lib/image-preview.ts +++ b/apps/web/src/lib/image-preview.ts @@ -1,14 +1,21 @@ import { formatHeaders } from "@/lib/api"; const SERVER_PREVIEW_EXTENSIONS = new Set([ - "heic", "heif", "hif", // HEIF - "jxl", // JPEG XL (Chrome dropped support) - "ico", // ICO (Sharp can't decode) - "dng", "cr2", "nef", "arw", "orf", "rw2", // Camera RAW - "tga", // Targa - "psd", // Photoshop - "exr", // OpenEXR - "hdr", // Radiance HDR + "heic", + "heif", + "hif", // HEIF + "jxl", // JPEG XL (Chrome dropped support) + "ico", // ICO (Sharp can't decode) + "dng", + "cr2", + "nef", + "arw", + "orf", + "rw2", // Camera RAW + "tga", // Targa + "psd", // Photoshop + "exr", // OpenEXR + "hdr", // Radiance HDR ]); export function needsServerPreview(file: File): boolean { diff --git a/apps/web/src/pages/tool-page.tsx b/apps/web/src/pages/tool-page.tsx index d02aae7a..37b011ae 100644 --- a/apps/web/src/pages/tool-page.tsx +++ b/apps/web/src/pages/tool-page.tsx @@ -250,7 +250,8 @@ export function ToolPage() { const input = document.createElement("input"); input.type = "file"; input.multiple = true; - input.accept = "image/*,.heic,.heif,.hif,.jxl,.dng,.cr2,.nef,.arw,.orf,.rw2,.tga,.psd,.exr,.hdr"; + input.accept = + "image/*,.heic,.heif,.hif,.jxl,.dng,.cr2,.nef,.arw,.orf,.rw2,.tga,.psd,.exr,.hdr"; input.onchange = (e) => { const newFiles = Array.from((e.target as HTMLInputElement).files || []); if (newFiles.length > 0) addFiles(newFiles); diff --git a/packages/image-engine/src/operations/compress.ts b/packages/image-engine/src/operations/compress.ts index a24fdcef..970c35ec 100644 --- a/packages/image-engine/src/operations/compress.ts +++ b/packages/image-engine/src/operations/compress.ts @@ -22,9 +22,9 @@ export async function compress(image: Sharp, options: CompressOptions): Promise< const metadata = await image.metadata(); const detected = metadata.format ?? "jpeg"; - const outputFormat = ( - FORMAT_MAP[format ?? ""] ?? FORMAT_MAP[detected] ?? detected - ) as keyof import("sharp").FormatEnum; + const outputFormat = (FORMAT_MAP[format ?? ""] ?? + FORMAT_MAP[detected] ?? + detected) as keyof import("sharp").FormatEnum; if (targetSizeBytes !== undefined) { if (targetSizeBytes <= 0) { diff --git a/packages/image-engine/src/types.ts b/packages/image-engine/src/types.ts index c5543c7f..0515e359 100644 --- a/packages/image-engine/src/types.ts +++ b/packages/image-engine/src/types.ts @@ -17,7 +17,16 @@ export interface OperationResult { info: ImageInfo; } -export type OutputFormat = "jpg" | "png" | "webp" | "avif" | "tiff" | "gif" | "heic" | "heif" | "jxl"; +export type OutputFormat = + | "jpg" + | "png" + | "webp" + | "avif" + | "tiff" + | "gif" + | "heic" + | "heif" + | "jxl"; export interface ResizeOptions { width?: number;