mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Feat: Implement Storage-based (JSON) Settings & Data-Storage (WIP) (#56)
This commit is contained in:
@@ -71,7 +71,7 @@
|
|||||||
inherit version;
|
inherit version;
|
||||||
src = frontendSrc;
|
src = frontendSrc;
|
||||||
fetcherVersion = 1;
|
fetcherVersion = 1;
|
||||||
hash = "sha256-nlhm3NYn4x+JlKcCgj1lAX43muB3QRKGDzaxfQNfJwc=";
|
hash = "sha256-t6Gj84hCE3CuDAJfbdXi0FuqgPCqlkMmAzETcKL4e3U=";
|
||||||
};
|
};
|
||||||
|
|
||||||
buildPhase = "pnpm build";
|
buildPhase = "pnpm build";
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@tauri-apps/plugin-http": "^2.5.8",
|
"@tauri-apps/plugin-http": "^2.5.8",
|
||||||
"@tauri-apps/plugin-os": "^2.3.2",
|
"@tauri-apps/plugin-os": "^2.3.2",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||||
|
"@tauri-apps/plugin-store": "~2.4.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"phosphor-svelte": "^3.1.0",
|
"phosphor-svelte": "^3.1.0",
|
||||||
"svelte-spa-router": "^4.0.1",
|
"svelte-spa-router": "^4.0.1",
|
||||||
|
|||||||
Generated
+10
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-shell':
|
'@tauri-apps/plugin-shell':
|
||||||
specifier: ^2.3.5
|
specifier: ^2.3.5
|
||||||
version: 2.3.5
|
version: 2.3.5
|
||||||
|
'@tauri-apps/plugin-store':
|
||||||
|
specifier: ~2.4.2
|
||||||
|
version: 2.4.2
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
@@ -457,6 +460,9 @@ packages:
|
|||||||
'@tauri-apps/plugin-shell@2.3.5':
|
'@tauri-apps/plugin-shell@2.3.5':
|
||||||
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
|
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.2':
|
||||||
|
resolution: {integrity: sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
@@ -1071,6 +1077,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.10.1
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.2':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/pug@2.0.10': {}
|
'@types/pug@2.0.10': {}
|
||||||
|
|||||||
Generated
+29
@@ -2107,6 +2107,7 @@ dependencies = [
|
|||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
|
"tauri-plugin-store",
|
||||||
"tokio",
|
"tokio",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -4435,6 +4436,22 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-store"
|
||||||
|
version = "2.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
@@ -4896,9 +4913,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ tauri-plugin-process = "2"
|
|||||||
tauri-plugin-http = "2"
|
tauri-plugin-http = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-os = "2.3.2"
|
tauri-plugin-os = "2.3.2"
|
||||||
|
tauri-plugin-store = "2"
|
||||||
tauri-plugin-discord-rpc = { git = "https://github.com/Youwes09/tauri-plugin-discord-rpc" }
|
tauri-plugin-discord-rpc = { git = "https://github.com/Youwes09/tauri-plugin-discord-rpc" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"process:allow-restart",
|
"process:allow-restart",
|
||||||
"http:default",
|
"http:default",
|
||||||
"http:allow-fetch",
|
"http:allow-fetch",
|
||||||
|
"store:default",
|
||||||
"discord-rpc:default",
|
"discord-rpc:default",
|
||||||
"discord-rpc:allow-connect",
|
"discord-rpc:allow-connect",
|
||||||
"discord-rpc:allow-disconnect",
|
"discord-rpc:allow-disconnect",
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use std::path::PathBuf;
|
|||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
fn backup_dir(app: &tauri::AppHandle) -> PathBuf {
|
fn backup_dir(app: &tauri::AppHandle) -> PathBuf {
|
||||||
app.path().app_data_dir()
|
app.path()
|
||||||
|
.app_data_dir()
|
||||||
.unwrap_or_else(|_| PathBuf::from("."))
|
.unwrap_or_else(|_| PathBuf::from("."))
|
||||||
.join("backups")
|
.join("backups")
|
||||||
}
|
}
|
||||||
@@ -20,7 +21,8 @@ pub async fn export_app_data(app: tauri::AppHandle, json: String) -> Result<Stri
|
|||||||
|
|
||||||
let filename = format!("moku-backup-{}.json", unix_now());
|
let filename = format!("moku-backup-{}.json", unix_now());
|
||||||
|
|
||||||
let path = app.dialog()
|
let path = app
|
||||||
|
.dialog()
|
||||||
.file()
|
.file()
|
||||||
.set_title("Save Moku app data backup")
|
.set_title("Save Moku app data backup")
|
||||||
.set_file_name(&filename)
|
.set_file_name(&filename)
|
||||||
@@ -37,7 +39,8 @@ pub async fn export_app_data(app: tauri::AppHandle, json: String) -> Result<Stri
|
|||||||
pub async fn import_app_data(app: tauri::AppHandle) -> Result<String, String> {
|
pub async fn import_app_data(app: tauri::AppHandle) -> Result<String, String> {
|
||||||
use tauri_plugin_dialog::DialogExt;
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
|
||||||
let path = app.dialog()
|
let path = app
|
||||||
|
.dialog()
|
||||||
.file()
|
.file()
|
||||||
.set_title("Open Moku app data backup")
|
.set_title("Open Moku app data backup")
|
||||||
.blocking_pick_file()
|
.blocking_pick_file()
|
||||||
@@ -57,7 +60,11 @@ pub fn auto_backup_app_data(app: tauri::AppHandle, json: String) -> Result<(), S
|
|||||||
let mut entries: Vec<_> = std::fs::read_dir(&dir)
|
let mut entries: Vec<_> = std::fs::read_dir(&dir)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
.filter(|e| e.file_name().to_string_lossy().starts_with("auto-moku-backup-"))
|
.filter(|e| {
|
||||||
|
e.file_name()
|
||||||
|
.to_string_lossy()
|
||||||
|
.starts_with("auto-moku-backup-")
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
entries.sort_by_key(|e| e.file_name());
|
entries.sort_by_key(|e| e.file_name());
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use tauri::Manager;
|
|
||||||
use crate::server::{self, resolve::suwayomi_data_dir, SpawnError};
|
use crate::server::{self, resolve::suwayomi_data_dir, SpawnError};
|
||||||
use crate::ServerState;
|
use crate::ServerState;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), SpawnError> {
|
pub fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), SpawnError> {
|
||||||
@@ -14,16 +14,24 @@ pub fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), SpawnEr
|
|||||||
let data_dir = suwayomi_data_dir();
|
let data_dir = suwayomi_data_dir();
|
||||||
let log_path = data_dir.join("moku-spawn.log");
|
let log_path = data_dir.join("moku-spawn.log");
|
||||||
let _ = std::fs::create_dir_all(&data_dir);
|
let _ = std::fs::create_dir_all(&data_dir);
|
||||||
let mut log = std::fs::OpenOptions::new().create(true).append(true).open(&log_path).ok();
|
let mut log = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&log_path)
|
||||||
|
.ok();
|
||||||
|
|
||||||
server::do_log(&mut log, &format!("[spawn_server] binary={:?} data_dir={:?}", binary, data_dir));
|
server::do_log(
|
||||||
|
&mut log,
|
||||||
|
&format!("[spawn_server] binary={:?} data_dir={:?}", binary, data_dir),
|
||||||
|
);
|
||||||
|
|
||||||
server::conf::seed_server_conf(&data_dir);
|
server::conf::seed_server_conf(&data_dir);
|
||||||
|
|
||||||
let mut invocation = server::resolve::resolve_server_binary(&binary, &app, &mut log).map_err(|e| {
|
let mut invocation =
|
||||||
server::do_log(&mut log, &format!("[spawn_server] resolve failed: {:?}", e));
|
server::resolve::resolve_server_binary(&binary, &app, &mut log).map_err(|e| {
|
||||||
e
|
server::do_log(&mut log, &format!("[spawn_server] resolve failed: {:?}", e));
|
||||||
})?;
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
if invocation.bin.ends_with("java") || invocation.bin.ends_with("java.exe") {
|
if invocation.bin.ends_with("java") || invocation.bin.ends_with("java.exe") {
|
||||||
let rootdir_flag = format!(
|
let rootdir_flag = format!(
|
||||||
@@ -33,12 +41,21 @@ pub fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), SpawnEr
|
|||||||
invocation.args.insert(0, rootdir_flag);
|
invocation.args.insert(0, rootdir_flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
let working_dir = invocation.working_dir.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
let working_dir = invocation
|
||||||
|
.working_dir
|
||||||
|
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
||||||
|
|
||||||
server::do_log(&mut log, &format!("[spawn_server] bin={:?} args={:?} cwd={:?}", invocation.bin, invocation.args, working_dir));
|
server::do_log(
|
||||||
|
&mut log,
|
||||||
|
&format!(
|
||||||
|
"[spawn_server] bin={:?} args={:?} cwd={:?}",
|
||||||
|
invocation.bin, invocation.args, working_dir
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
let cmd = app.shell()
|
let cmd = app
|
||||||
|
.shell()
|
||||||
.command(&invocation.bin)
|
.command(&invocation.bin)
|
||||||
.env("JAVA_TOOL_OPTIONS", "-Djava.awt.headless=true")
|
.env("JAVA_TOOL_OPTIONS", "-Djava.awt.headless=true")
|
||||||
.args(&invocation.args)
|
.args(&invocation.args)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::path::PathBuf;
|
||||||
use sysinfo::Disks;
|
use sysinfo::Disks;
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
@@ -10,8 +10,8 @@ use crate::server::resolve::suwayomi_data_dir;
|
|||||||
pub struct StorageInfo {
|
pub struct StorageInfo {
|
||||||
pub manga_bytes: u64,
|
pub manga_bytes: u64,
|
||||||
pub total_bytes: u64,
|
pub total_bytes: u64,
|
||||||
pub free_bytes: u64,
|
pub free_bytes: u64,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_downloads_path(downloads_path: &str) -> PathBuf {
|
fn resolve_downloads_path(downloads_path: &str) -> PathBuf {
|
||||||
@@ -53,8 +53,8 @@ pub fn get_storage_info(downloads_path: String) -> Result<StorageInfo, String> {
|
|||||||
Ok(StorageInfo {
|
Ok(StorageInfo {
|
||||||
manga_bytes,
|
manga_bytes,
|
||||||
total_bytes: disk.total_space(),
|
total_bytes: disk.total_space(),
|
||||||
free_bytes: disk.available_space(),
|
free_bytes: disk.available_space(),
|
||||||
path: path.to_string_lossy().into_owned(),
|
path: path.to_string_lossy().into_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,11 @@ pub fn create_directory(path: String) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn migrate_downloads(app: tauri::AppHandle, src: String, dst: String) -> Result<(), String> {
|
pub async fn migrate_downloads(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
src: String,
|
||||||
|
dst: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
let src_path = PathBuf::from(src.trim());
|
let src_path = PathBuf::from(src.trim());
|
||||||
@@ -90,12 +94,18 @@ pub async fn migrate_downloads(app: tauri::AppHandle, src: String, dst: String)
|
|||||||
.filter(|e| e.file_type().is_file())
|
.filter(|e| e.file_type().is_file())
|
||||||
.count() as u64;
|
.count() as u64;
|
||||||
|
|
||||||
let _ = app.emit("migrate_progress", serde_json::json!({ "done": 0u64, "total": total, "current": "" }));
|
let _ = app.emit(
|
||||||
|
"migrate_progress",
|
||||||
|
serde_json::json!({ "done": 0u64, "total": total, "current": "" }),
|
||||||
|
);
|
||||||
|
|
||||||
let mut done: u64 = 0;
|
let mut done: u64 = 0;
|
||||||
|
|
||||||
for entry in WalkDir::new(&src_path).into_iter().filter_map(|e| e.ok()) {
|
for entry in WalkDir::new(&src_path).into_iter().filter_map(|e| e.ok()) {
|
||||||
let rel = entry.path().strip_prefix(&src_path).map_err(|e| e.to_string())?;
|
let rel = entry
|
||||||
|
.path()
|
||||||
|
.strip_prefix(&src_path)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
let target = dst_path.join(rel);
|
let target = dst_path.join(rel);
|
||||||
|
|
||||||
if entry.file_type().is_dir() {
|
if entry.file_type().is_dir() {
|
||||||
@@ -106,9 +116,12 @@ pub async fn migrate_downloads(app: tauri::AppHandle, src: String, dst: String)
|
|||||||
}
|
}
|
||||||
fs::copy(entry.path(), &target).map_err(|e| e.to_string())?;
|
fs::copy(entry.path(), &target).map_err(|e| e.to_string())?;
|
||||||
done += 1;
|
done += 1;
|
||||||
let _ = app.emit("migrate_progress", serde_json::json!({
|
let _ = app.emit(
|
||||||
"done": done, "total": total, "current": rel.to_string_lossy()
|
"migrate_progress",
|
||||||
}));
|
serde_json::json!({
|
||||||
|
"done": done, "total": total, "current": rel.to_string_lossy()
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use tauri::Manager;
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use crate::server::resolve::strip_unc;
|
use crate::server::resolve::strip_unc;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_platform_ui_scale(window: tauri::Window) -> f64 {
|
pub fn get_platform_ui_scale(window: tauri::Window) -> f64 {
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
pub struct ReleaseInfo {
|
pub struct ReleaseInfo {
|
||||||
pub tag_name: String,
|
pub tag_name: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub published_at: String,
|
pub published_at: String,
|
||||||
pub html_url: String,
|
pub html_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||||
struct UpdateProgress {
|
struct UpdateProgress {
|
||||||
downloaded: u64,
|
downloaded: u64,
|
||||||
total: Option<u64>,
|
total: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -22,11 +22,11 @@ pub async fn list_releases() -> Result<Vec<ReleaseInfo>, String> {
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct GhRelease {
|
struct GhRelease {
|
||||||
tag_name: String,
|
tag_name: String,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
published_at: Option<String>,
|
published_at: Option<String>,
|
||||||
html_url: String,
|
html_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
@@ -44,17 +44,20 @@ pub async fn list_releases() -> Result<Vec<ReleaseInfo>, String> {
|
|||||||
return Err(format!("GitHub API returned {}", resp.status()));
|
return Err(format!("GitHub API returned {}", resp.status()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let releases: Vec<GhRelease> = serde_json::from_str(
|
let releases: Vec<GhRelease> =
|
||||||
&resp.text().await.map_err(|e| e.to_string())?
|
serde_json::from_str(&resp.text().await.map_err(|e| e.to_string())?)
|
||||||
).map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
Ok(releases.into_iter().map(|r| ReleaseInfo {
|
Ok(releases
|
||||||
tag_name: r.tag_name.clone(),
|
.into_iter()
|
||||||
name: r.name.unwrap_or_else(|| r.tag_name.clone()),
|
.map(|r| ReleaseInfo {
|
||||||
body: r.body.unwrap_or_default(),
|
tag_name: r.tag_name.clone(),
|
||||||
published_at: r.published_at.unwrap_or_default(),
|
name: r.name.unwrap_or_else(|| r.tag_name.clone()),
|
||||||
html_url: r.html_url,
|
body: r.body.unwrap_or_default(),
|
||||||
}).collect())
|
published_at: r.published_at.unwrap_or_default(),
|
||||||
|
html_url: r.html_url,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -70,9 +73,15 @@ pub async fn download_and_install_update(app: tauri::AppHandle, tag: String) ->
|
|||||||
use tauri_plugin_http::reqwest;
|
use tauri_plugin_http::reqwest;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Asset { name: String, browser_download_url: String, size: u64 }
|
struct Asset {
|
||||||
|
name: String,
|
||||||
|
browser_download_url: String,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Release { assets: Vec<Asset> }
|
struct Release {
|
||||||
|
assets: Vec<Asset>,
|
||||||
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.user_agent("Moku")
|
.user_agent("Moku")
|
||||||
@@ -80,26 +89,41 @@ pub async fn download_and_install_update(app: tauri::AppHandle, tag: String) ->
|
|||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(format!("https://api.github.com/repos/moku-project/Moku/releases/tags/{}", tag))
|
.get(format!(
|
||||||
|
"https://api.github.com/repos/moku-project/Moku/releases/tags/{}",
|
||||||
|
tag
|
||||||
|
))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
return Err(format!("GitHub API returned {} for tag {}", resp.status(), tag));
|
return Err(format!(
|
||||||
|
"GitHub API returned {} for tag {}",
|
||||||
|
resp.status(),
|
||||||
|
tag
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let release: Release = serde_json::from_str(
|
let release: Release = serde_json::from_str(&resp.text().await.map_err(|e| e.to_string())?)
|
||||||
&resp.text().await.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?;
|
||||||
).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let asset = release.assets
|
let asset = release
|
||||||
|
.assets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|a| a.name.ends_with("_x64-setup.exe"))
|
.find(|a| a.name.ends_with("_x64-setup.exe"))
|
||||||
.ok_or_else(|| format!("No x64-setup.exe asset found in release {}", tag))?;
|
.ok_or_else(|| format!("No x64-setup.exe asset found in release {}", tag))?;
|
||||||
|
|
||||||
let total = if asset.size > 0 { Some(asset.size) } else { None };
|
let total = if asset.size > 0 {
|
||||||
let mut resp = client.get(&asset.browser_download_url).send().await.map_err(|e| e.to_string())?;
|
Some(asset.size)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let mut resp = client
|
||||||
|
.get(&asset.browser_download_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let tmp_path = std::env::temp_dir().join(&asset.name);
|
let tmp_path = std::env::temp_dir().join(&asset.name);
|
||||||
let mut file = std::fs::File::create(&tmp_path).map_err(|e| e.to_string())?;
|
let mut file = std::fs::File::create(&tmp_path).map_err(|e| e.to_string())?;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ pub struct ServerState(pub Mutex<Option<CommandChild>>);
|
|||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_store::Builder::default().build())
|
||||||
.plugin(tauri_plugin_discord_rpc::init())
|
.plugin(tauri_plugin_discord_rpc::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
|
|||||||
@@ -31,14 +31,18 @@ pub fn seed_server_conf(data_dir: &PathBuf) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(contents) = std::fs::read_to_string(&conf_path) else { return };
|
let Ok(contents) = std::fs::read_to_string(&conf_path) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let patched = patch_conf_key(
|
let patched = patch_conf_key(
|
||||||
patch_conf_key(
|
patch_conf_key(
|
||||||
patch_conf_key(contents, "server.webUIEnabled", "false"),
|
patch_conf_key(contents, "server.webUIEnabled", "false"),
|
||||||
"server.initialOpenInBrowserEnabled", "false",
|
"server.initialOpenInBrowserEnabled",
|
||||||
|
"false",
|
||||||
),
|
),
|
||||||
"server.systemTrayEnabled", "false",
|
"server.systemTrayEnabled",
|
||||||
|
"false",
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = std::fs::write(&conf_path, patched);
|
let _ = std::fs::write(&conf_path, patched);
|
||||||
@@ -60,7 +64,9 @@ fn patch_conf_key(text: String, key: &str, value: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut out = text;
|
let mut out = text;
|
||||||
if !out.ends_with('\n') { out.push('\n'); }
|
if !out.ends_with('\n') {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
out.push_str(&replacement);
|
out.push_str(&replacement);
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
out
|
out
|
||||||
|
|||||||
@@ -39,11 +39,15 @@ pub fn kill_tachidesk(app: &tauri::AppHandle) {
|
|||||||
.map(|o| String::from_utf8_lossy(&o.stdout).contains("java.exe"))
|
.map(|o| String::from_utf8_lossy(&o.stdout).contains("java.exe"))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if !still_running { break; }
|
if !still_running {
|
||||||
|
break;
|
||||||
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let _ = std::process::Command::new("pkill").args(["-f", "tachidesk"]).status();
|
let _ = std::process::Command::new("pkill")
|
||||||
|
.args(["-f", "tachidesk"])
|
||||||
|
.status();
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use serde::Serialize;
|
|
||||||
use tauri::Manager;
|
|
||||||
use crate::server::do_log;
|
use crate::server::do_log;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
#[serde(tag = "kind", content = "message")]
|
#[serde(tag = "kind", content = "message")]
|
||||||
@@ -11,8 +11,8 @@ pub enum SpawnError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ServerInvocation {
|
pub struct ServerInvocation {
|
||||||
pub bin: String,
|
pub bin: String,
|
||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
pub working_dir: Option<PathBuf>,
|
pub working_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,8 +54,15 @@ fn find_java_in_bundle(bundle_dir: &PathBuf, log: &mut Option<std::fs::File>) ->
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let java = bundle_dir.join("jre").join("bin").join("java");
|
let java = bundle_dir.join("jre").join("bin").join("java");
|
||||||
|
|
||||||
do_log(log, &format!("[find_java] path: {:?} exists: {}", java, java.exists()));
|
do_log(
|
||||||
if java.exists() { Some(java) } else { None }
|
log,
|
||||||
|
&format!("[find_java] path: {:?} exists: {}", java, java.exists()),
|
||||||
|
);
|
||||||
|
if java.exists() {
|
||||||
|
Some(java)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_server_binary(
|
pub fn resolve_server_binary(
|
||||||
@@ -67,11 +74,14 @@ pub fn resolve_server_binary(
|
|||||||
|
|
||||||
if !binary.trim().is_empty() {
|
if !binary.trim().is_empty() {
|
||||||
let path = strip_unc(PathBuf::from(binary.trim()));
|
let path = strip_unc(PathBuf::from(binary.trim()));
|
||||||
do_log(log, &format!("[resolve] user path: {:?} exists={}", path, path.exists()));
|
do_log(
|
||||||
|
log,
|
||||||
|
&format!("[resolve] user path: {:?} exists={}", path, path.exists()),
|
||||||
|
);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: path.to_string_lossy().into_owned(),
|
bin: path.to_string_lossy().into_owned(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
working_dir: path.parent().map(|p| p.to_path_buf()),
|
working_dir: path.parent().map(|p| p.to_path_buf()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -82,11 +92,14 @@ pub fn resolve_server_binary(
|
|||||||
if let Some(bin_dir) = exe.parent() {
|
if let Some(bin_dir) = exe.parent() {
|
||||||
for name in &["tachidesk-server", "suwayomi-launcher"] {
|
for name in &["tachidesk-server", "suwayomi-launcher"] {
|
||||||
let p = bin_dir.join(name);
|
let p = bin_dir.join(name);
|
||||||
do_log(log, &format!("[resolve] sibling: {:?} exists={}", p, p.exists()));
|
do_log(
|
||||||
|
log,
|
||||||
|
&format!("[resolve] sibling: {:?} exists={}", p, p.exists()),
|
||||||
|
);
|
||||||
if p.exists() {
|
if p.exists() {
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: p.to_string_lossy().into_owned(),
|
bin: p.to_string_lossy().into_owned(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
working_dir: Some(bin_dir.to_path_buf()),
|
working_dir: Some(bin_dir.to_path_buf()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -105,16 +118,26 @@ pub fn resolve_server_binary(
|
|||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
{
|
{
|
||||||
let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle");
|
let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle");
|
||||||
let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar");
|
let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar");
|
||||||
|
|
||||||
do_log(log, &format!("[resolve] bundle_dir={:?} exists={}", bundle_dir, bundle_dir.exists()));
|
do_log(
|
||||||
do_log(log, &format!("[resolve] jar={:?} exists={}", jar, jar.exists()));
|
log,
|
||||||
|
&format!(
|
||||||
|
"[resolve] bundle_dir={:?} exists={}",
|
||||||
|
bundle_dir,
|
||||||
|
bundle_dir.exists()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
do_log(
|
||||||
|
log,
|
||||||
|
&format!("[resolve] jar={:?} exists={}", jar, jar.exists()),
|
||||||
|
);
|
||||||
|
|
||||||
match find_java_in_bundle(&bundle_dir, log) {
|
match find_java_in_bundle(&bundle_dir, log) {
|
||||||
Some(java) if jar.exists() => {
|
Some(java) if jar.exists() => {
|
||||||
do_log(log, "[resolve] using bundled JRE");
|
do_log(log, "[resolve] using bundled JRE");
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: java.to_string_lossy().into_owned(),
|
bin: java.to_string_lossy().into_owned(),
|
||||||
args: vec!["-jar".to_string(), jar.to_string_lossy().into_owned()],
|
args: vec!["-jar".to_string(), jar.to_string_lossy().into_owned()],
|
||||||
working_dir: Some(bundle_dir),
|
working_dir: Some(bundle_dir),
|
||||||
});
|
});
|
||||||
@@ -122,31 +145,43 @@ pub fn resolve_server_binary(
|
|||||||
_ => do_log(log, "[resolve] bundled JRE/jar not found, falling through"),
|
_ => do_log(log, "[resolve] bundled JRE/jar not found, falling through"),
|
||||||
}
|
}
|
||||||
|
|
||||||
for name in &["suwayomi-launcher", "suwayomi-launcher.sh", "tachidesk-server"] {
|
for name in &[
|
||||||
|
"suwayomi-launcher",
|
||||||
|
"suwayomi-launcher.sh",
|
||||||
|
"tachidesk-server",
|
||||||
|
] {
|
||||||
let p = resource_dir.join(name);
|
let p = resource_dir.join(name);
|
||||||
do_log(log, &format!("[resolve] sidecar: {:?} exists={}", p, p.exists()));
|
do_log(
|
||||||
|
log,
|
||||||
|
&format!("[resolve] sidecar: {:?} exists={}", p, p.exists()),
|
||||||
|
);
|
||||||
if p.exists() {
|
if p.exists() {
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: p.to_string_lossy().into_owned(),
|
bin: p.to_string_lossy().into_owned(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
working_dir: Some(resource_dir.clone()),
|
working_dir: Some(resource_dir.clone()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(java) = find_java_in_bundle(&resource_dir, log) {
|
if let Some(java) = find_java_in_bundle(&resource_dir, log) {
|
||||||
let jar = std::fs::read_dir(&resource_dir)
|
let jar = std::fs::read_dir(&resource_dir).ok().and_then(|mut rd| {
|
||||||
.ok()
|
rd.find(|e| {
|
||||||
.and_then(|mut rd| {
|
e.as_ref()
|
||||||
rd.find(|e| e.as_ref().map(|e| e.file_name().to_string_lossy().ends_with(".jar")).unwrap_or(false))
|
.map(|e| e.file_name().to_string_lossy().ends_with(".jar"))
|
||||||
.and_then(|e| e.ok())
|
.unwrap_or(false)
|
||||||
.map(|e| e.path())
|
})
|
||||||
});
|
.and_then(|e| e.ok())
|
||||||
|
.map(|e| e.path())
|
||||||
|
});
|
||||||
|
|
||||||
if let Some(jar_path) = jar {
|
if let Some(jar_path) = jar {
|
||||||
do_log(log, &format!("[resolve] generic JRE java={:?} jar={:?}", java, jar_path));
|
do_log(
|
||||||
|
log,
|
||||||
|
&format!("[resolve] generic JRE java={:?} jar={:?}", java, jar_path),
|
||||||
|
);
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: java.to_string_lossy().into_owned(),
|
bin: java.to_string_lossy().into_owned(),
|
||||||
args: vec!["-jar".to_string(), jar_path.to_string_lossy().into_owned()],
|
args: vec!["-jar".to_string(), jar_path.to_string_lossy().into_owned()],
|
||||||
working_dir: Some(resource_dir),
|
working_dir: Some(resource_dir),
|
||||||
});
|
});
|
||||||
@@ -159,7 +194,10 @@ pub fn resolve_server_binary(
|
|||||||
let resource_dir = app.path().resource_dir().unwrap_or_default();
|
let resource_dir = app.path().resource_dir().unwrap_or_default();
|
||||||
let contents_dir = resource_dir.parent().unwrap_or(&resource_dir).to_path_buf();
|
let contents_dir = resource_dir.parent().unwrap_or(&resource_dir).to_path_buf();
|
||||||
|
|
||||||
do_log(log, &format!("[resolve] macOS contents_dir = {:?}", contents_dir));
|
do_log(
|
||||||
|
log,
|
||||||
|
&format!("[resolve] macOS contents_dir = {:?}", contents_dir),
|
||||||
|
);
|
||||||
|
|
||||||
const NATIVE_NAMES: &[&str] = &[
|
const NATIVE_NAMES: &[&str] = &[
|
||||||
"suwayomi-server-aarch64-apple-darwin",
|
"suwayomi-server-aarch64-apple-darwin",
|
||||||
@@ -171,7 +209,7 @@ pub fn resolve_server_binary(
|
|||||||
];
|
];
|
||||||
|
|
||||||
let mut found_binary: Option<ServerInvocation> = None;
|
let mut found_binary: Option<ServerInvocation> = None;
|
||||||
let mut found_java: Option<(PathBuf, PathBuf)> = None;
|
let mut found_java: Option<(PathBuf, PathBuf)> = None;
|
||||||
|
|
||||||
'outer: for depth in 0u8..=8 {
|
'outer: for depth in 0u8..=8 {
|
||||||
let entries: Vec<PathBuf> = WalkDir::new(&contents_dir)
|
let entries: Vec<PathBuf> = WalkDir::new(&contents_dir)
|
||||||
@@ -184,15 +222,18 @@ pub fn resolve_server_binary(
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for dir in &entries {
|
for dir in &entries {
|
||||||
do_log(log, &format!("[resolve] scanning depth={} dir={:?}", depth, dir));
|
do_log(
|
||||||
|
log,
|
||||||
|
&format!("[resolve] scanning depth={} dir={:?}", depth, dir),
|
||||||
|
);
|
||||||
|
|
||||||
for name in NATIVE_NAMES {
|
for name in NATIVE_NAMES {
|
||||||
let p = dir.join(name);
|
let p = dir.join(name);
|
||||||
if p.exists() {
|
if p.exists() {
|
||||||
do_log(log, &format!("[resolve] found native binary: {:?}", p));
|
do_log(log, &format!("[resolve] found native binary: {:?}", p));
|
||||||
found_binary = Some(ServerInvocation {
|
found_binary = Some(ServerInvocation {
|
||||||
bin: p.to_string_lossy().into_owned(),
|
bin: p.to_string_lossy().into_owned(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
working_dir: Some(dir.clone()),
|
working_dir: Some(dir.clone()),
|
||||||
});
|
});
|
||||||
break 'outer;
|
break 'outer;
|
||||||
@@ -220,7 +261,10 @@ pub fn resolve_server_binary(
|
|||||||
for entry in rd.filter_map(|e| e.ok()) {
|
for entry in rd.filter_map(|e| e.ok()) {
|
||||||
if entry.file_name().to_string_lossy().ends_with(".jar") {
|
if entry.file_name().to_string_lossy().ends_with(".jar") {
|
||||||
let jar = entry.path();
|
let jar = entry.path();
|
||||||
do_log(log, &format!("[resolve] found jar in bin/: {:?}", jar));
|
do_log(
|
||||||
|
log,
|
||||||
|
&format!("[resolve] found jar in bin/: {:?}", jar),
|
||||||
|
);
|
||||||
found_java = Some((java_exe.clone(), jar));
|
found_java = Some((java_exe.clone(), jar));
|
||||||
break 'jar;
|
break 'jar;
|
||||||
}
|
}
|
||||||
@@ -228,7 +272,7 @@ pub fn resolve_server_binary(
|
|||||||
}
|
}
|
||||||
match search.parent() {
|
match search.parent() {
|
||||||
Some(p) => search = p,
|
Some(p) => search = p,
|
||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,7 +287,7 @@ pub fn resolve_server_binary(
|
|||||||
if let Some((java, jar)) = found_java {
|
if let Some((java, jar)) = found_java {
|
||||||
let working_dir = jar.parent().map(|p| p.to_path_buf());
|
let working_dir = jar.parent().map(|p| p.to_path_buf());
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: java.to_string_lossy().into_owned(),
|
bin: java.to_string_lossy().into_owned(),
|
||||||
args: vec!["-jar".to_string(), jar.to_string_lossy().into_owned()],
|
args: vec!["-jar".to_string(), jar.to_string_lossy().into_owned()],
|
||||||
working_dir,
|
working_dir,
|
||||||
});
|
});
|
||||||
@@ -254,12 +298,24 @@ pub fn resolve_server_binary(
|
|||||||
|
|
||||||
for name in &["suwayomi-server", "tachidesk-server"] {
|
for name in &["suwayomi-server", "tachidesk-server"] {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let found = std::process::Command::new("where").arg(name).output().map(|o| o.status.success()).unwrap_or(false);
|
let found = std::process::Command::new("where")
|
||||||
|
.arg(name)
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let found = std::process::Command::new("which").arg(name).output().map(|o| o.status.success()).unwrap_or(false);
|
let found = std::process::Command::new("which")
|
||||||
|
.arg(name)
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
return Ok(ServerInvocation { bin: name.to_string(), args: vec![], working_dir: None });
|
return Ok(ServerInvocation {
|
||||||
|
bin: name.to_string(),
|
||||||
|
args: vec![],
|
||||||
|
working_dir: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -6,7 +6,7 @@
|
|||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import { store, setActiveDownloads } from "@store/state.svelte";
|
import { store, setActiveDownloads } from "@store/state.svelte";
|
||||||
import { downloadStore } from "@features/downloads/store/downloadState.svelte";
|
import { downloadStore } from "@features/downloads/store/downloadState.svelte";
|
||||||
import { boot, startProbe, stopProbe, retryBoot, bypassBoot } from "@store/boot.svelte";
|
import { boot, initStore, startProbe, stopProbe, retryBoot, bypassBoot } from "@store/boot.svelte";
|
||||||
import { initRpc, setIdle, clearReading, destroyRpc } from "@store/discord";
|
import { initRpc, setIdle, clearReading, destroyRpc } from "@store/discord";
|
||||||
import { applyTheme } from "@core/theme";
|
import { applyTheme } from "@core/theme";
|
||||||
import { applyZoom, mountZoomKey, mountIdleDetection } from "@core/ui";
|
import { applyZoom, mountZoomKey, mountIdleDetection } from "@core/ui";
|
||||||
@@ -100,6 +100,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await initStore();
|
||||||
startProbe();
|
startProbe();
|
||||||
|
|
||||||
const unlistenDownload = await listen<{ chapterId: number; mangaId: number; progress: number }[]>(
|
const unlistenDownload = await listen<{ chapterId: number; mangaId: number; progress: number }[]>(
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { loadAllStores, persistSettings, persistLibrary, persistUpdates } from "./persist";
|
||||||
|
export type { PersistedData } from "./persist";
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { LazyStore } from "@tauri-apps/plugin-store";
|
||||||
|
|
||||||
|
const settingsStore = new LazyStore("settings.json", { autoSave: false });
|
||||||
|
const libraryStore = new LazyStore("library.json", { autoSave: false });
|
||||||
|
const updatesStore = new LazyStore("updates.json", { autoSave: false });
|
||||||
|
|
||||||
|
export interface PersistedData {
|
||||||
|
settings: any;
|
||||||
|
storeVersion: number | null;
|
||||||
|
history: any[];
|
||||||
|
bookmarks: any[];
|
||||||
|
markers: any[];
|
||||||
|
readLog: any[];
|
||||||
|
readingStats: any | null;
|
||||||
|
dailyReadCounts: Record<string, number>;
|
||||||
|
libraryUpdates: any[];
|
||||||
|
lastLibraryRefresh: number;
|
||||||
|
acknowledgedUpdateIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAllStores(): Promise<PersistedData> {
|
||||||
|
const migrated = await migrateFromLocalStorage();
|
||||||
|
if (migrated) return migrated;
|
||||||
|
|
||||||
|
const [sv, s, hist, bk, mk, rl, rs, dc, lu, llr, au] = await Promise.all([
|
||||||
|
settingsStore.get<number>("storeVersion"),
|
||||||
|
settingsStore.get<any>("settings"),
|
||||||
|
libraryStore.get<any[]>("history"),
|
||||||
|
libraryStore.get<any[]>("bookmarks"),
|
||||||
|
libraryStore.get<any[]>("markers"),
|
||||||
|
libraryStore.get<any[]>("readLog"),
|
||||||
|
libraryStore.get<any>("readingStats"),
|
||||||
|
libraryStore.get<Record<string, number>>("dailyReadCounts"),
|
||||||
|
updatesStore.get<any[]>("libraryUpdates"),
|
||||||
|
updatesStore.get<number>("lastLibraryRefresh"),
|
||||||
|
updatesStore.get<number[]>("acknowledgedUpdateIds"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
storeVersion: sv ?? null,
|
||||||
|
settings: s ?? null,
|
||||||
|
history: hist ?? [],
|
||||||
|
bookmarks: bk ?? [],
|
||||||
|
markers: mk ?? [],
|
||||||
|
readLog: rl ?? [],
|
||||||
|
readingStats: rs ?? null,
|
||||||
|
dailyReadCounts: dc ?? {},
|
||||||
|
libraryUpdates: lu ?? [],
|
||||||
|
lastLibraryRefresh: llr ?? 0,
|
||||||
|
acknowledgedUpdateIds: au ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateFromLocalStorage(): Promise<PersistedData | null> {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem("moku-store");
|
||||||
|
if (!raw) return null;
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
persistSettings({ settings: data.settings ?? null, storeVersion: data.storeVersion ?? 1 }),
|
||||||
|
persistLibrary({
|
||||||
|
history: data.history ?? [],
|
||||||
|
bookmarks: data.bookmarks ?? [],
|
||||||
|
markers: data.markers ?? [],
|
||||||
|
readLog: data.readLog ?? [],
|
||||||
|
readingStats: data.readingStats ?? null,
|
||||||
|
dailyReadCounts: data.dailyReadCounts ?? {},
|
||||||
|
}),
|
||||||
|
persistUpdates({
|
||||||
|
libraryUpdates: data.libraryUpdates ?? [],
|
||||||
|
lastLibraryRefresh: data.lastLibraryRefresh ?? 0,
|
||||||
|
acknowledgedUpdateIds: data.acknowledgedUpdateIds ?? [],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
localStorage.removeItem("moku-store");
|
||||||
|
|
||||||
|
return {
|
||||||
|
storeVersion: data.storeVersion ?? null,
|
||||||
|
settings: data.settings ?? null,
|
||||||
|
history: data.history ?? [],
|
||||||
|
bookmarks: data.bookmarks ?? [],
|
||||||
|
markers: data.markers ?? [],
|
||||||
|
readLog: data.readLog ?? [],
|
||||||
|
readingStats: data.readingStats ?? null,
|
||||||
|
dailyReadCounts: data.dailyReadCounts ?? {},
|
||||||
|
libraryUpdates: data.libraryUpdates ?? [],
|
||||||
|
lastLibraryRefresh: data.lastLibraryRefresh ?? 0,
|
||||||
|
acknowledgedUpdateIds: data.acknowledgedUpdateIds ?? [],
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function persistSettings(data: { settings: any; storeVersion: number }) {
|
||||||
|
await Promise.all([
|
||||||
|
settingsStore.set("settings", data.settings),
|
||||||
|
settingsStore.set("storeVersion", data.storeVersion),
|
||||||
|
]);
|
||||||
|
await settingsStore.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function persistLibrary(data: {
|
||||||
|
history: any[];
|
||||||
|
bookmarks: any[];
|
||||||
|
markers: any[];
|
||||||
|
readLog: any[];
|
||||||
|
readingStats: any;
|
||||||
|
dailyReadCounts: Record<string, number>;
|
||||||
|
}) {
|
||||||
|
await Promise.all([
|
||||||
|
libraryStore.set("history", data.history),
|
||||||
|
libraryStore.set("bookmarks", data.bookmarks),
|
||||||
|
libraryStore.set("markers", data.markers),
|
||||||
|
libraryStore.set("readLog", data.readLog),
|
||||||
|
libraryStore.set("readingStats", data.readingStats),
|
||||||
|
libraryStore.set("dailyReadCounts", data.dailyReadCounts),
|
||||||
|
]);
|
||||||
|
await libraryStore.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function persistUpdates(data: {
|
||||||
|
libraryUpdates: any[];
|
||||||
|
lastLibraryRefresh: number;
|
||||||
|
acknowledgedUpdateIds: number[];
|
||||||
|
}) {
|
||||||
|
await Promise.all([
|
||||||
|
updatesStore.set("libraryUpdates", data.libraryUpdates),
|
||||||
|
updatesStore.set("lastLibraryRefresh", data.lastLibraryRefresh),
|
||||||
|
updatesStore.set("acknowledgedUpdateIds", data.acknowledgedUpdateIds),
|
||||||
|
]);
|
||||||
|
await updatesStore.save();
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { store } from "@store/state.svelte";
|
import { store } from "@store/state.svelte";
|
||||||
import { probeServer, loginBasic } from "@core/auth";
|
import { probeServer, loginBasic } from "@core/auth";
|
||||||
import { trackingState } from "@features/tracking/store/trackingState.svelte";
|
import { trackingState } from "@features/tracking/store/trackingState.svelte";
|
||||||
|
import { loadAllStores } from "@core/persistence/persist";
|
||||||
|
|
||||||
const MAX_ATTEMPTS = 40;
|
const MAX_ATTEMPTS = 40;
|
||||||
|
|
||||||
@@ -18,6 +19,11 @@ export const boot = $state({
|
|||||||
|
|
||||||
let probeGeneration = 0;
|
let probeGeneration = 0;
|
||||||
|
|
||||||
|
export async function initStore() {
|
||||||
|
const saved = await loadAllStores();
|
||||||
|
store.hydrate(saved);
|
||||||
|
}
|
||||||
|
|
||||||
export function startProbe() {
|
export function startProbe() {
|
||||||
const gen = ++probeGeneration;
|
const gen = ++probeGeneration;
|
||||||
boot.failed = false;
|
boot.failed = false;
|
||||||
|
|||||||
+58
-44
@@ -8,6 +8,8 @@ import { DEFAULT_SETTINGS } from "../t
|
|||||||
import { DEFAULT_READING_STATS } from "../types/history";
|
import { DEFAULT_READING_STATS } from "../types/history";
|
||||||
import { notifications } from "./notifications.svelte";
|
import { notifications } from "./notifications.svelte";
|
||||||
import { app } from "./app.svelte";
|
import { app } from "./app.svelte";
|
||||||
|
import { persistSettings, persistLibrary, persistUpdates } from "../core/persistence/persist";
|
||||||
|
import type { PersistedData } from "../core/persistence/persist";
|
||||||
|
|
||||||
export type { NavPage } from "./app.svelte";
|
export type { NavPage } from "./app.svelte";
|
||||||
export type { Toast, ActiveDownload } from "./notifications.svelte";
|
export type { Toast, ActiveDownload } from "./notifications.svelte";
|
||||||
@@ -27,29 +29,6 @@ const STORE_VERSION = 3;
|
|||||||
const AVG_MIN_PER_CHAPTER = 5;
|
const AVG_MIN_PER_CHAPTER = 5;
|
||||||
const RESET_ON_UPGRADE: (keyof Settings)[] = ["serverBinary", "readerZoom", "uiZoom"];
|
const RESET_ON_UPGRADE: (keyof Settings)[] = ["serverBinary", "readerZoom", "uiZoom"];
|
||||||
|
|
||||||
function loadPersisted(): any {
|
|
||||||
try { const raw = localStorage.getItem("moku-store"); return raw ? JSON.parse(raw) : null; }
|
|
||||||
catch { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function persist(patch: Record<string, unknown>) {
|
|
||||||
try { localStorage.setItem("moku-store", JSON.stringify({ ...loadPersisted() ?? {}, ...patch })); }
|
|
||||||
catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saved = (() => {
|
|
||||||
const data = loadPersisted();
|
|
||||||
if (!data) return null;
|
|
||||||
if ((data.storeVersion ?? 1) < STORE_VERSION) {
|
|
||||||
const resetPatch: Partial<Settings> = {};
|
|
||||||
for (const key of RESET_ON_UPGRADE) (resetPatch as any)[key] = (DEFAULT_SETTINGS as any)[key];
|
|
||||||
const migrated = { ...data, storeVersion: STORE_VERSION, settings: { ...data.settings, ...resetPatch } };
|
|
||||||
try { localStorage.setItem("moku-store", JSON.stringify(migrated)); } catch {}
|
|
||||||
return migrated;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
})();
|
|
||||||
|
|
||||||
function mergeSettings(saved: any): Settings {
|
function mergeSettings(saved: any): Settings {
|
||||||
return {
|
return {
|
||||||
...DEFAULT_SETTINGS, ...saved?.settings,
|
...DEFAULT_SETTINGS, ...saved?.settings,
|
||||||
@@ -73,7 +52,7 @@ function mergeSettings(saved: any): Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Store {
|
class Store {
|
||||||
settings: Settings = $state(mergeSettings(saved));
|
settings: Settings = $state(mergeSettings(null));
|
||||||
activeManga: Manga | null = $state(null);
|
activeManga: Manga | null = $state(null);
|
||||||
previewManga: Manga | null = $state(null);
|
previewManga: Manga | null = $state(null);
|
||||||
activeChapter: Chapter | null = $state(null);
|
activeChapter: Chapter | null = $state(null);
|
||||||
@@ -84,19 +63,22 @@ class Store {
|
|||||||
categories: Category[] = $state([]);
|
categories: Category[] = $state([]);
|
||||||
activeSource: Source | null = $state(null);
|
activeSource: Source | null = $state(null);
|
||||||
libraryTagFilter: string[] = $state([]);
|
libraryTagFilter: string[] = $state([]);
|
||||||
history: HistoryEntry[] = $state(saved?.history ?? []);
|
history: HistoryEntry[] = $state([]);
|
||||||
bookmarks: BookmarkEntry[]= $state(saved?.bookmarks ?? []);
|
bookmarks: BookmarkEntry[]= $state([]);
|
||||||
markers: MarkerEntry[] = $state(saved?.markers ?? []);
|
markers: MarkerEntry[] = $state([]);
|
||||||
readLog: ReadLogEntry[] = $state(saved?.readLog ?? []);
|
readLog: ReadLogEntry[] = $state([]);
|
||||||
readingStats: ReadingStats = $state(saved?.readingStats ?? { ...DEFAULT_READING_STATS });
|
readingStats: ReadingStats = $state({ ...DEFAULT_READING_STATS });
|
||||||
dailyReadCounts: Record<string, number> = $state(saved?.dailyReadCounts ?? {});
|
dailyReadCounts: Record<string, number> = $state({});
|
||||||
searchCache: Map<string, any> = $state(new Map());
|
searchCache: Map<string, any> = $state(new Map());
|
||||||
searchLibraryIds: Set<number> = $state(new Set());
|
searchLibraryIds: Set<number> = $state(new Set());
|
||||||
searchSrcOffset: number = $state(0);
|
searchSrcOffset: number = $state(0);
|
||||||
readerSessionId: number = $state(0);
|
readerSessionId: number = $state(0);
|
||||||
libraryUpdates: LibraryUpdateEntry[] = $state(saved?.libraryUpdates ?? []);
|
libraryUpdates: LibraryUpdateEntry[] = $state([]);
|
||||||
lastLibraryRefresh: number = $state(saved?.lastLibraryRefresh ?? 0);
|
lastLibraryRefresh: number = $state(0);
|
||||||
acknowledgedUpdates: Set<number> = $state(new Set(saved?.acknowledgedUpdateIds ?? []));
|
acknowledgedUpdates: Set<number> = $state(new Set());
|
||||||
|
isFullscreen: boolean = $state(false);
|
||||||
|
|
||||||
|
#ready = false;
|
||||||
|
|
||||||
get toasts() { return notifications.toasts; }
|
get toasts() { return notifications.toasts; }
|
||||||
get activeDownloads() { return notifications.activeDownloads; }
|
get activeDownloads() { return notifications.activeDownloads; }
|
||||||
@@ -109,19 +91,51 @@ class Store {
|
|||||||
get genreFilter() { return app.genreFilter; }
|
get genreFilter() { return app.genreFilter; }
|
||||||
set genreFilter(v) { app.setGenreFilter(v); }
|
set genreFilter(v) { app.setGenreFilter(v); }
|
||||||
|
|
||||||
constructor() {
|
hydrate(saved: PersistedData) {
|
||||||
|
if (this.#ready) return;
|
||||||
|
|
||||||
|
if ((saved.storeVersion ?? 1) < STORE_VERSION && saved.settings) {
|
||||||
|
for (const key of RESET_ON_UPGRADE)
|
||||||
|
(saved.settings as any)[key] = (DEFAULT_SETTINGS as any)[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settings = mergeSettings(saved);
|
||||||
|
this.history = saved.history ?? [];
|
||||||
|
this.bookmarks = saved.bookmarks ?? [];
|
||||||
|
this.markers = saved.markers ?? [];
|
||||||
|
this.readLog = saved.readLog ?? [];
|
||||||
|
this.readingStats = saved.readingStats ?? { ...DEFAULT_READING_STATS };
|
||||||
|
this.dailyReadCounts = saved.dailyReadCounts ?? {};
|
||||||
|
this.libraryUpdates = saved.libraryUpdates ?? [];
|
||||||
|
this.lastLibraryRefresh = saved.lastLibraryRefresh ?? 0;
|
||||||
|
this.acknowledgedUpdates = new Set(saved.acknowledgedUpdateIds ?? []);
|
||||||
|
|
||||||
|
this.#ready = true;
|
||||||
|
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
persist({
|
const s = this.settings;
|
||||||
settings: this.settings, history: this.history,
|
if (!this.#ready) return;
|
||||||
bookmarks: this.bookmarks, markers: this.markers,
|
persistSettings({ settings: s, storeVersion: STORE_VERSION });
|
||||||
readLog: this.readLog, readingStats: this.readingStats,
|
});
|
||||||
dailyReadCounts: this.dailyReadCounts,
|
|
||||||
libraryUpdates: this.libraryUpdates,
|
$effect(() => {
|
||||||
lastLibraryRefresh: this.lastLibraryRefresh,
|
const h = this.history;
|
||||||
acknowledgedUpdateIds: [...this.acknowledgedUpdates],
|
const bk = this.bookmarks;
|
||||||
storeVersion: STORE_VERSION,
|
const mk = this.markers;
|
||||||
});
|
const rl = this.readLog;
|
||||||
|
const rs = this.readingStats;
|
||||||
|
const dc = this.dailyReadCounts;
|
||||||
|
if (!this.#ready) return;
|
||||||
|
persistLibrary({ history: h, bookmarks: bk, markers: mk, readLog: rl, readingStats: rs, dailyReadCounts: dc });
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const lu = this.libraryUpdates;
|
||||||
|
const llr = this.lastLibraryRefresh;
|
||||||
|
const au = this.acknowledgedUpdates;
|
||||||
|
if (!this.#ready) return;
|
||||||
|
persistUpdates({ libraryUpdates: lu, lastLibraryRefresh: llr, acknowledgedUpdateIds: [...au] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user