Make app fully portable, remove installers

- Remove MSI and NSIS installer targets, only produce vesper.exe
- Remove tauri-plugin-window-state, replace with manual window state
  save/restore to data/window-state.json next to the exe
- Redirect WebView2 user data (including localStorage) to data/
  folder next to the exe via WEBVIEW2_USER_DATA_DIR
- Nothing written to AppData, registry, or any system location
- Update README with portable usage info
This commit is contained in:
Your Name
2026-02-14 12:01:26 +02:00
parent 2af6b7ebe7
commit 99abaac097
9 changed files with 105 additions and 44 deletions

3
.gitignore vendored
View File

@@ -19,6 +19,9 @@ src-tauri/target/
icon-*.html
nul
# Portable data directory
data/
# Editor directories and files
.vscode/*
!.vscode/extensions.json

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