diff --git a/app.go b/app.go index a8e7efc..943afb6 100644 --- a/app.go +++ b/app.go @@ -489,3 +489,27 @@ func formatShutterSpeed(num, den int64) string { return fmt.Sprintf("%d/%ds", reducedNum, reducedDen) } } + +// OpenSettingsWindow opens the settings window or focuses it if already open. +func (a *App) OpenSettingsWindow() { + app := application.Get() + + // If the window already exists, focus it + if win, ok := app.Window.GetByName("settings"); ok && win != nil { + win.Show() + win.Focus() + return + } + + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Name: "settings", + Title: "Preferences", + Width: 600, + Height: 500, + Mac: application.MacWindow{ + TitleBar: application.MacTitleBarHiddenInsetUnified, + }, + BackgroundColour: application.NewRGB(27, 38, 54), + URL: "/settings", + }) +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 81c5b66..d9474cc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useCallback, ChangeEvent } from 'react'; import './App.css'; // @ts-expect-error generated bindings does not provide declaration files for JS module import { App as AppAPI, Settings } from '../bindings/ExifFrame/index'; -import { Window, Events, System } from '@wailsio/runtime'; +import { Window, Events, System, Call } from '@wailsio/runtime'; interface ExifData { camera: string; @@ -33,8 +33,8 @@ interface MetadataVisibility { } const VISIBILITY_KEYS = [ - 'camera','lens','focalLength','aperture','shutterSpeed','iso', - 'film','developer','dilution','temperature','time', + 'camera', 'lens', 'focalLength', 'aperture', 'shutterSpeed', 'iso', + 'film', 'developer', 'dilution', 'temperature', 'time', ] as const; const settingsKey = (k: typeof VISIBILITY_KEYS[number]) => @@ -217,9 +217,9 @@ const ToggleInput = ({ label, id, value, onChange, visible, onToggleVisibility }
-
-
); @@ -260,12 +260,11 @@ interface WailsProcessFileEvent { } function App() { - const [showSettings, setShowSettings] = useState(false); const [watchFolder, setWatchFolder] = useState(""); const [exportFolder, setExportFolder] = useState(""); const [profile, setProfile] = useState("digital"); - + useEffect(() => { const unsubProcess = Events.On("process_file", (event: WailsProcessFileEvent) => { if (!event?.data) return; @@ -301,7 +300,7 @@ function App() { temperature: currentSet.temperature || "", time: currentSet.time || "" }; - + renderImageToCanvas(offscreenCanvas, img, exifData, { aspectRatioPreset: currentSet.aspectRatioPreset || "4300:3618", customRatioW: currentSet.customRatioW || 4300, @@ -325,7 +324,7 @@ function App() { offscreenCanvas.toBlob(async (blob) => { if (!blob) return; try { - const resultSave = await AppAPI.SaveAutoImage(isPng, savePath); + const resultSave = await AppAPI.SaveAutoImage(isPng, savePath); if (resultSave.saveToken) { const arrayBuffer = await blob.arrayBuffer(); const resp = await fetch(`/api/save?token=${encodeURIComponent(resultSave.saveToken)}`, { @@ -358,7 +357,7 @@ function App() { unsubProcess(); }; }, []); - + const canvasRef = useRef(null); const [imageLoaded, setImageLoaded] = useState(false); const [exif, setExif] = useState({ @@ -428,7 +427,7 @@ function App() { window.cancelAnimationFrame(toastRafRef.current); toastRafRef.current = null; } - + // Force a re-render by clearing the state first setToastMessage(null); toastRafRef.current = requestAnimationFrame(() => { @@ -455,7 +454,7 @@ function App() { if (s.profile) { setProfile(['digital', 'film'].includes(s.profile) ? s.profile : 'digital'); } - + setExif(prev => ({ ...prev, film: s.film || "", @@ -475,31 +474,26 @@ function App() { }, 100); }); - const unsubSettings = Events.On("open_settings", () => { - console.log("open_settings event received"); - setShowSettings(true); + const unsubSettings = Events.On("settings_saved", () => { + AppAPI.GetSettings().then((s: Settings) => { + if (s.watchFolder) setWatchFolder(s.watchFolder); + else setWatchFolder(""); + if (s.exportFolder) setExportFolder(s.exportFolder); + else setExportFolder(""); + }).catch((err: any) => { + console.error("Failed to reload settings:", err); + }); }); - + return () => { unsubSettings(); }; }, []); - // Escape key to close settings modal - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - setShowSettings(false); - } - }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, []); - // Save settings when aspect ratio etc changes useEffect(() => { if (isInitialLoad.current) return; - + const saveCurrentSettings = async () => { const s = new Settings(); s.watchFolder = watchFolder; @@ -516,7 +510,7 @@ function App() { s.dilution = exif.dilution; s.temperature = exif.temperature; s.time = exif.time; - + applyVisibility(s, visibility); try { @@ -689,9 +683,9 @@ function App() { {filePath && {filePath.split(/[/\\]/).filter(Boolean).join(' > ')}}
- - - -
- +

Metadata Settings

- -
- - setExif(prev => ({ ...prev, camera: e.target.value }))} + + setExif(prev => ({ ...prev, camera: e.target.value }))} visible={visibility.camera} onToggleVisibility={() => setVisibility(prev => ({ ...prev, camera: !prev.camera }))} /> - setExif(prev => ({ ...prev, lens: e.target.value }))} + setExif(prev => ({ ...prev, lens: e.target.value }))} visible={visibility.lens} onToggleVisibility={() => setVisibility(prev => ({ ...prev, lens: !prev.lens }))} /> - +
- setExif(prev => ({ ...prev, focalLength: e.target.value }))} + setExif(prev => ({ ...prev, focalLength: e.target.value }))} visible={visibility.focalLength} onToggleVisibility={() => setVisibility(prev => ({ ...prev, focalLength: !prev.focalLength }))} /> - setExif(prev => ({ ...prev, aperture: e.target.value }))} + setExif(prev => ({ ...prev, aperture: e.target.value }))} visible={visibility.aperture} onToggleVisibility={() => setVisibility(prev => ({ ...prev, aperture: !prev.aperture }))} />
- +
- setExif(prev => ({ ...prev, shutterSpeed: e.target.value }))} + setExif(prev => ({ ...prev, shutterSpeed: e.target.value }))} visible={visibility.shutterSpeed} onToggleVisibility={() => setVisibility(prev => ({ ...prev, shutterSpeed: !prev.shutterSpeed }))} /> {profile === 'digital' ? ( - setExif(prev => ({ ...prev, iso: e.target.value }))} + setExif(prev => ({ ...prev, iso: e.target.value }))} visible={visibility.iso} onToggleVisibility={() => setVisibility(prev => ({ ...prev, iso: !prev.iso }))} /> ) : ( - setExif(prev => ({ ...prev, film: e.target.value }))} + setExif(prev => ({ ...prev, film: e.target.value }))} visible={visibility.film} onToggleVisibility={() => setVisibility(prev => ({ ...prev, film: !prev.film }))} /> )}
- + {profile === 'film' && ( <>
- setExif(prev => ({ ...prev, developer: e.target.value }))} + setExif(prev => ({ ...prev, developer: e.target.value }))} visible={visibility.developer} onToggleVisibility={() => setVisibility(prev => ({ ...prev, developer: !prev.developer }))} /> - setExif(prev => ({ ...prev, dilution: e.target.value }))} + setExif(prev => ({ ...prev, dilution: e.target.value }))} visible={visibility.dilution} onToggleVisibility={() => setVisibility(prev => ({ ...prev, dilution: !prev.dilution }))} />
- setExif(prev => ({ ...prev, temperature: e.target.value }))} + setExif(prev => ({ ...prev, temperature: e.target.value }))} visible={visibility.temperature} onToggleVisibility={() => setVisibility(prev => ({ ...prev, temperature: !prev.temperature }))} /> - setExif(prev => ({ ...prev, time: e.target.value }))} + setExif(prev => ({ ...prev, time: e.target.value }))} visible={visibility.time} onToggleVisibility={() => setVisibility(prev => ({ ...prev, time: !prev.time }))} /> @@ -1013,52 +1007,6 @@ function App() { )} - {showSettings && ( -
setShowSettings(false)}> -
e.stopPropagation()}> -

