mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Feat: Manual Binary-Selection (CSS-WIP) (#91)
This commit is contained in:
@@ -3,7 +3,12 @@ use crate::ServerState;
|
||||
use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn spawn_server(binary: String, web_ui_enabled: bool, app: tauri::AppHandle) -> Result<(), SpawnError> {
|
||||
pub fn spawn_server(
|
||||
binary: String,
|
||||
binary_args: Option<String>,
|
||||
web_ui_enabled: bool,
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<(), SpawnError> {
|
||||
{
|
||||
let state = app.state::<ServerState>();
|
||||
if state.0.lock().unwrap().is_some() {
|
||||
@@ -20,9 +25,14 @@ pub fn spawn_server(binary: String, web_ui_enabled: bool, app: tauri::AppHandle)
|
||||
.open(&log_path)
|
||||
.ok();
|
||||
|
||||
let binary_args = binary_args.unwrap_or_default();
|
||||
|
||||
server::do_log(
|
||||
&mut log,
|
||||
&format!("[spawn_server] binary={:?} web_ui_enabled={} data_dir={:?}", binary, web_ui_enabled, data_dir),
|
||||
&format!(
|
||||
"[spawn_server] binary={:?} binary_args={:?} web_ui_enabled={} data_dir={:?}",
|
||||
binary, binary_args, web_ui_enabled, data_dir
|
||||
),
|
||||
);
|
||||
|
||||
server::conf::seed_server_conf(&data_dir, web_ui_enabled);
|
||||
@@ -33,6 +43,13 @@ pub fn spawn_server(binary: String, web_ui_enabled: bool, app: tauri::AppHandle)
|
||||
e
|
||||
})?;
|
||||
|
||||
if !binary_args.trim().is_empty() {
|
||||
let extra: Vec<String> = binary_args.split_whitespace().map(|s| s.to_string()).collect();
|
||||
let mut merged = extra;
|
||||
merged.extend(invocation.args);
|
||||
invocation.args = merged;
|
||||
}
|
||||
|
||||
if invocation.bin.ends_with("java") || invocation.bin.ends_with("java.exe") {
|
||||
let rootdir_flag = format!(
|
||||
"-Dsuwayomi.tachidesk.config.server.rootDir={}",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::server::resolve::strip_unc;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::path::PathBuf;
|
||||
use tauri::Manager;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_platform_ui_scale(window: tauri::Window) -> f64 {
|
||||
@@ -54,6 +52,34 @@ pub async fn pick_downloads_folder(app: tauri::AppHandle) -> Option<String> {
|
||||
.map(|p| p.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn pick_server_binary(app: tauri::AppHandle) -> Option<String> {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let dialog = app
|
||||
.dialog()
|
||||
.file()
|
||||
.set_title("Choose Server Binary")
|
||||
.add_filter("Executable", &["exe", "jar", "bat", "cmd"]);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let dialog = app
|
||||
.dialog()
|
||||
.file()
|
||||
.set_title("Choose Server Binary")
|
||||
.add_filter("Executable or JAR", &["jar", "command", "sh", "app"]);
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
let dialog = app
|
||||
.dialog()
|
||||
.file()
|
||||
.set_title("Choose Server Binary")
|
||||
.add_filter("Executable or JAR", &["jar", "sh"]);
|
||||
|
||||
dialog.blocking_pick_file().map(|p| p.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn exit_app(app: tauri::AppHandle) {
|
||||
app.exit(0);
|
||||
@@ -100,11 +126,6 @@ pub async fn clear_moku_cache(app: tauri::AppHandle) -> Result<(), String> {
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<Result<(), String>>();
|
||||
|
||||
// Note: We intentionally skip the WebView2 COM-level ClearBrowsingDataAll call here.
|
||||
// The webview2_com crate pulls in a different version of windows_core than Tauri's
|
||||
// own windows dependency, causing irreconcilable trait-impl conflicts at compile time.
|
||||
// The filesystem cache removal below (app_cache_dir) is sufficient for our purposes;
|
||||
// WebView2 will rebuild its cache on next launch from a clean directory.
|
||||
window
|
||||
.with_webview(move |_wv| {
|
||||
let _ = tx.send(Ok(()));
|
||||
|
||||
@@ -102,6 +102,7 @@ pub fn run() {
|
||||
commands::system::reset_suwayomi_data,
|
||||
commands::system::open_path,
|
||||
commands::system::pick_downloads_folder,
|
||||
commands::system::pick_server_binary,
|
||||
commands::backup::export_app_data,
|
||||
commands::backup::import_app_data,
|
||||
commands::backup::auto_backup_app_data,
|
||||
@@ -159,5 +160,5 @@ pub fn run() {
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running moku");
|
||||
.expect("error while running moku")
|
||||
}
|
||||
+76
-175
@@ -1,7 +1,6 @@
|
||||
use crate::server::do_log;
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
use walkdir::WalkDir;
|
||||
use tauri::Manager;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -48,22 +47,14 @@ pub fn strip_unc(path: PathBuf) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn find_java_in_bundle(bundle_dir: &PathBuf, log: &mut Option<std::fs::File>) -> Option<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let java = strip_unc(bundle_dir.join("jre").join("bin").join("java.exe"));
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let java = bundle_dir.join("jre").join("bin").join("java");
|
||||
fn java_bin_name() -> &'static str {
|
||||
if cfg!(target_os = "windows") { "java.exe" } else { "java" }
|
||||
}
|
||||
|
||||
do_log(
|
||||
log,
|
||||
&format!("[find_java] path: {:?} exists: {}", java, java.exists()),
|
||||
);
|
||||
if java.exists() {
|
||||
Some(java)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
fn find_java_in_bundle(bundle_dir: &PathBuf, log: &mut Option<std::fs::File>) -> Option<PathBuf> {
|
||||
let java = bundle_dir.join("jre").join("bin").join(java_bin_name());
|
||||
do_log(log, &format!("[find_java] {:?} exists={}", java, java.exists()));
|
||||
if java.exists() { Some(java) } else { None }
|
||||
}
|
||||
|
||||
fn data_root_args() -> Vec<String> {
|
||||
@@ -74,24 +65,34 @@ fn jar_data_root_flag() -> String {
|
||||
format!("-Dsuwayomi.server.rootDir={}", suwayomi_data_dir().to_string_lossy())
|
||||
}
|
||||
|
||||
fn jar_invocation(java: PathBuf, jar: PathBuf, working_dir: PathBuf) -> ServerInvocation {
|
||||
ServerInvocation {
|
||||
bin: java.to_string_lossy().into_owned(),
|
||||
args: vec![
|
||||
jar_data_root_flag(),
|
||||
"-jar".to_string(),
|
||||
jar.to_string_lossy().into_owned(),
|
||||
],
|
||||
working_dir: Some(working_dir),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_server_binary(
|
||||
binary: &str,
|
||||
app: &tauri::AppHandle,
|
||||
log: &mut Option<std::fs::File>,
|
||||
) -> Result<ServerInvocation, SpawnError> {
|
||||
do_log(log, &format!("[resolve] binary = {:?}", binary));
|
||||
do_log(log, &format!("[resolve] binary={:?}", binary));
|
||||
|
||||
if !binary.trim().is_empty() {
|
||||
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() {
|
||||
let working_dir = path.parent().map(|p| p.to_path_buf());
|
||||
return Ok(ServerInvocation {
|
||||
bin: path.to_string_lossy().into_owned(),
|
||||
args: data_root_args(),
|
||||
working_dir: path.parent().map(|p| p.to_path_buf()),
|
||||
working_dir,
|
||||
});
|
||||
}
|
||||
do_log(log, "[resolve] user path not found, falling through");
|
||||
@@ -101,10 +102,7 @@ pub fn resolve_server_binary(
|
||||
if let Some(bin_dir) = exe.parent() {
|
||||
for name in &["tachidesk-server", "suwayomi-launcher"] {
|
||||
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() {
|
||||
return Ok(ServerInvocation {
|
||||
bin: p.to_string_lossy().into_owned(),
|
||||
@@ -116,54 +114,31 @@ pub fn resolve_server_binary(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let resource_dir = {
|
||||
let raw = app.path().resource_dir().unwrap_or_default();
|
||||
let stripped = strip_unc(raw);
|
||||
do_log(log, &format!("[resolve] resource_dir = {:?}", stripped));
|
||||
stripped
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let resource_dir = {
|
||||
let raw = app.path().resource_dir().unwrap_or_default();
|
||||
let stripped = strip_unc(raw);
|
||||
do_log(log, &format!("[resolve] resource_dir={:?}", stripped));
|
||||
stripped
|
||||
};
|
||||
|
||||
let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle");
|
||||
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(
|
||||
log,
|
||||
&format!("[resolve] jar={:?} exists={}", jar, jar.exists()),
|
||||
);
|
||||
do_log(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) {
|
||||
Some(java) if jar.exists() => {
|
||||
do_log(log, "[resolve] using bundled JRE");
|
||||
return Ok(ServerInvocation {
|
||||
bin: java.to_string_lossy().into_owned(),
|
||||
args: vec![jar_data_root_flag(), "-jar".to_string(), jar.to_string_lossy().into_owned()],
|
||||
working_dir: Some(bundle_dir),
|
||||
});
|
||||
if let Some(java) = find_java_in_bundle(&bundle_dir, log) {
|
||||
if jar.exists() {
|
||||
do_log(log, "[resolve] using bundled JRE + jar");
|
||||
return Ok(jar_invocation(java, jar, bundle_dir));
|
||||
}
|
||||
_ => 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);
|
||||
do_log(
|
||||
log,
|
||||
&format!("[resolve] sidecar: {:?} exists={}", p, p.exists()),
|
||||
);
|
||||
do_log(log, &format!("[resolve] sidecar={:?} exists={}", p, p.exists()));
|
||||
if p.exists() {
|
||||
return Ok(ServerInvocation {
|
||||
bin: p.to_string_lossy().into_owned(),
|
||||
@@ -174,26 +149,16 @@ pub fn resolve_server_binary(
|
||||
}
|
||||
|
||||
if let Some(java) = find_java_in_bundle(&resource_dir, log) {
|
||||
let jar = std::fs::read_dir(&resource_dir).ok().and_then(|mut rd| {
|
||||
rd.find(|e| {
|
||||
e.as_ref()
|
||||
.map(|e| e.file_name().to_string_lossy().ends_with(".jar"))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.and_then(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
});
|
||||
|
||||
if let Some(jar_path) = jar {
|
||||
do_log(
|
||||
log,
|
||||
&format!("[resolve] generic JRE java={:?} jar={:?}", java, jar_path),
|
||||
);
|
||||
return Ok(ServerInvocation {
|
||||
bin: java.to_string_lossy().into_owned(),
|
||||
args: vec![jar_data_root_flag(), "-jar".to_string(), jar_path.to_string_lossy().into_owned()],
|
||||
working_dir: Some(resource_dir),
|
||||
let jar = std::fs::read_dir(&resource_dir)
|
||||
.ok()
|
||||
.and_then(|mut rd| {
|
||||
rd.find(|e| e.as_ref().map(|e| e.file_name().to_string_lossy().ends_with(".jar")).unwrap_or(false))
|
||||
.and_then(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
});
|
||||
if let Some(jar_path) = jar {
|
||||
do_log(log, &format!("[resolve] generic JRE java={:?} jar={:?}", java, jar_path));
|
||||
return Ok(jar_invocation(java, jar_path, resource_dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,108 +166,43 @@ pub fn resolve_server_binary(
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let resource_dir = app.path().resource_dir().unwrap_or_default();
|
||||
let contents_dir = resource_dir.parent().unwrap_or(&resource_dir).to_path_buf();
|
||||
let bundle_dir = resource_dir.join("suwayomi-bundle");
|
||||
|
||||
do_log(
|
||||
log,
|
||||
&format!("[resolve] macOS contents_dir = {:?}", contents_dir),
|
||||
);
|
||||
do_log(log, &format!("[resolve] macOS resource_dir={:?}", resource_dir));
|
||||
do_log(log, &format!("[resolve] macOS bundle_dir={:?} exists={}", bundle_dir, bundle_dir.exists()));
|
||||
|
||||
const NATIVE_NAMES: &[&str] = &[
|
||||
"suwayomi-server-aarch64-apple-darwin",
|
||||
"suwayomi-server-x86_64-apple-darwin",
|
||||
"suwayomi-server",
|
||||
"suwayomi-launcher",
|
||||
"suwayomi-launcher.sh",
|
||||
"tachidesk-server",
|
||||
];
|
||||
let java = bundle_dir.join("jre").join("bin").join("java");
|
||||
let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar");
|
||||
let launcher_sh = bundle_dir.join("Suwayomi Launcher.command");
|
||||
let launcher_jar = bundle_dir.join("Suwayomi-Launcher.jar");
|
||||
|
||||
let mut found_binary: Option<ServerInvocation> = None;
|
||||
let mut found_java: Option<(PathBuf, PathBuf)> = None;
|
||||
do_log(log, &format!("[resolve] java={:?} exists={}", java, java.exists()));
|
||||
do_log(log, &format!("[resolve] jar={:?} exists={}", jar, jar.exists()));
|
||||
do_log(log, &format!("[resolve] launcher.command={:?} exists={}", launcher_sh, launcher_sh.exists()));
|
||||
do_log(log, &format!("[resolve] launcher.jar={:?} exists={}", launcher_jar, launcher_jar.exists()));
|
||||
|
||||
'outer: for depth in 0u8..=8 {
|
||||
let entries: Vec<PathBuf> = WalkDir::new(&contents_dir)
|
||||
.min_depth(depth as usize)
|
||||
.max_depth(depth as usize)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().is_dir())
|
||||
.map(|e| e.into_path())
|
||||
.collect();
|
||||
|
||||
for dir in &entries {
|
||||
do_log(
|
||||
log,
|
||||
&format!("[resolve] scanning depth={} dir={:?}", depth, dir),
|
||||
);
|
||||
|
||||
for name in NATIVE_NAMES {
|
||||
let p = dir.join(name);
|
||||
if p.exists() {
|
||||
do_log(log, &format!("[resolve] found native binary: {:?}", p));
|
||||
found_binary = Some(ServerInvocation {
|
||||
bin: p.to_string_lossy().into_owned(),
|
||||
args: data_root_args(),
|
||||
working_dir: Some(dir.clone()),
|
||||
});
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
if found_java.is_none() {
|
||||
let java_exe = dir.join("bin").join("java");
|
||||
if java_exe.exists() {
|
||||
do_log(log, &format!("[resolve] found java: {:?}", java_exe));
|
||||
let mut search = dir.as_path();
|
||||
'jar: for _ in 0..5 {
|
||||
if let Ok(rd) = std::fs::read_dir(search) {
|
||||
for entry in rd.filter_map(|e| e.ok()) {
|
||||
if entry.file_name().to_string_lossy().ends_with(".jar") {
|
||||
let jar = entry.path();
|
||||
do_log(log, &format!("[resolve] found jar: {:?}", jar));
|
||||
found_java = Some((java_exe.clone(), jar));
|
||||
break 'jar;
|
||||
}
|
||||
}
|
||||
}
|
||||
let bin_sibling = search.join("bin");
|
||||
if let Ok(rd) = std::fs::read_dir(&bin_sibling) {
|
||||
for entry in rd.filter_map(|e| e.ok()) {
|
||||
if entry.file_name().to_string_lossy().ends_with(".jar") {
|
||||
let jar = entry.path();
|
||||
do_log(
|
||||
log,
|
||||
&format!("[resolve] found jar in bin/: {:?}", jar),
|
||||
);
|
||||
found_java = Some((java_exe.clone(), jar));
|
||||
break 'jar;
|
||||
}
|
||||
}
|
||||
}
|
||||
match search.parent() {
|
||||
Some(p) => search = p,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if java.exists() && jar.exists() {
|
||||
do_log(log, "[resolve] macOS using bundled JRE + Suwayomi-Server.jar");
|
||||
return Ok(jar_invocation(java, jar, bundle_dir));
|
||||
}
|
||||
|
||||
if let Some(inv) = found_binary {
|
||||
return Ok(inv);
|
||||
}
|
||||
|
||||
if let Some((java, jar)) = found_java {
|
||||
let working_dir = jar.parent().map(|p| p.to_path_buf());
|
||||
if launcher_sh.exists() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(&launcher_sh, std::fs::Permissions::from_mode(0o755));
|
||||
do_log(log, "[resolve] macOS using Suwayomi Launcher.command");
|
||||
return Ok(ServerInvocation {
|
||||
bin: java.to_string_lossy().into_owned(),
|
||||
args: vec![jar_data_root_flag(), "-jar".to_string(), jar.to_string_lossy().into_owned()],
|
||||
working_dir,
|
||||
bin: launcher_sh.to_string_lossy().into_owned(),
|
||||
args: vec![],
|
||||
working_dir: Some(bundle_dir),
|
||||
});
|
||||
}
|
||||
|
||||
do_log(log, "[resolve] macOS scan found nothing in bundle");
|
||||
if java.exists() && launcher_jar.exists() {
|
||||
do_log(log, "[resolve] macOS using bundled JRE + Suwayomi-Launcher.jar");
|
||||
return Ok(jar_invocation(java, launcher_jar, bundle_dir));
|
||||
}
|
||||
|
||||
do_log(log, "[resolve] macOS bundle not found, falling through to PATH");
|
||||
}
|
||||
|
||||
for name in &["suwayomi-server", "tachidesk-server"] {
|
||||
@@ -314,6 +214,7 @@ pub fn resolve_server_binary(
|
||||
.filter(|o| o.status.success())
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.and_then(|s| s.lines().next().map(|l| l.trim().to_string()));
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let resolved = std::process::Command::new("which")
|
||||
.arg(name)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { store, updateSettings } from "@store/state.svelte";
|
||||
import { selectPortal } from "@core/actions/selectPortal";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
interface Props {
|
||||
selectOpen: string | null;
|
||||
@@ -12,6 +13,12 @@
|
||||
let { selectOpen, closingSelect, toggleSelect, anims }: Props = $props();
|
||||
|
||||
let triggerIdleTimeout = $state<HTMLButtonElement>(null!);
|
||||
let serverAdvancedOpen = $state(false);
|
||||
|
||||
async function pickServerBinary() {
|
||||
const picked = await invoke<string | null>("pick_server_binary");
|
||||
if (picked) updateSettings({ serverBinary: picked });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="s-panel">
|
||||
@@ -43,18 +50,70 @@
|
||||
<div class="s-section">
|
||||
<p class="s-section-title">Server</p>
|
||||
<div class="s-section-body">
|
||||
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><span class="s-label">Server URL</span><span class="s-desc">Base URL of your Suwayomi instance</span></div>
|
||||
<input class="s-input" value={store.settings.serverUrl ?? "http://localhost:4567"} oninput={(e) => updateSettings({ serverUrl: e.currentTarget.value })} placeholder="http://localhost:4567" spellcheck="false" />
|
||||
<div class="s-row-info">
|
||||
<span class="s-label">Server URL</span>
|
||||
<span class="s-desc">Base URL of your Suwayomi instance</span>
|
||||
</div>
|
||||
<div class="srv-url-group">
|
||||
<input class="s-input" value={store.settings.serverUrl ?? "http://localhost:4567"}
|
||||
oninput={(e) => updateSettings({ serverUrl: e.currentTarget.value })}
|
||||
placeholder="http://localhost:4567" spellcheck="false" />
|
||||
<button
|
||||
class="srv-adv-btn"
|
||||
class:open={serverAdvancedOpen}
|
||||
onclick={() => serverAdvancedOpen = !serverAdvancedOpen}
|
||||
title="Server launch options"
|
||||
aria-expanded={serverAdvancedOpen}
|
||||
>
|
||||
<svg width="10" height="6" viewBox="0 0 10 6" fill="none">
|
||||
<path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="s-row">
|
||||
<div class="s-row-info"><span class="s-label">Auto-start server</span><span class="s-desc">Launch tachidesk-server when Moku opens</span></div>
|
||||
<button role="switch" aria-checked={store.settings.autoStartServer} aria-label="Auto-start server" class="s-toggle" class:on={store.settings.autoStartServer} onclick={() => updateSettings({ autoStartServer: !store.settings.autoStartServer })}><span class="s-toggle-thumb"></span></button>
|
||||
<button role="switch" aria-checked={store.settings.autoStartServer} aria-label="Auto-start server"
|
||||
class="s-toggle" class:on={store.settings.autoStartServer}
|
||||
onclick={() => updateSettings({ autoStartServer: !store.settings.autoStartServer })}>
|
||||
<span class="s-toggle-thumb"></span>
|
||||
</button>
|
||||
</label>
|
||||
|
||||
<label class="s-row">
|
||||
<div class="s-row-info"><span class="s-label">Suwayomi Web UI</span><span class="s-desc">Enable the built-in Suwayomi web interface alongside Moku</span></div>
|
||||
<button role="switch" aria-checked={store.settings.suwayomiWebUI ?? false} aria-label="Suwayomi Web UI" class="s-toggle" class:on={store.settings.suwayomiWebUI ?? false} onclick={() => updateSettings({ suwayomiWebUI: !(store.settings.suwayomiWebUI ?? false) })}><span class="s-toggle-thumb"></span></button>
|
||||
<button role="switch" aria-checked={store.settings.suwayomiWebUI ?? false} aria-label="Suwayomi Web UI"
|
||||
class="s-toggle" class:on={store.settings.suwayomiWebUI ?? false}
|
||||
onclick={() => updateSettings({ suwayomiWebUI: !(store.settings.suwayomiWebUI ?? false) })}>
|
||||
<span class="s-toggle-thumb"></span>
|
||||
</button>
|
||||
</label>
|
||||
|
||||
{#if serverAdvancedOpen}
|
||||
<div class="srv-adv-panel">
|
||||
<div class="srv-adv-row">
|
||||
<div class="s-row-info">
|
||||
<span class="s-label">Server binary</span>
|
||||
<span class="s-desc">Path to server executable — leave blank to use bundled</span>
|
||||
</div>
|
||||
<div class="srv-file-group">
|
||||
<input class="s-input srv-path-input" value={store.settings.serverBinary ?? ""}
|
||||
oninput={(e) => updateSettings({ serverBinary: e.currentTarget.value })}
|
||||
placeholder="auto-detect" spellcheck="false" />
|
||||
<button class="srv-file-btn" onclick={pickServerBinary} title="Browse">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M1.5 4.5h11v7a1 1 0 01-1 1h-9a1 1 0 01-1-1v-7z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
|
||||
<path d="M1.5 4.5l1.8-2.5h3.4l1.3 2.5" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +146,7 @@
|
||||
<div class="s-row-info"><span class="s-label">Close button behavior</span><span class="s-desc">What happens when you click the X button</span></div>
|
||||
<div class="s-seg">
|
||||
{#each [["ask","Ask"],["tray","Tray"],["quit","Quit"]] as [v, l]}
|
||||
<button class="s-seg-btn" class:active={( store.settings.closeAction ?? "ask") === v} onclick={() => updateSettings({ closeAction: v as "ask" | "tray" | "quit" })}>{l}</button>
|
||||
<button class="s-seg-btn" class:active={(store.settings.closeAction ?? "ask") === v} onclick={() => updateSettings({ closeAction: v as "ask" | "tray" | "quit" })}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,4 +196,70 @@
|
||||
.s-seg-btn:not(:last-child) { border-right: 1px solid var(--border-strong); }
|
||||
.s-seg-btn.active { background: var(--accent-muted); color: var(--accent-fg); }
|
||||
.s-seg-btn:not(.active):hover { background: var(--bg-raised); color: var(--text-secondary); }
|
||||
|
||||
.srv-url-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.srv-adv-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.srv-adv-btn:hover { background: var(--bg-overlay); color: var(--text-muted); border-color: var(--border-strong); }
|
||||
.srv-adv-btn.open { background: var(--bg-overlay); color: var(--text-secondary); border-color: var(--border-strong); }
|
||||
.srv-adv-btn svg { transition: transform var(--t-base); }
|
||||
.srv-adv-btn.open svg { transform: rotate(180deg); }
|
||||
|
||||
.srv-adv-panel {
|
||||
border-top: 1px solid var(--border-dim);
|
||||
background: var(--bg-base);
|
||||
}
|
||||
|
||||
.srv-adv-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px var(--sp-4);
|
||||
gap: var(--sp-4);
|
||||
}
|
||||
|
||||
.srv-file-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.srv-path-input {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.srv-file-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.srv-file-btn:hover { background: var(--bg-overlay); color: var(--text-muted); border-color: var(--border-strong); }
|
||||
</style>
|
||||
@@ -92,7 +92,7 @@ export interface Settings {
|
||||
discordRpc: boolean;
|
||||
chapterSortDir: ChapterSortDir; chapterSortMode: ChapterSortMode; chapterPageSize: number;
|
||||
uiZoom: number; compactSidebar: boolean; gpuAcceleration: boolean;
|
||||
serverUrl: string; serverBinary: string; autoStartServer: boolean; suwayomiWebUI: boolean;
|
||||
serverUrl: string; serverBinary: string; serverBinaryArgs: string; autoStartServer: boolean; suwayomiWebUI: boolean;
|
||||
preferredExtensionLang: string; keybinds: Keybinds;
|
||||
idleTimeoutMin?: number; splashCards?: boolean;
|
||||
storageLimitGb: number | null; markReadOnNext: boolean; readerDebounceMs: number;
|
||||
@@ -143,7 +143,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
discordRpc: false,
|
||||
chapterSortDir: "desc", chapterSortMode: "source", chapterPageSize: 25,
|
||||
uiZoom: 1.0, compactSidebar: false, gpuAcceleration: true,
|
||||
serverUrl: "http://localhost:4567", serverBinary: "", autoStartServer: true, suwayomiWebUI: false,
|
||||
serverUrl: "http://localhost:4567", serverBinary: "", serverBinaryArgs: "", autoStartServer: true, suwayomiWebUI: false,
|
||||
preferredExtensionLang: "en", keybinds: DEFAULT_KEYBINDS,
|
||||
idleTimeoutMin: 5, splashCards: true, storageLimitGb: null,
|
||||
markReadOnNext: true, readerDebounceMs: 120, autoBookmark: true,
|
||||
|
||||
Reference in New Issue
Block a user