From 0bed45084338b4a282f45f99f653d2ce8c05066b Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Tue, 2 Jun 2026 04:55:51 +0530 Subject: [PATCH 01/10] feat: add auto-start and minimize to tray feature --- frontend/src-tauri/Cargo.lock | 56 +++++++++++-- frontend/src-tauri/Cargo.toml | 3 +- frontend/src-tauri/capabilities/migrated.json | 2 +- frontend/src-tauri/src/main.rs | 84 +++++++++++++++++-- frontend/src/pages/SettingsPage/Settings.tsx | 2 + .../components/SystemSettingsCard.tsx | 70 ++++++++++++++++ 6 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 038f82916..2d6e74544 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -243,6 +243,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -781,6 +792,15 @@ dependencies = [ "dirs-sys 0.3.7", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "6.0.0" @@ -933,7 +953,7 @@ dependencies = [ "rustc_version", "toml 0.9.10+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -2992,6 +3012,7 @@ dependencies = [ "sysinfo", "tauri", "tauri-build", + "tauri-plugin-autostart", "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-opener", @@ -4344,7 +4365,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -4395,7 +4416,7 @@ checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4467,6 +4488,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-autostart" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70" +dependencies = [ + "auto-launch", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", +] + [[package]] name = "tauri-plugin-dialog" version = "2.4.2" @@ -4583,7 +4618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" dependencies = [ "base64 0.22.1", - "dirs", + "dirs 6.0.0", "flate2", "futures-util", "http", @@ -5080,7 +5115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2", @@ -6025,6 +6060,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.55.0" @@ -6057,7 +6101,7 @@ dependencies = [ "block2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dpi", "dunce", "gdkx11", diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index c16753a36..9bc645443 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -11,7 +11,7 @@ edition = "2021" tauri-build = { version = "2.0.6", features = [] } [dependencies] -tauri = { version = "2.9.1", features = ["protocol-asset", "devtools"] } +tauri = { version = "2.9.1", features = ["protocol-asset", "devtools", "tray-icon"] } reqwest = { version = "0.12", features = ["blocking"] } walkdir = "2.3" sysinfo = "0.37.2" @@ -37,6 +37,7 @@ tauri-plugin-process = "2.3.1" tauri-plugin-store = "2.4.1" tauri-plugin-updater = "2.9.0" tauri-plugin-opener = "2.5.2" +tauri-plugin-autostart = "2" [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/frontend/src-tauri/capabilities/migrated.json b/frontend/src-tauri/capabilities/migrated.json index 297b9f7be..b47fbeeb4 100644 --- a/frontend/src-tauri/capabilities/migrated.json +++ b/frontend/src-tauri/capabilities/migrated.json @@ -11,6 +11,7 @@ "core:resources:default", "core:menu:default", "core:tray:default", + "autostart:default", "shell:allow-open", "core:path:default", "core:event:default", @@ -19,7 +20,6 @@ "core:app:default", "core:resources:default", "core:menu:default", - "core:tray:default", "core:window:allow-set-title", "dialog:allow-open", "fs:default", diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index 5b7aa6acc..31847f110 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -4,8 +4,11 @@ mod services; use sysinfo::System; +use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::path::BaseDirectory; +use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; use tauri::{Manager, Window, WindowEvent}; +use tauri_plugin_autostart::ManagerExt; use tauri_plugin_shell::ShellExt; const ENDPOINTS: [(&str, &str, &str); 2] = [ @@ -43,13 +46,13 @@ fn is_process_alive() -> bool { true } +// Hide the window instead of exiting so the app lives in the system tray. +// The user can quit via the tray context menu's "Quit" item. fn on_window_event(window: &Window, event: &WindowEvent) { - if !matches!(event, WindowEvent::CloseRequested { .. }) { - return; + if let WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + let _ = window.hide(); } - - let _ = kill_process_tree(); - window.app_handle().exit(0); } #[cfg(unix)] @@ -188,8 +191,28 @@ fn prod(_app: &tauri::AppHandle, _resource_path: &std::path::Path) -> Result<(), Ok(()) } +#[tauri::command] +fn enable_autostart(app: tauri::AppHandle) -> Result<(), String> { + app.autolaunch().enable().map_err(|e| e.to_string()) +} + +#[tauri::command] +fn disable_autostart(app: tauri::AppHandle) -> Result<(), String> { + app.autolaunch().disable().map_err(|e| e.to_string()) +} + +#[tauri::command] +fn is_autostart_enabled(app: tauri::AppHandle) -> Result { + app.autolaunch().is_enabled().map_err(|e| e.to_string()) +} + fn main() { tauri::Builder::default() + // Auto-start: pass --minimized so the window starts hidden when launched at boot + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + Some(vec!["--minimized"]), + )) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_store::Builder::new().build()) @@ -202,10 +225,61 @@ fn main() { println!("Resource path: {:?}", resource_path); prod(app.handle(), &resource_path)?; + + // When auto-started at boot (--minimized flag), keep the window hidden + if std::env::args().any(|a| a == "--minimized") { + if let Some(window) = app.get_webview_window("main") { + let _ = window.hide(); + } + } + + // System tray: context menu with Show / Quit + let show_item = MenuItem::with_id(app, "show", "Show", true, None::<&str>)?; + let separator = PredefinedMenuItem::separator(app)?; + let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show_item, &separator, &quit_item])?; + + TrayIconBuilder::new() + .icon(app.default_window_icon().unwrap().clone()) + .tooltip("PictoPy") + .menu(&menu) + .on_menu_event(|app, event| match event.id.as_ref() { + "show" => { + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + } + "quit" => { + let _ = kill_process_tree(); + app.exit(0); + } + _ => {} + }) + // Left-click on the tray icon also shows the window + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + let app = tray.app_handle(); + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + } + }) + .build(app)?; + Ok(()) }) .invoke_handler(tauri::generate_handler![ services::get_resources_folder_path, + enable_autostart, + disable_autostart, + is_autostart_enabled, ]) .on_window_event(on_window_event) .run(tauri::generate_context!()) diff --git a/frontend/src/pages/SettingsPage/Settings.tsx b/frontend/src/pages/SettingsPage/Settings.tsx index 472f788fb..df5d4a0f1 100644 --- a/frontend/src/pages/SettingsPage/Settings.tsx +++ b/frontend/src/pages/SettingsPage/Settings.tsx @@ -6,6 +6,7 @@ import FolderManagementCard from './components/FolderManagementCard'; import UserPreferencesCard from './components/UserPreferencesCard'; import ApplicationControlsCard from './components/ApplicationControlsCard'; import AccountSettingsCard from './components/AccountSettingsCard'; +import SystemSettingsCard from './components/SystemSettingsCard'; /** * Settings page component @@ -70,6 +71,7 @@ const Settings: React.FC = () => { + )} diff --git a/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx new file mode 100644 index 000000000..6dc160ea3 --- /dev/null +++ b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx @@ -0,0 +1,70 @@ +import React, { useEffect, useState } from 'react'; +import { Monitor } from 'lucide-react'; +import { invoke } from '@tauri-apps/api/core'; +import SettingsCard from './SettingsCard'; + +const SystemSettingsCard: React.FC = () => { + const [autostart, setAutostart] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + invoke('is_autostart_enabled') + .then(setAutostart) + .catch(() => setAutostart(false)) + .finally(() => setLoading(false)); + }, []); + + const handleToggle = async () => { + const next = !autostart; + try { + await invoke(next ? 'enable_autostart' : 'disable_autostart'); + setAutostart(next); + } catch (err) { + console.error('Failed to toggle autostart:', err); + } + }; + + return ( + +
+
+
Launch at startup
+
+ Automatically start PictoPy when you log in. The window starts + minimized to the system tray. +
+
+ + +
+
+ ); +}; + +export default SystemSettingsCard; From 42fb8e035c37d619c3b6b8b753fb6e907ac714c8 Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Tue, 2 Jun 2026 05:20:14 +0530 Subject: [PATCH 02/10] fix: address CodeRabbit review comments --- frontend/src-tauri/src/main.rs | 9 ++++-- .../components/SystemSettingsCard.tsx | 29 ++++++++++++++----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index 31847f110..9a6b77ca6 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -9,7 +9,6 @@ use tauri::path::BaseDirectory; use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; use tauri::{Manager, Window, WindowEvent}; use tauri_plugin_autostart::ManagerExt; -use tauri_plugin_shell::ShellExt; const ENDPOINTS: [(&str, &str, &str); 2] = [ ( @@ -137,6 +136,7 @@ fn prod(app: &tauri::AppHandle, resource_path: &std::path::Path) -> Result<(), S println!("Sync spawned with PID {}", sync_child.pid()); use tauri_plugin_shell::process::CommandEvent; + use tauri_plugin_shell::ShellExt; tauri::async_runtime::spawn(async move { while let Some(event) = backend_rx.recv().await { match event { @@ -240,9 +240,14 @@ fn main() { let menu = Menu::with_items(app, &[&show_item, &separator, &quit_item])?; TrayIconBuilder::new() - .icon(app.default_window_icon().unwrap().clone()) + .icon( + app.default_window_icon() + .ok_or("no default window icon")? + .clone(), + ) .tooltip("PictoPy") .menu(&menu) + .menu_on_left_click(false) .on_menu_event(|app, event| match event.id.as_ref() { "show" => { if let Some(w) = app.get_webview_window("main") { diff --git a/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx index 6dc160ea3..f9c9bbe82 100644 --- a/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx +++ b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx @@ -4,26 +4,35 @@ import { invoke } from '@tauri-apps/api/core'; import SettingsCard from './SettingsCard'; const SystemSettingsCard: React.FC = () => { - const [autostart, setAutostart] = useState(false); + // null = unknown / error reading state + const [autostart, setAutostart] = useState(null); const [loading, setLoading] = useState(true); + const [pending, setPending] = useState(false); useEffect(() => { invoke('is_autostart_enabled') .then(setAutostart) - .catch(() => setAutostart(false)) + .catch(() => setAutostart(null)) .finally(() => setLoading(false)); }, []); const handleToggle = async () => { + if (autostart === null) return; const next = !autostart; + setPending(true); try { await invoke(next ? 'enable_autostart' : 'disable_autostart'); setAutostart(next); } catch (err) { console.error('Failed to toggle autostart:', err); + } finally { + setPending(false); } }; + const isDisabled = loading || pending || autostart === null; + const isChecked = autostart === true; + return ( { >
-
Launch at startup
-
+
+ Launch at startup +
+
Automatically start PictoPy when you log in. The window starts minimized to the system tray.
@@ -41,15 +52,17 @@ const SystemSettingsCard: React.FC = () => { From 75674aa08d4cfa35a025fa69690c4d30c271843c Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Thu, 4 Jun 2026 00:22:08 +0530 Subject: [PATCH 03/10] fix: add autostart invoke mocks and fix test queries --- frontend/jest.setup.ts | 12 +++++++++++- frontend/src/pages/__tests__/PageSanity.test.tsx | 1 + frontend/src/pages/__tests__/SettingsPage.test.tsx | 5 +++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index f76fb5b23..a63a9baf4 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -52,7 +52,17 @@ Object.defineProperty(window, 'matchMedia', { // Mock the module imports jest.mock('@tauri-apps/api/core', () => ({ - invoke: jest.fn().mockResolvedValue(null), + invoke: jest.fn().mockImplementation((cmd: string) => { + switch (cmd) { + case 'is_autostart_enabled': + return Promise.resolve(false); + case 'enable_autostart': + case 'disable_autostart': + return Promise.resolve(undefined); + default: + return Promise.resolve(null); + } + }), })); jest.mock('@tauri-apps/api/app', () => ({ diff --git a/frontend/src/pages/__tests__/PageSanity.test.tsx b/frontend/src/pages/__tests__/PageSanity.test.tsx index 9c68744df..65ddf51f8 100644 --- a/frontend/src/pages/__tests__/PageSanity.test.tsx +++ b/frontend/src/pages/__tests__/PageSanity.test.tsx @@ -20,6 +20,7 @@ describe('Page Sanity Tests', () => { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); + await screen.findByRole('switch', { name: /launch at startup/i }); expect(screen.getByText('Folder Management')).toBeInTheDocument(); expect(screen.getByText('User Preferences')).toBeInTheDocument(); diff --git a/frontend/src/pages/__tests__/SettingsPage.test.tsx b/frontend/src/pages/__tests__/SettingsPage.test.tsx index f173afd19..a636924c4 100644 --- a/frontend/src/pages/__tests__/SettingsPage.test.tsx +++ b/frontend/src/pages/__tests__/SettingsPage.test.tsx @@ -10,6 +10,7 @@ describe('Settings Page', () => { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); + await screen.findByRole('switch', { name: /launch at startup/i }); return { user }; }; @@ -33,7 +34,7 @@ describe('Settings Page', () => { test('GPU Acceleration toggle changes state on click', async () => { const { user } = await setupTest(); - const gpuSwitch = screen.getByRole('switch'); + const gpuSwitch = screen.getByRole('switch', { name: /gpu acceleration/i }); expect(gpuSwitch).toHaveAttribute('aria-checked', 'false'); await user.click(gpuSwitch); @@ -107,7 +108,7 @@ describe('Settings Page', () => { test('toggle cycles through ON/OFF states', async () => { const { user } = await setupTest(); - const gpuSwitch = screen.getByRole('switch'); + const gpuSwitch = screen.getByRole('switch', { name: /gpu acceleration/i }); expect(gpuSwitch).toHaveAttribute('aria-checked', 'false'); await user.click(gpuSwitch); From da48feb194f32c770ca25b337e9dc38666012f64 Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Thu, 4 Jun 2026 00:37:09 +0530 Subject: [PATCH 04/10] fix: prettier formatting and add autostart toggle test --- .../src/pages/__tests__/SettingsPage.test.tsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/__tests__/SettingsPage.test.tsx b/frontend/src/pages/__tests__/SettingsPage.test.tsx index a636924c4..b162374ab 100644 --- a/frontend/src/pages/__tests__/SettingsPage.test.tsx +++ b/frontend/src/pages/__tests__/SettingsPage.test.tsx @@ -1,4 +1,5 @@ import { render, screen, act } from '@/test-utils'; +import { invoke } from '@tauri-apps/api/core'; import userEvent from '@testing-library/user-event'; import Settings from '../SettingsPage/Settings'; @@ -34,13 +35,29 @@ describe('Settings Page', () => { test('GPU Acceleration toggle changes state on click', async () => { const { user } = await setupTest(); - const gpuSwitch = screen.getByRole('switch', { name: /gpu acceleration/i }); + const gpuSwitch = screen.getByRole('switch', { + name: /gpu acceleration/i, + }); expect(gpuSwitch).toHaveAttribute('aria-checked', 'false'); await user.click(gpuSwitch); expect(gpuSwitch).toHaveAttribute('aria-checked', 'true'); }); + + test('launch at startup switch enables autostart', async () => { + const { user } = await setupTest(); + const mockedInvoke = invoke as jest.MockedFunction; + mockedInvoke.mockClear(); + + const autostartSwitch = screen.getByRole('switch', { + name: /launch at startup/i, + }); + + await user.click(autostartSwitch); + + expect(mockedInvoke).toHaveBeenCalledWith('enable_autostart'); + }); }); describe('Action Buttons', () => { @@ -108,7 +125,9 @@ describe('Settings Page', () => { test('toggle cycles through ON/OFF states', async () => { const { user } = await setupTest(); - const gpuSwitch = screen.getByRole('switch', { name: /gpu acceleration/i }); + const gpuSwitch = screen.getByRole('switch', { + name: /gpu acceleration/i, + }); expect(gpuSwitch).toHaveAttribute('aria-checked', 'false'); await user.click(gpuSwitch); From 51cb119515839b47ecee5ded0221b99cc4367e74 Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Thu, 4 Jun 2026 00:44:51 +0530 Subject: [PATCH 05/10] chore: format backend image modules with black --- backend/app/database/images.py | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/backend/app/database/images.py b/backend/app/database/images.py index beb60eca1..3bbc7a0bd 100644 --- a/backend/app/database/images.py +++ b/backend/app/database/images.py @@ -62,8 +62,7 @@ def db_create_images_table() -> None: cursor = conn.cursor() # Create new images table with merged fields including Memories feature columns - cursor.execute( - """ + cursor.execute(""" CREATE TABLE IF NOT EXISTS images ( id TEXT PRIMARY KEY, path VARCHAR UNIQUE, @@ -77,8 +76,7 @@ def db_create_images_table() -> None: captured_at DATETIME, FOREIGN KEY (folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE ) - """ - ) + """) # Create indexes for Memories feature queries cursor.execute("CREATE INDEX IF NOT EXISTS ix_images_latitude ON images(latitude)") @@ -93,8 +91,7 @@ def db_create_images_table() -> None: ) # Create new image_classes junction table - cursor.execute( - """ + cursor.execute(""" CREATE TABLE IF NOT EXISTS image_classes ( image_id TEXT, class_id INTEGER, @@ -102,8 +99,7 @@ def db_create_images_table() -> None: FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, FOREIGN KEY (class_id) REFERENCES mappings(class_id) ON DELETE CASCADE ) - """ - ) + """) conn.commit() conn.close() @@ -265,15 +261,13 @@ def db_get_untagged_images() -> List[UntaggedImageRecord]: cursor = conn.cursor() try: - cursor.execute( - """ + cursor.execute(""" SELECT i.id, i.path, i.folder_id, i.thumbnailPath, i.metadata FROM images i JOIN folders f ON i.folder_id = f.folder_id WHERE f.AI_Tagging = TRUE AND i.isTagged = FALSE - """ - ) + """) results = cursor.fetchall() @@ -754,8 +748,7 @@ def db_get_images_with_location() -> List[dict]: cursor = conn.cursor() try: - cursor.execute( - """ + cursor.execute(""" SELECT i.id, i.path, @@ -775,8 +768,7 @@ def db_get_images_with_location() -> List[dict]: AND i.longitude IS NOT NULL GROUP BY i.id ORDER BY i.captured_at DESC - """ - ) + """) results = cursor.fetchall() @@ -821,8 +813,7 @@ def db_get_all_images_for_memories() -> List[dict]: cursor = conn.cursor() try: - cursor.execute( - """ + cursor.execute(""" SELECT i.id, i.path, @@ -840,8 +831,7 @@ def db_get_all_images_for_memories() -> List[dict]: LEFT JOIN mappings m ON ic.class_id = m.class_id GROUP BY i.id ORDER BY i.captured_at DESC - """ - ) + """) results = cursor.fetchall() From 0f4b50ede3064dc38ea2cef92fe90bae67bc8890 Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Thu, 4 Jun 2026 00:49:53 +0530 Subject: [PATCH 06/10] chore: format backend images with CI Black 24.4.2 --- backend/app/database/images.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/backend/app/database/images.py b/backend/app/database/images.py index 3bbc7a0bd..beb60eca1 100644 --- a/backend/app/database/images.py +++ b/backend/app/database/images.py @@ -62,7 +62,8 @@ def db_create_images_table() -> None: cursor = conn.cursor() # Create new images table with merged fields including Memories feature columns - cursor.execute(""" + cursor.execute( + """ CREATE TABLE IF NOT EXISTS images ( id TEXT PRIMARY KEY, path VARCHAR UNIQUE, @@ -76,7 +77,8 @@ def db_create_images_table() -> None: captured_at DATETIME, FOREIGN KEY (folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE ) - """) + """ + ) # Create indexes for Memories feature queries cursor.execute("CREATE INDEX IF NOT EXISTS ix_images_latitude ON images(latitude)") @@ -91,7 +93,8 @@ def db_create_images_table() -> None: ) # Create new image_classes junction table - cursor.execute(""" + cursor.execute( + """ CREATE TABLE IF NOT EXISTS image_classes ( image_id TEXT, class_id INTEGER, @@ -99,7 +102,8 @@ def db_create_images_table() -> None: FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, FOREIGN KEY (class_id) REFERENCES mappings(class_id) ON DELETE CASCADE ) - """) + """ + ) conn.commit() conn.close() @@ -261,13 +265,15 @@ def db_get_untagged_images() -> List[UntaggedImageRecord]: cursor = conn.cursor() try: - cursor.execute(""" + cursor.execute( + """ SELECT i.id, i.path, i.folder_id, i.thumbnailPath, i.metadata FROM images i JOIN folders f ON i.folder_id = f.folder_id WHERE f.AI_Tagging = TRUE AND i.isTagged = FALSE - """) + """ + ) results = cursor.fetchall() @@ -748,7 +754,8 @@ def db_get_images_with_location() -> List[dict]: cursor = conn.cursor() try: - cursor.execute(""" + cursor.execute( + """ SELECT i.id, i.path, @@ -768,7 +775,8 @@ def db_get_images_with_location() -> List[dict]: AND i.longitude IS NOT NULL GROUP BY i.id ORDER BY i.captured_at DESC - """) + """ + ) results = cursor.fetchall() @@ -813,7 +821,8 @@ def db_get_all_images_for_memories() -> List[dict]: cursor = conn.cursor() try: - cursor.execute(""" + cursor.execute( + """ SELECT i.id, i.path, @@ -831,7 +840,8 @@ def db_get_all_images_for_memories() -> List[dict]: LEFT JOIN mappings m ON ic.class_id = m.class_id GROUP BY i.id ORDER BY i.captured_at DESC - """) + """ + ) results = cursor.fetchall() From b1ddb24f18f4bbf21db63881f880ff40f774a476 Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Sat, 6 Jun 2026 08:25:24 +0530 Subject: [PATCH 07/10] fix: use British spelling in toggle-favourite API description --- docs/backend/backend_python/openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backend/backend_python/openapi.json b/docs/backend/backend_python/openapi.json index 90f83983b..462e93beb 100644 --- a/docs/backend/backend_python/openapi.json +++ b/docs/backend/backend_python/openapi.json @@ -857,7 +857,7 @@ "post": { "tags": ["Images"], "summary": "Toggle Favourite", - "description": "Toggle the favorite status of an image.", + "description": "Toggle the favourite status of an image.", "operationId": "toggle_favourite_images_toggle_favourite_post", "requestBody": { "content": { From 7636708e80867f9e0583783a87fede56eece71fd Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Mon, 8 Jun 2026 09:27:46 +0530 Subject: [PATCH 08/10] feat: add close-to-tray toggle in system settings --- docs/backend/backend_python/openapi.json | 18 ++--- frontend/src-tauri/src/main.rs | 41 ++++++++++- .../components/SystemSettingsCard.tsx | 69 +++++++++++++++++-- 3 files changed, 110 insertions(+), 18 deletions(-) diff --git a/docs/backend/backend_python/openapi.json b/docs/backend/backend_python/openapi.json index 462e93beb..b69ee0a4d 100644 --- a/docs/backend/backend_python/openapi.json +++ b/docs/backend/backend_python/openapi.json @@ -857,7 +857,7 @@ "post": { "tags": ["Images"], "summary": "Toggle Favourite", - "description": "Toggle the favourite status of an image.", + "description": "Toggle the favorite status of an image.", "operationId": "toggle_favourite_images_toggle_favourite_post", "requestBody": { "content": { @@ -1074,14 +1074,9 @@ "in": "query", "required": false, "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/InputType" - } - ], + "$ref": "#/components/schemas/InputType", "description": "Choose input type: 'path' or 'base64'", - "default": "path", - "title": "Input Type" + "default": "path" }, "description": "Choose input type: 'path' or 'base64'" } @@ -1341,7 +1336,7 @@ "required": false, "schema": { "type": "number", - "maximum": 100.0, + "maximum": 100, "minimum": 0.1, "description": "Location clustering radius in km", "default": 5.0, @@ -1427,7 +1422,7 @@ "required": false, "schema": { "type": "number", - "maximum": 100.0, + "maximum": 100, "minimum": 0.1, "description": "Location clustering radius in km", "default": 5.0, @@ -1503,7 +1498,7 @@ "required": false, "schema": { "type": "number", - "maximum": 100.0, + "maximum": 100, "minimum": 0.1, "description": "Location clustering radius in km", "default": 5.0, @@ -2614,6 +2609,7 @@ "metadata": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index 9a6b77ca6..ae992016f 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -9,6 +9,10 @@ use tauri::path::BaseDirectory; use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; use tauri::{Manager, Window, WindowEvent}; use tauri_plugin_autostart::ManagerExt; +use tauri_plugin_store::StoreExt; + +const STORE_PATH: &str = "settings.json"; +const CLOSE_TO_TRAY_KEY: &str = "close_to_tray"; const ENDPOINTS: [(&str, &str, &str); 2] = [ ( @@ -45,12 +49,25 @@ fn is_process_alive() -> bool { true } -// Hide the window instead of exiting so the app lives in the system tray. -// The user can quit via the tray context menu's "Quit" item. fn on_window_event(window: &Window, event: &WindowEvent) { if let WindowEvent::CloseRequested { api, .. } = event { + // Always take control of the close event. api.prevent_close(); - let _ = window.hide(); + + let app = window.app_handle().clone(); + let close_to_tray = app + .store(STORE_PATH) + .ok() + .and_then(|s| s.get(CLOSE_TO_TRAY_KEY)) + .and_then(|v| v.as_bool()) + .unwrap_or(true); // default: hide to tray + + if close_to_tray { + let _ = window.hide(); + } else { + let _ = kill_process_tree(); + app.exit(0); + } } } @@ -206,6 +223,22 @@ fn is_autostart_enabled(app: tauri::AppHandle) -> Result { app.autolaunch().is_enabled().map_err(|e| e.to_string()) } +#[tauri::command] +fn get_close_to_tray(app: tauri::AppHandle) -> Result { + let store = app.store(STORE_PATH).map_err(|e| e.to_string())?; + Ok(store + .get(CLOSE_TO_TRAY_KEY) + .and_then(|v| v.as_bool()) + .unwrap_or(true)) +} + +#[tauri::command] +fn set_close_to_tray(app: tauri::AppHandle, enabled: bool) -> Result<(), String> { + let store = app.store(STORE_PATH).map_err(|e| e.to_string())?; + store.set(CLOSE_TO_TRAY_KEY, enabled); + store.save().map_err(|e| e.to_string()) +} + fn main() { tauri::Builder::default() // Auto-start: pass --minimized so the window starts hidden when launched at boot @@ -285,6 +318,8 @@ fn main() { enable_autostart, disable_autostart, is_autostart_enabled, + get_close_to_tray, + set_close_to_tray, ]) .on_window_event(on_window_event) .run(tauri::generate_context!()) diff --git a/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx index f9c9bbe82..aa9e02ef3 100644 --- a/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx +++ b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx @@ -6,14 +6,20 @@ import SettingsCard from './SettingsCard'; const SystemSettingsCard: React.FC = () => { // null = unknown / error reading state const [autostart, setAutostart] = useState(null); + const [closeToTray, setCloseToTray] = useState(null); const [loading, setLoading] = useState(true); const [pending, setPending] = useState(false); + const [pendingCloseToTray, setPendingCloseToTray] = useState(false); useEffect(() => { - invoke('is_autostart_enabled') - .then(setAutostart) - .catch(() => setAutostart(null)) - .finally(() => setLoading(false)); + Promise.all([ + invoke('is_autostart_enabled') + .then(setAutostart) + .catch(() => setAutostart(null)), + invoke('get_close_to_tray') + .then(setCloseToTray) + .catch(() => setCloseToTray(null)), + ]).finally(() => setLoading(false)); }, []); const handleToggle = async () => { @@ -30,9 +36,26 @@ const SystemSettingsCard: React.FC = () => { } }; + const handleCloseToTrayToggle = async () => { + if (closeToTray === null) return; + const next = !closeToTray; + setPendingCloseToTray(true); + try { + await invoke('set_close_to_tray', { enabled: next }); + setCloseToTray(next); + } catch (err) { + console.error('Failed to toggle close-to-tray:', err); + } finally { + setPendingCloseToTray(false); + } + }; + const isDisabled = loading || pending || autostart === null; const isChecked = autostart === true; + const closeToTrayDisabled = loading || pendingCloseToTray || closeToTray === null; + const closeToTrayChecked = closeToTray === true; + return ( { />
+ +
+
+
+ Close to tray +
+
+ When enabled, closing the window hides the app to the system tray + instead of exiting. +
+
+ + +
); }; From c7974e32a74df6bc28f5ff3a8e5f03fc722dd93c Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Mon, 8 Jun 2026 09:38:49 +0530 Subject: [PATCH 09/10] fix: prettier formatting in SystemSettingsCard --- .../pages/SettingsPage/components/SystemSettingsCard.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx index aa9e02ef3..4a05be707 100644 --- a/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx +++ b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx @@ -53,7 +53,8 @@ const SystemSettingsCard: React.FC = () => { const isDisabled = loading || pending || autostart === null; const isChecked = autostart === true; - const closeToTrayDisabled = loading || pendingCloseToTray || closeToTray === null; + const closeToTrayDisabled = + loading || pendingCloseToTray || closeToTray === null; const closeToTrayChecked = closeToTray === true; return ( @@ -105,7 +106,10 @@ const SystemSettingsCard: React.FC = () => {
Close to tray
-
+
When enabled, closing the window hides the app to the system tray instead of exiting.
From 25c3a5f726de0d96badb29838cde79576f649703 Mon Sep 17 00:00:00 2001 From: g-k-s-03 Date: Mon, 8 Jun 2026 09:57:06 +0530 Subject: [PATCH 10/10] fix: use show_menu_on_left_click instead of deprecated menu_on_left_click --- frontend/src-tauri/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index ae992016f..6948846a6 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -280,7 +280,7 @@ fn main() { ) .tooltip("PictoPy") .menu(&menu) - .menu_on_left_click(false) + .show_menu_on_left_click(false) .on_menu_event(|app, event| match event.id.as_ref() { "show" => { if let Some(w) = app.get_webview_window("main") {