Preferences

-
- -
- - - -
- Images dropped here will be processed automatically. -
-
- -
- - - -
- Auto-processed images will be saved here. -
-
- -
-
-
- )}
); } diff --git a/frontend/src/SettingsWindow.tsx b/frontend/src/SettingsWindow.tsx new file mode 100644 index 0000000..aa14753 --- /dev/null +++ b/frontend/src/SettingsWindow.tsx @@ -0,0 +1,115 @@ +import { useState, useEffect } from 'react'; +import './App.css'; +// @ts-expect-error generated bindings does not provide declaration files for JS module +import { App as AppAPI, Settings } from '../bindings/ExifFrame/index'; +import { Events, System } from '@wailsio/runtime'; + +function SettingsWindow() { + const [watchFolder, setWatchFolder] = useState(""); + const [exportFolder, setExportFolder] = useState(""); + const [fullSettings, setFullSettings] = useState(null); + const [isMac, setIsMac] = useState(false); + + useEffect(() => { + setIsMac(System.IsMac()); + + // Load initial settings + AppAPI.GetSettings().then((s: Settings) => { + setFullSettings(s); + setWatchFolder(s.watchFolder || ""); + setExportFolder(s.exportFolder || ""); + }).catch((err: any) => console.error("Failed to load settings:", err)); + + // Listen for settings_saved from main window + const unsub = Events.On("settings_saved", () => { + AppAPI.GetSettings().then((s: Settings) => { + setFullSettings(s); + setWatchFolder(s.watchFolder || ""); + setExportFolder(s.exportFolder || ""); + }).catch((err: any) => console.error("Failed to reload settings:", err)); + }); + + return () => { + unsub(); + }; + }, []); + + const handleSave = async (newWatch: string, newExport: string) => { + if (!fullSettings) return; + + const s = new Settings(); + // Copy existing settings + Object.assign(s, fullSettings); + + // Update folders + s.watchFolder = newWatch; + s.exportFolder = newExport; + + try { + const errStr = await AppAPI.SaveSettings(s); + if (errStr && errStr !== "") { + console.error(errStr); + alert(errStr); + return; + } + + // Update local state + setWatchFolder(newWatch); + setExportFolder(newExport); + setFullSettings(s); + + // Notify other windows + Events.Emit("settings_saved"); + } catch (e: any) { + console.error("Error saving settings", e); + } + }; + + return ( +
+
+

