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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,6 +19,9 @@ src-tauri/target/
|
||||
icon-*.html
|
||||
nul
|
||||
|
||||
# Portable data directory
|
||||
data/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
25
README.md
25
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
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
16
src-tauri/Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"targets": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
Reference in New Issue
Block a user