Make app fully portable, remove installers

This commit is contained in:
2026-02-14 12:01:26 +02:00
parent 758a93c9f6
commit 4f60339240
8 changed files with 102 additions and 44 deletions

View File

@@ -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

10
package-lock.json generated
View File

@@ -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",

View File

@@ -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"}}
{"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"}}

16
src-tauri/Cargo.lock generated
View File

@@ -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]]

View File

@@ -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"

View File

@@ -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"
]
}

View File

@@ -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<i32>,
y: Option<i32>,
width: Option<u32>,
height: Option<u32>,
maximized: Option<bool>,
}
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");
}

View File

@@ -30,7 +30,7 @@
},
"bundle": {
"active": true,
"targets": "all",
"targets": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",