Preferences

+
+
+
+ +
+ + + +
+ Images dropped here will be processed automatically. +
+
+ +
+ + + +
+ Auto-processed images will be saved here. +
+
+
+ ); +} + +export default SettingsWindow; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 3626ff3..3edc61a 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,13 +2,22 @@ import React from 'react' import {createRoot} from 'react-dom/client' import './style.css' import App from './App' +import SettingsWindow from './SettingsWindow' const container = document.getElementById('root') const root = createRoot(container!) -root.render( - - - -) +if (window.location.pathname === '/settings') { + root.render( + + + + ) +} else { + root.render( + + + + ) +} diff --git a/main.go b/main.go index 90efcb3..dc3e0ad 100644 --- a/main.go +++ b/main.go @@ -19,8 +19,7 @@ func buildMenu(app *App) *application.Menu { appleMenu.AddRole(application.About) appleMenu.AddSeparator() appleMenu.Add("Preferences...").SetAccelerator("CmdOrCtrl+,").OnClick(func(ctx *application.Context) { - application.Get().Show() - application.Get().Event.Emit("open_settings") + app.OpenSettingsWindow() }) appleMenu.AddSeparator() appleMenu.Add("Hide ExifFrame").SetAccelerator("CmdOrCtrl+h").OnClick(func(ctx *application.Context) { @@ -38,8 +37,7 @@ func buildMenu(app *App) *application.Menu { } else { fileMenu := appMenu.AddSubmenu("File") fileMenu.Add("Preferences...").SetAccelerator("CmdOrCtrl+,").OnClick(func(ctx *application.Context) { - application.Get().Show() - application.Get().Event.Emit("open_settings") + app.OpenSettingsWindow() }) }