From c6fea186ef2900f5d657c30cbe12af9c15892ba6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Feb 2026 22:18:50 +0200 Subject: [PATCH] feat: custom scrollbars, portable storage, window state persistence - Custom scrollbar CSS using ::-webkit-scrollbar for Tauri's Chromium WebView - Portable storage: all data written next to exe in data/ folder instead of AppData - Rust get_portable_data_dir command with runtime FS scope for exe directory - Window size/position/maximized saved to settings on close, restored on startup --- src-tauri/capabilities/default.json | 7 +++--- src-tauri/src/lib.rs | 33 ++++++++++++++++++++++++ src/App.tsx | 39 +++++++++++++++++++++++++++-- src/index.css | 33 ++++++++++++++++++++++++ src/lib/schemas.ts | 9 +++++++ src/lib/storage.ts | 8 +++--- src/stores/app-store.ts | 1 + src/types/settings.ts | 9 +++++++ 8 files changed, 130 insertions(+), 9 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 343f402..aeef755 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -17,8 +17,9 @@ "dialog:default", "shell:default", "fs:default", - "fs:allow-appdata-read-recursive", - "fs:allow-appdata-write-recursive", - "fs:allow-appdata-meta-recursive" + "core:window:allow-set-size", + "core:window:allow-set-position", + "core:window:allow-outer-size", + "core:window:allow-outer-position" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5b615b7..49e9e63 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,15 @@ +use tauri_plugin_fs::FsExt; + +#[tauri::command] +fn get_portable_data_dir() -> Result { + let exe_path = std::env::current_exe().map_err(|e| e.to_string())?; + let exe_dir = exe_path + .parent() + .ok_or_else(|| "Failed to get exe directory".to_string())?; + let data_dir = exe_dir.join("data"); + Ok(data_dir.to_string_lossy().to_string()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -5,6 +17,27 @@ pub fn run() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) + .setup(|app| { + // Get portable data directory next to the exe + let exe_path = + std::env::current_exe().expect("Failed to get exe path"); + let exe_dir = exe_path + .parent() + .expect("Failed to get exe directory"); + let data_dir = exe_dir.join("data"); + + // Ensure data directory exists + std::fs::create_dir_all(&data_dir) + .expect("Failed to create portable data directory"); + + // Allow FS plugin access to the portable data directory + app.fs_scope() + .allow_directory(&data_dir, true) + .expect("Failed to allow data directory in FS scope"); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![get_portable_data_dir]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/App.tsx b/src/App.tsx index a98f9ab..eeab6a9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ import { useState, useEffect, useCallback } from "react"; +import { getCurrentWindow, LogicalSize, LogicalPosition } from "@tauri-apps/api/window"; import { AnimatePresence, motion } from "framer-motion"; import { springs, fadeSlideLeft, fadeSlideRight } from "@/lib/motion"; import { useAppStore } from "@/stores/app-store"; import { useBoardStore } from "@/stores/board-store"; +import { saveSettings } from "@/lib/storage"; import { AppShell } from "@/components/layout/AppShell"; import { BoardList } from "@/components/boards/BoardList"; import { BoardView } from "@/components/board/BoardView"; @@ -21,13 +23,46 @@ export default function App() { const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false); useEffect(() => { - init(); + init().then(() => { + // Restore window state after settings are loaded + const { settings } = useAppStore.getState(); + const ws = settings.windowState; + if (ws) { + const appWindow = getCurrentWindow(); + if (ws.maximized) { + appWindow.maximize(); + } else { + appWindow.setSize(new LogicalSize(ws.width, ws.height)); + appWindow.setPosition(new LogicalPosition(ws.x, ws.y)); + } + } + }); }, [init]); - // Flush pending board saves before the app window closes + // Save window state + flush board saves before the app window closes useEffect(() => { function handleBeforeUnload() { useBoardStore.getState().closeBoard(); + + // Save window state synchronously-ish (fire and forget) + const appWindow = getCurrentWindow(); + Promise.all([ + appWindow.outerSize(), + appWindow.outerPosition(), + appWindow.isMaximized(), + ]).then(([size, position, maximized]) => { + const settings = useAppStore.getState().settings; + saveSettings({ + ...settings, + windowState: { + x: position.x, + y: position.y, + width: size.width, + height: size.height, + maximized, + }, + }); + }); } window.addEventListener("beforeunload", handleBeforeUnload); return () => { diff --git a/src/index.css b/src/index.css index 33eae4d..a0ee237 100644 --- a/src/index.css +++ b/src/index.css @@ -148,6 +148,39 @@ .dark * { scrollbar-color: oklch(80% 0 0 / 15%) transparent; } + + /* Custom scrollbar for Chromium/WebKit (Tauri WebView) */ + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + ::-webkit-scrollbar-track { + background: transparent; + } + ::-webkit-scrollbar-thumb { + background: oklch(50% 0 0 / 20%); + border-radius: 9999px; + border: 2px solid transparent; + background-clip: content-box; + } + ::-webkit-scrollbar-thumb:hover { + background: oklch(50% 0 0 / 35%); + border: 2px solid transparent; + background-clip: content-box; + } + ::-webkit-scrollbar-corner { + background: transparent; + } + .dark ::-webkit-scrollbar-thumb { + background: oklch(80% 0 0 / 15%); + border: 2px solid transparent; + background-clip: content-box; + } + .dark ::-webkit-scrollbar-thumb:hover { + background: oklch(80% 0 0 / 30%); + border: 2px solid transparent; + background-clip: content-box; + } body { @apply bg-background text-foreground; font-family: "Satoshi", system-ui, -apple-system, sans-serif; diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index ab68031..98ef95f 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -57,6 +57,14 @@ export const boardSchema = z.object({ settings: boardSettingsSchema.default({ attachmentMode: "link", background: "none" }), }); +export const windowStateSchema = z.object({ + x: z.number(), + y: z.number(), + width: z.number(), + height: z.number(), + maximized: z.boolean(), +}); + export const appSettingsSchema = z.object({ theme: z.enum(["light", "dark", "system"]).default("system"), dataDirectory: z.string().nullable().default(null), @@ -65,4 +73,5 @@ export const appSettingsSchema = z.object({ uiZoom: z.number().min(0.75).max(1.5).default(1), density: z.enum(["compact", "comfortable", "spacious"]).default("comfortable"), defaultColumnWidth: z.enum(["narrow", "standard", "wide"]).default("standard"), + windowState: windowStateSchema.nullable().default(null), }); diff --git a/src/lib/storage.ts b/src/lib/storage.ts index a650365..a8a901c 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -7,18 +7,18 @@ import { remove, copyFile, } from "@tauri-apps/plugin-fs"; -import { appDataDir, join } from "@tauri-apps/api/path"; +import { join } from "@tauri-apps/api/path"; +import { invoke } from "@tauri-apps/api/core"; import { boardSchema, appSettingsSchema } from "./schemas"; import type { Board, BoardMeta } from "@/types/board"; import type { AppSettings } from "@/types/settings"; // --------------------------------------------------------------------------- -// Path helpers +// Path helpers — portable: all data lives next to the exe // --------------------------------------------------------------------------- async function getBaseDir(): Promise { - const base = await appDataDir(); - return join(base, "openpylon"); + return invoke("get_portable_data_dir"); } async function getBoardsDir(): Promise { diff --git a/src/stores/app-store.ts b/src/stores/app-store.ts index 1b74fb7..24f3d38 100644 --- a/src/stores/app-store.ts +++ b/src/stores/app-store.ts @@ -62,6 +62,7 @@ export const useAppStore = create((set, get) => ({ uiZoom: 1, density: "comfortable", defaultColumnWidth: "standard", + windowState: null, }, boards: [], view: { type: "board-list" }, diff --git a/src/types/settings.ts b/src/types/settings.ts index 5536e7e..53f6062 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -1,5 +1,13 @@ import type { ColumnWidth } from "./board"; +export interface WindowState { + x: number; + y: number; + width: number; + height: number; + maximized: boolean; +} + export interface AppSettings { theme: "light" | "dark" | "system"; dataDirectory: string | null; @@ -8,4 +16,5 @@ export interface AppSettings { uiZoom: number; density: "compact" | "comfortable" | "spacious"; defaultColumnWidth: ColumnWidth; + windowState: WindowState | null; }