diff --git a/.gitignore b/.gitignore index d78d1aa..82d4746 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ src-tauri/target/ icon-*.html nul +# Portable data directory +data/ + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/README.md b/README.md index 4a1dda5..e8119dc 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ A beautiful, distraction-free markdown reader for Windows. Vesper renders your m - **UI scale** (50%--200%) with a spinner in the View menu, persisted across sessions via localStorage - **Kinetic scrolling** on right-mouse-button drag with iOS-style rubber band overscroll and damped spring snapback - **Custom scrollbars** via OverlayScrollbars -- thin, auto-hiding after 800ms, accent-colored on hover -- Window state (size, position, maximized) remembered between sessions via `tauri-plugin-window-state` +- Window state (size, position, maximized) remembered between sessions ### File Handling @@ -107,11 +107,7 @@ A beautiful, distraction-free markdown reader for Windows. Vesper renders your m ### Download -Download the latest installer from the [Releases](https://github.com/yourusername/vesper/releases) page: - -- **NSIS installer** (`.exe`) -- recommended, includes uninstaller -- **MSI installer** (`.msi`) -- alternative for enterprise/group policy deployment -- **Portable exe** -- standalone `vesper.exe` with no installation required +Download the latest `vesper.exe` from the [Releases](https://github.com/yourusername/vesper/releases) page. No installation required -- Vesper is fully portable. ### Build from Source @@ -135,10 +131,7 @@ npm run tauri dev npm run tauri build ``` -Build outputs: -- `src-tauri/target/release/vesper.exe` -- portable executable -- `src-tauri/target/release/bundle/msi/Vesper_1.0.0_x64_en-US.msi` -- MSI installer -- `src-tauri/target/release/bundle/nsis/Vesper_1.0.0_x64-setup.exe` -- NSIS installer +Build output: `src-tauri/target/release/vesper.exe` --- @@ -160,11 +153,21 @@ Build outputs: - `tauri-plugin-dialog` -- native file open dialog - `tauri-plugin-fs` -- file system read access -- `tauri-plugin-window-state` -- persist and restore window size, position, and maximized state - `tauri-plugin-opener` -- system default app launcher --- +## Portable + +Vesper is fully portable. It stores all data in a `data/` folder next to the executable: + +- `data/window-state.json` -- window position, size, and maximized state +- `data/EBWebView/` -- WebView2 data including localStorage (UI zoom preference) + +Nothing is written to AppData, the registry, or any other system location. To move Vesper to another machine, just copy the exe (and optionally the `data/` folder to preserve settings). + +--- + ## Design Philosophy 1. **Content First** -- the reader is the primary focus; all UI chrome can be hidden diff --git a/package-lock.json b/package-lock.json index 541761e..5ea0bcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@tauri-apps/plugin-dialog": "^2.6.0", "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2", - "@tauri-apps/plugin-window-state": "^2.4.1", "daisyui": "^5.5.18", "framer-motion": "^12.34.0", "highlight.js": "^11.11.1", @@ -2196,15 +2195,6 @@ "@tauri-apps/api": "^2.8.0" } }, - "node_modules/@tauri-apps/plugin-window-state": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-window-state/-/plugin-window-state-2.4.1.tgz", - "integrity": "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", diff --git a/package.json b/package.json index df6e737..d90b5bb 100644 --- a/package.json +++ b/package.json @@ -1 +1 @@ -{"name":"vesper","private":true,"version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"tsc && vite build","preview":"vite preview","tauri":"tauri"},"dependencies":{"@fontsource-variable/inter":"^5.2.8","@fontsource-variable/jetbrains-mono":"^5.2.8","@tailwindcss/typography":"^0.5.19","@tailwindcss/vite":"^4.1.18","@tauri-apps/api":"^2","@tauri-apps/plugin-dialog":"^2.6.0","@tauri-apps/plugin-fs":"^2.4.5","@tauri-apps/plugin-opener":"^2","@tauri-apps/plugin-window-state":"^2.4.1","daisyui":"^5.5.18","framer-motion":"^12.34.0","highlight.js":"^11.11.1","lucide-react":"^0.564.0","markdown-it":"^14.1.1","markdown-it-mark":"^4.0.0","markdown-it-sub":"^2.0.0","markdown-it-sup":"^2.0.0","markdown-it-task-lists":"^2.1.1","overlayscrollbars":"^2.14.0","overlayscrollbars-react":"^0.5.6","react":"^19.1.0","react-dom":"^19.1.0","tailwindcss":"^4.1.18"},"devDependencies":{"@tauri-apps/cli":"^2","@types/markdown-it":"^14.1.2","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","png-to-ico":"^3.0.1","puppeteer-core":"^24.37.3","sharp":"^0.34.5","typescript":"~5.8.3","vite":"^7.0.4"}} \ No newline at end of file +{"name":"vesper","private":true,"version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"tsc && vite build","preview":"vite preview","tauri":"tauri"},"dependencies":{"@fontsource-variable/inter":"^5.2.8","@fontsource-variable/jetbrains-mono":"^5.2.8","@tailwindcss/typography":"^0.5.19","@tailwindcss/vite":"^4.1.18","@tauri-apps/api":"^2","@tauri-apps/plugin-dialog":"^2.6.0","@tauri-apps/plugin-fs":"^2.4.5","@tauri-apps/plugin-opener":"^2","daisyui":"^5.5.18","framer-motion":"^12.34.0","highlight.js":"^11.11.1","lucide-react":"^0.564.0","markdown-it":"^14.1.1","markdown-it-mark":"^4.0.0","markdown-it-sub":"^2.0.0","markdown-it-sup":"^2.0.0","markdown-it-task-lists":"^2.1.1","overlayscrollbars":"^2.14.0","overlayscrollbars-react":"^0.5.6","react":"^19.1.0","react-dom":"^19.1.0","tailwindcss":"^4.1.18"},"devDependencies":{"@tauri-apps/cli":"^2","@types/markdown-it":"^14.1.2","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","png-to-ico":"^3.0.1","puppeteer-core":"^24.37.3","sharp":"^0.34.5","typescript":"~5.8.3","vite":"^7.0.4"}} \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 19fc8b7..905df4b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3703,21 +3703,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "tauri-plugin-window-state" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" -dependencies = [ - "bitflags 2.10.0", - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", -] - [[package]] name = "tauri-runtime" version = "2.10.0" @@ -4305,7 +4290,6 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-opener", - "tauri-plugin-window-state", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a0a138b..6dd7da0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,7 +19,6 @@ tauri = { version = "2", features = [] } tauri-plugin-opener = "2" tauri-plugin-dialog = "2" tauri-plugin-fs = "2" -tauri-plugin-window-state = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 7fa2366..4b9890e 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -13,8 +13,6 @@ "core:window:allow-start-dragging", "fs:default", "fs:allow-read-text-file", - "fs:read-all", - "window-state:allow-restore-state", - "window-state:allow-save-window-state" + "fs:read-all" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 62b5904..3e44904 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,10 +1,94 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use tauri::Manager; + +#[derive(Serialize, Deserialize, Default)] +struct WindowState { + x: Option, + y: Option, + width: Option, + height: Option, + maximized: Option, +} + +fn get_data_dir() -> PathBuf { + env::current_exe() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + .unwrap_or_else(|| PathBuf::from(".")) + .join("data") +} + +fn load_window_state() -> WindowState { + let path = get_data_dir().join("window-state.json"); + fs::read_to_string(&path) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() +} + +fn save_window_state(state: &WindowState) { + let dir = get_data_dir(); + let _ = fs::create_dir_all(&dir); + let path = dir.join("window-state.json"); + if let Ok(json) = serde_json::to_string_pretty(state) { + let _ = fs::write(path, json); + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + let data_dir = get_data_dir(); + let _ = fs::create_dir_all(&data_dir); + + // Redirect WebView2 user data to portable location next to the exe + env::set_var( + "WEBVIEW2_USER_DATA_DIR", + data_dir.to_string_lossy().to_string(), + ); + tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_window_state::Builder::new().build()) + .setup(|app| { + let state = load_window_state(); + let window = app.get_webview_window("main").unwrap(); + + if let (Some(x), Some(y)) = (state.x, state.y) { + let _ = window.set_position(tauri::Position::Physical( + tauri::PhysicalPosition::new(x, y), + )); + } + if let (Some(w), Some(h)) = (state.width, state.height) { + let _ = window.set_size(tauri::Size::Physical( + tauri::PhysicalSize::new(w, h), + )); + } + if let Some(true) = state.maximized { + let _ = window.maximize(); + } + + Ok(()) + }) + .on_window_event(|window, event| { + if let tauri::WindowEvent::CloseRequested { .. } = event { + let maximized = window.is_maximized().unwrap_or(false); + if let (Ok(pos), Ok(size)) = (window.outer_position(), window.outer_size()) { + let state = WindowState { + x: Some(pos.x), + y: Some(pos.y), + width: Some(size.width), + height: Some(size.height), + maximized: Some(maximized), + }; + save_window_state(&state); + } + } + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 54145a6..44e6c15 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -30,7 +30,7 @@ }, "bundle": { "active": true, - "targets": "all", + "targets": [], "icon": [ "icons/32x32.png", "icons/128x128.png",