Feat: Revamped Logo, QOL Home-Screen Additions, Scaling Logic Revamp
@@ -27,9 +27,9 @@
|
|||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
<release version="0.1.0" date="2025-01-01">
|
<release version="0.4.0" date="2025-03-22">
|
||||||
<description>
|
<description>
|
||||||
<p>Initial release.</p>
|
<p>Svelte rewrite with improved UI, bundled server, and cross-platform fixes.</p>
|
||||||
</description>
|
</description>
|
||||||
</release>
|
</release>
|
||||||
</releases>
|
</releases>
|
||||||
|
|||||||
@@ -8,6 +8,23 @@
|
|||||||
"shell:allow-open",
|
"shell:allow-open",
|
||||||
"shell:allow-kill",
|
"shell:allow-kill",
|
||||||
"shell:allow-spawn",
|
"shell:allow-spawn",
|
||||||
"shell:allow-execute"
|
"shell:allow-execute",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-unminimize",
|
||||||
|
"core:window:allow-maximize",
|
||||||
|
"core:window:allow-unmaximize",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-set-focus",
|
||||||
|
"core:window:allow-set-fullscreen",
|
||||||
|
"core:window:allow-is-fullscreen",
|
||||||
|
"core:window:allow-is-maximized",
|
||||||
|
"core:window:allow-is-minimized",
|
||||||
|
"core:window:allow-inner-size",
|
||||||
|
"core:window:allow-outer-size",
|
||||||
|
"core:window:allow-inner-position",
|
||||||
|
"core:window:allow-outer-position",
|
||||||
|
"core:window:allow-scale-factor"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 803 B After Width: | Height: | Size: 740 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 538 B After Width: | Height: | Size: 706 B |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 842 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 16 KiB |
@@ -83,8 +83,13 @@ fn get_storage_info(downloads_path: String) -> Result<StorageInfo, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_scale_factor(window: tauri::Window) -> f64 {
|
fn get_platform_ui_scale() -> f64 {
|
||||||
window.scale_factor().unwrap_or(1.0)
|
#[cfg(target_os = "windows")]
|
||||||
|
return 1.0;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
return 1.0;
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
|
return 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kill_tachidesk(app: &tauri::AppHandle) {
|
fn kill_tachidesk(app: &tauri::AppHandle) {
|
||||||
@@ -249,51 +254,34 @@ fn resolve_server_binary(
|
|||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
{
|
{
|
||||||
// Tauri 2 resource bundling behaviour depends on the config:
|
let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle");
|
||||||
// - Structured layout: resource_dir/binaries/suwayomi-bundle/{bin,jre}/...
|
let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar");
|
||||||
// - Flat layout: resource_dir/{java.exe,Suwayomi-Server.jar,...}
|
|
||||||
// We try both so the binary works regardless of which layout the installer produced.
|
|
||||||
let search_candidates: &[(&str, &str)] = &[
|
|
||||||
// Structured — what the config intends
|
|
||||||
("binaries/suwayomi-bundle", "binaries/suwayomi-bundle/bin/Suwayomi-Server.jar"),
|
|
||||||
// Flat — what Tauri 2 actually produces with glob resources
|
|
||||||
("", "Suwayomi-Server.jar"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (bundle_rel, jar_rel) in search_candidates {
|
do_log(log, &format!("[resolve] bundle_dir = {:?}", bundle_dir));
|
||||||
let bundle_dir = if bundle_rel.is_empty() {
|
do_log(log, &format!("[resolve] bundle_dir exists: {}", bundle_dir.exists()));
|
||||||
resource_dir.clone()
|
do_log(log, &format!("[resolve] jar = {:?}", jar));
|
||||||
} else {
|
do_log(log, &format!("[resolve] jar exists: {}", jar.exists()));
|
||||||
resource_dir.join(bundle_rel)
|
|
||||||
};
|
|
||||||
let jar = resource_dir.join(jar_rel);
|
|
||||||
|
|
||||||
do_log(log, &format!("[resolve] trying bundle_dir = {:?}", bundle_dir));
|
match find_java_in_bundle(&bundle_dir, log) {
|
||||||
do_log(log, &format!("[resolve] bundle_dir exists: {}", bundle_dir.exists()));
|
Some(java) => {
|
||||||
do_log(log, &format!("[resolve] jar = {:?}", jar));
|
do_log(log, &format!("[resolve] java found: {:?}", java));
|
||||||
do_log(log, &format!("[resolve] jar exists: {}", jar.exists()));
|
if jar.exists() {
|
||||||
|
do_log(log, "[resolve] both java and jar found — using bundled JRE");
|
||||||
match find_java_in_bundle(&bundle_dir, log) {
|
return Ok(ServerInvocation {
|
||||||
Some(java) => {
|
bin: java.to_string_lossy().into_owned(),
|
||||||
do_log(log, &format!("[resolve] java found: {:?}", java));
|
args: vec![
|
||||||
if jar.exists() {
|
"-jar".to_string(),
|
||||||
do_log(log, "[resolve] both java and jar found — using bundled JRE");
|
jar.to_string_lossy().into_owned(),
|
||||||
return Ok(ServerInvocation {
|
],
|
||||||
bin: java.to_string_lossy().into_owned(),
|
working_dir: Some(bundle_dir),
|
||||||
args: vec![
|
});
|
||||||
"-jar".to_string(),
|
} else {
|
||||||
jar.to_string_lossy().into_owned(),
|
do_log(log, "[resolve] java found but jar MISSING — skipping bundled path");
|
||||||
],
|
|
||||||
working_dir: Some(bundle_dir),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
do_log(log, "[resolve] java found but jar MISSING — trying next candidate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
do_log(log, "[resolve] java NOT found — trying next candidate");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
do_log(log, "[resolve] java NOT found in bundle — skipping bundled path");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +424,7 @@ pub fn run() {
|
|||||||
get_storage_info,
|
get_storage_info,
|
||||||
spawn_server,
|
spawn_server,
|
||||||
kill_server,
|
kill_server,
|
||||||
get_scale_factor,
|
get_platform_ui_scale,
|
||||||
])
|
])
|
||||||
.setup(|_app| Ok(()))
|
.setup(|_app| Ok(()))
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
"minHeight": 600,
|
"minHeight": 600,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"decorations": false
|
"decorations": false,
|
||||||
|
"center": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
@@ -26,7 +27,9 @@
|
|||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["nsis"],
|
"targets": [
|
||||||
|
"nsis"
|
||||||
|
],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import SplashScreen from "./components/layout/SplashScreen.svelte";
|
import SplashScreen from "./components/layout/SplashScreen.svelte";
|
||||||
import MangaPreview from "./components/shared/MangaPreview.svelte";
|
import MangaPreview from "./components/shared/MangaPreview.svelte";
|
||||||
|
|
||||||
const MAX_ATTEMPTS = 30;
|
const MAX_ATTEMPTS = 60;
|
||||||
|
|
||||||
let serverProbeOk = $state(!store.settings.autoStartServer);
|
let serverProbeOk = $state(!store.settings.autoStartServer);
|
||||||
let appReady = $state(!store.settings.autoStartServer);
|
let appReady = $state(!store.settings.autoStartServer);
|
||||||
@@ -22,6 +22,14 @@
|
|||||||
let notConfigured = $state(false);
|
let notConfigured = $state(false);
|
||||||
let idle = $state(false);
|
let idle = $state(false);
|
||||||
let devSplash = $state(false);
|
let devSplash = $state(false);
|
||||||
|
let platformScale = $state(1);
|
||||||
|
|
||||||
|
function applyZoom() {
|
||||||
|
const normalized = store.settings.uiScale * platformScale;
|
||||||
|
document.documentElement.style.zoom = `${normalized}%`;
|
||||||
|
document.documentElement.style.setProperty("--ui-scale", String(normalized));
|
||||||
|
document.documentElement.style.setProperty("--visual-vh", `${window.innerHeight / (normalized / 100)}px`);
|
||||||
|
}
|
||||||
|
|
||||||
let prevQueue: DownloadQueueItem[] = [];
|
let prevQueue: DownloadQueueItem[] = [];
|
||||||
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@@ -66,10 +74,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const scale = store.settings.uiScale * 1.5;
|
// Re-runs whenever uiScale or platformScale changes.
|
||||||
document.documentElement.style.zoom = `${scale}%`;
|
store.settings.uiScale; platformScale;
|
||||||
document.documentElement.style.setProperty("--ui-scale", String(scale));
|
applyZoom();
|
||||||
document.documentElement.style.setProperty("--visual-vh", `${window.innerHeight / (scale / 100)}px`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -85,61 +92,48 @@
|
|||||||
return () => clearInterval(pollInterval);
|
return () => clearInterval(pollInterval);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Probe the server in a loop until it responds or we hit MAX_ATTEMPTS.
|
|
||||||
// Returns a cleanup function that cancels any pending probe.
|
|
||||||
function startProbe(): () => void {
|
|
||||||
let cancelled = false, tries = 0;
|
|
||||||
|
|
||||||
async function probe() {
|
|
||||||
if (cancelled) return;
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${store.settings.serverUrl}/api/graphql`, {
|
|
||||||
method: "POST", headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ query: "{ __typename }" }),
|
|
||||||
signal: AbortSignal.timeout(2000),
|
|
||||||
});
|
|
||||||
if (res.ok && !cancelled) { serverProbeOk = true; return; }
|
|
||||||
} catch {}
|
|
||||||
if (tries >= MAX_ATTEMPTS && !cancelled) { failed = true; return; }
|
|
||||||
if (!cancelled) setTimeout(probe, 800);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give the server a moment to start binding its port before the first probe.
|
|
||||||
setTimeout(probe, 1200);
|
|
||||||
return () => { cancelled = true; };
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
document.addEventListener("contextmenu", e => e.preventDefault());
|
document.addEventListener("contextmenu", e => e.preventDefault());
|
||||||
(window as any).__mokuShowSplash = () => devSplash = true;
|
(window as any).__mokuShowSplash = () => devSplash = true;
|
||||||
|
|
||||||
let cancelProbe = () => {};
|
// Fetch the platform scale factor then immediately re-apply zoom.
|
||||||
|
platformScale = await invoke<number>("get_platform_ui_scale").catch(() => 1);
|
||||||
|
applyZoom();
|
||||||
|
|
||||||
if (store.settings.autoStartServer) {
|
if (store.settings.autoStartServer) {
|
||||||
try {
|
invoke<void>("spawn_server", { binary: store.settings.serverBinary }).catch((err: any) => {
|
||||||
await invoke<void>("spawn_server", { binary: store.settings.serverBinary ?? "" });
|
|
||||||
// spawn_server succeeded — JRE found and process started. Begin probing.
|
|
||||||
cancelProbe = startProbe();
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err?.kind === "NotConfigured") {
|
if (err?.kind === "NotConfigured") {
|
||||||
notConfigured = true;
|
notConfigured = true;
|
||||||
} else {
|
} else {
|
||||||
// SpawnFailed — process couldn't be launched (permissions, bad path, etc.)
|
console.warn("Could not start server:", err);
|
||||||
console.error("spawn_server failed:", err);
|
|
||||||
failed = true;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverProbeOk) {
|
||||||
|
let cancelled = false, tries = 0;
|
||||||
|
async function probe() {
|
||||||
|
if (cancelled) return;
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${store.settings.serverUrl}/api/graphql`, {
|
||||||
|
method: "POST", headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ query: "{ __typename }" }),
|
||||||
|
signal: AbortSignal.timeout(2000),
|
||||||
|
});
|
||||||
|
if (res.ok && !cancelled) { serverProbeOk = true; return; }
|
||||||
|
} catch {}
|
||||||
|
if (tries >= MAX_ATTEMPTS && !cancelled) { failed = true; return; }
|
||||||
|
if (!cancelled) setTimeout(probe, 500);
|
||||||
}
|
}
|
||||||
} else {
|
setTimeout(probe, 800);
|
||||||
// autoStartServer is off — user manages the server themselves, just probe.
|
|
||||||
cancelProbe = startProbe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type P = { chapterId: number; mangaId: number; progress: number }[];
|
type P = { chapterId: number; mangaId: number; progress: number }[];
|
||||||
unlistenDownload = await listen<P>("download-progress", e => { setActiveDownloads(e.payload); });
|
unlistenDownload = await listen<P>("download-progress", e => { setActiveDownloads(e.payload); });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelProbe();
|
cancelled = true;
|
||||||
if (store.settings.autoStartServer) invoke("kill_server").catch(() => {});
|
if (store.settings.autoStartServer) invoke("kill_server").catch(() => {});
|
||||||
if (idleTimer) clearTimeout(idleTimer);
|
if (idleTimer) clearTimeout(idleTimer);
|
||||||
if (pollInterval) clearInterval(pollInterval);
|
if (pollInterval) clearInterval(pollInterval);
|
||||||
@@ -148,13 +142,7 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleRetry() {
|
function handleRetry() { failed = false; notConfigured = false; serverProbeOk = false; }
|
||||||
failed = false;
|
|
||||||
notConfigured = false;
|
|
||||||
serverProbeOk = false;
|
|
||||||
// Re-run the full startup flow by reloading — simplest way to reset all state cleanly.
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if devSplash}
|
{#if devSplash}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,13 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
<g transform="translate(256,265) rotate(7) scale(0.098,-0.098) translate(-5000,-4800)" fill="#2d7a5f" stroke="none">
|
||||||
width="512" height="512" viewBox="0 0 512 512">
|
|
||||||
|
|
||||||
<!-- Background -->
|
|
||||||
<rect width="512" height="512" rx="112" ry="112" fill="#0e1a14"/>
|
|
||||||
|
|
||||||
<!-- Leaf scaled up and centered: original paths scaled ~2.2x and centered -->
|
|
||||||
<g transform="translate(256,256) scale(0.072,-0.072) translate(-5000,-4800)"
|
|
||||||
fill="#2d7a5f" stroke="none">
|
|
||||||
<path d="M4908 6615 c-597 -629 -935 -1264 -934 -1755 0 -508 195 -778 790
|
<path d="M4908 6615 c-597 -629 -935 -1264 -934 -1755 0 -508 195 -778 790
|
||||||
-1098 l194 -104 22 -114 c33 -164 76 -271 140 -347 80 -94 106 -62 77 94 -47
|
-1098 l194 -104 22 -114 c33 -164 76 -271 140 -347 80 -94 106 -62 77 94 -47
|
||||||
255 43 443 294 613 797 539 714 1392 -250 2551 -230 278 -224 275 -333 160z
|
255 43 443 294 613 797 539 714 1392 -250 2551 -230 278 -224 275 -333 160z
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<g transform="translate(256,265) scale(0.098,-0.098) translate(-5000,-4800)" fill="#2d7a5f" stroke="none">
|
||||||
|
<path d="M4908 6615 c-597 -629 -935 -1264 -934 -1755 0 -508 195 -778 790
|
||||||
|
-1098 l194 -104 22 -114 c33 -164 76 -271 140 -347 80 -94 106 -62 77 94 -47
|
||||||
|
255 43 443 294 613 797 539 714 1392 -250 2551 -230 278 -224 275 -333 160z
|
||||||
|
m52 -945 c0 -472 -8 -850 -17 -850 -36 0 -693 703 -692 740 2 48 167 321 309
|
||||||
|
509 175 233 359 451 380 451 13 0 20 -304 20 -850z m183 775 c120 -126 357
|
||||||
|
-447 356 -482 0 -18 -47 -83 -105 -144 -57 -61 -151 -163 -208 -225 -151 -166
|
||||||
|
-146 -180 -146 406 0 286 7 520 16 520 9 0 48 -34 87 -75z m541 -750 c86 -150
|
||||||
|
196 -391 196 -430 0 -8 -25 -42 -55 -75 -31 -33 -149 -163 -264 -290 -339
|
||||||
|
-374 -480 -520 -501 -520 -13 0 -20 167 -20 465 l1 465 151 170 c83 94 193
|
||||||
|
217 244 275 122 138 137 135 248 -60z m-1098 -633 l374 -378 0 -462 c0 -254
|
||||||
|
-5 -462 -11 -462 -6 0 -55 23 -110 50 l-99 51 0 208 c0 259 -11 288 -176 457
|
||||||
|
-196 200 -188 199 -326 62 l-117 -116 -35 79 c-71 161 -39 569 64 816 42 100
|
||||||
|
20 116 436 -305z m1349 23 c109 -390 -87 -848 -475 -1111 -207 -139 -305 -262
|
||||||
|
-338 -420 -7 -31 -21 -56 -32 -56 -36 -1 -46 86 -48 405 l-2 313 123 137 c68
|
||||||
|
75 214 236 325 357 454 493 421 466 447 375z m-1292 -785 c12 -29 15 -118 7
|
||||||
|
-215 l-14 -166 -73 44 c-174 104 -403 341 -403 416 0 15 47 70 105 123 l104
|
||||||
|
98 127 -125 c70 -69 136 -147 147 -175z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,27 +1,22 @@
|
|||||||
<?xml version="1.0" standalone="no"?>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
<rect width="512" height="512" rx="112" ry="112" fill="#091209"/>
|
||||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
<g transform="translate(256,265) rotate(7) scale(0.098,-0.098) translate(-5000,-4800)" fill="#2d7a5f" stroke="none">
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
<path d="M4908 6615 c-597 -629 -935 -1264 -934 -1755 0 -508 195 -778 790
|
||||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 500.000000 500.000000"
|
-1098 l194 -104 22 -114 c33 -164 76 -271 140 -347 80 -94 106 -62 77 94 -47
|
||||||
preserveAspectRatio="xMidYMid meet">
|
255 43 443 294 613 797 539 714 1392 -250 2551 -230 278 -224 275 -333 160z
|
||||||
<g transform="translate(0.000000,500.000000) scale(0.050000,-0.050000)"
|
m52 -945 c0 -472 -8 -850 -17 -850 -36 0 -693 703 -692 740 2 48 167 321 309
|
||||||
fill="#2d7a5f" stroke="none">
|
509 175 233 359 451 380 451 13 0 20 -304 20 -850z m183 775 c120 -126 357
|
||||||
<path d="M4908 6615 c-597 -629 -935 -1264 -934 -1755 0 -508 195 -778 790
|
-447 356 -482 0 -18 -47 -83 -105 -144 -57 -61 -151 -163 -208 -225 -151 -166
|
||||||
-1098 l194 -104 22 -114 c33 -164 76 -271 140 -347 80 -94 106 -62 77 94 -47
|
-146 -180 -146 406 0 286 7 520 16 520 9 0 48 -34 87 -75z m541 -750 c86 -150
|
||||||
255 43 443 294 613 797 539 714 1392 -250 2551 -230 278 -224 275 -333 160z
|
196 -391 196 -430 0 -8 -25 -42 -55 -75 -31 -33 -149 -163 -264 -290 -339
|
||||||
m52 -945 c0 -472 -8 -850 -17 -850 -36 0 -693 703 -692 740 2 48 167 321 309
|
-374 -480 -520 -501 -520 -13 0 -20 167 -20 465 l1 465 151 170 c83 94 193
|
||||||
509 175 233 359 451 380 451 13 0 20 -304 20 -850z m183 775 c120 -126 357
|
217 244 275 122 138 137 135 248 -60z m-1098 -633 l374 -378 0 -462 c0 -254
|
||||||
-447 356 -482 0 -18 -47 -83 -105 -144 -57 -61 -151 -163 -208 -225 -151 -166
|
-5 -462 -11 -462 -6 0 -55 23 -110 50 l-99 51 0 208 c0 259 -11 288 -176 457
|
||||||
-146 -180 -146 406 0 286 7 520 16 520 9 0 48 -34 87 -75z m541 -750 c86 -150
|
-196 200 -188 199 -326 62 l-117 -116 -35 79 c-71 161 -39 569 64 816 42 100
|
||||||
196 -391 196 -430 0 -8 -25 -42 -55 -75 -31 -33 -149 -163 -264 -290 -339
|
20 116 436 -305z m1349 23 c109 -390 -87 -848 -475 -1111 -207 -139 -305 -262
|
||||||
-374 -480 -520 -501 -520 -13 0 -20 167 -20 465 l1 465 151 170 c83 94 193
|
-338 -420 -7 -31 -21 -56 -32 -56 -36 -1 -46 86 -48 405 l-2 313 123 137 c68
|
||||||
217 244 275 122 138 137 135 248 -60z m-1098 -633 l374 -378 0 -462 c0 -254
|
75 214 236 325 357 454 493 421 466 447 375z m-1292 -785 c12 -29 15 -118 7
|
||||||
-5 -462 -11 -462 -6 0 -55 23 -110 50 l-99 51 0 208 c0 259 -11 288 -176 457
|
-215 l-14 -166 -73 44 c-174 104 -403 341 -403 416 0 15 47 70 105 123 l104
|
||||||
-196 200 -188 199 -326 62 l-117 -116 -35 79 c-71 161 -39 569 64 816 42 100
|
98 127 -125 c70 -69 136 -147 147 -175z"/>
|
||||||
20 116 436 -305z m1349 23 c109 -390 -87 -848 -475 -1111 -207 -139 -305 -262
|
</g>
|
||||||
-338 -420 -7 -31 -21 -56 -32 -56 -36 -1 -46 86 -48 405 l-2 313 123 137 c68
|
|
||||||
75 214 236 325 357 454 493 421 466 447 375z m-1292 -785 c12 -29 15 -118 7
|
|
||||||
-215 l-14 -166 -73 44 c-174 104 -403 341 -403 416 0 15 47 70 105 123 l104
|
|
||||||
98 127 -125 c70 -69 136 -147 147 -175z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -54,7 +54,7 @@
|
|||||||
.logo:hover { opacity: 0.8; transform: scale(0.96); }
|
.logo:hover { opacity: 0.8; transform: scale(0.96); }
|
||||||
.logo:active { transform: scale(0.92); }
|
.logo:active { transform: scale(0.92); }
|
||||||
.logo:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
.logo:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
.logo-icon { width: 80px; height: 80px; background-color: var(--accent); mask-image: url("../../assets/moku-icon.svg"); mask-repeat: no-repeat; mask-position: center; mask-size: contain; -webkit-mask-image: url("../../assets/moku-icon.svg"); -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; -webkit-mask-size: contain; filter: drop-shadow(0 0 8px rgba(107,143,107,0.35)); pointer-events: none; }
|
.logo-icon { width: 67px; height: 67px; background-color: var(--accent); mask-image: url("../../assets/moku-icon-wordmark.svg"); mask-repeat: no-repeat; mask-position: center; mask-size: contain; -webkit-mask-image: url("../../assets/moku-icon-wordmark.svg"); -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; -webkit-mask-size: contain; filter: drop-shadow(0 0 8px rgba(107,143,107,0.35)); pointer-events: none; }
|
||||||
.nav { flex: 1; display: flex; flex-direction: column; align-items: center; gap: var(--sp-1); width: 100%; padding: 0 var(--sp-2); }
|
.nav { flex: 1; display: flex; flex-direction: column; align-items: center; gap: var(--sp-1); width: 100%; padding: 0 var(--sp-2); }
|
||||||
.tab { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); color: var(--text-faint); background: none; border: none; outline: none; cursor: pointer; padding: 0; appearance: none; -webkit-appearance: none; transition: color var(--t-base), background var(--t-base); }
|
.tab { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); color: var(--text-faint); background: none; border: none; outline: none; cursor: pointer; padding: 0; appearance: none; -webkit-appearance: none; transition: color var(--t-base), background var(--t-base); }
|
||||||
.tab:hover { color: var(--text-muted); background: var(--bg-raised); }
|
.tab:hover { color: var(--text-muted); background: var(--bg-raised); }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { store } from "../../store/state.svelte";
|
import { store } from "../../store/state.svelte";
|
||||||
import logoUrl from "../../assets/moku-icon.svg";
|
import logoUrl from "../../assets/moku-icon-splash.svg";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mode?: "loading" | "idle";
|
mode?: "loading" | "idle";
|
||||||
@@ -20,6 +20,15 @@
|
|||||||
showCards = true, showFps = false, onReady, onRetry, onDismiss }: Props = $props();
|
showCards = true, showFps = false, onReady, onRetry, onDismiss }: Props = $props();
|
||||||
|
|
||||||
const EXIT_MS = 320;
|
const EXIT_MS = 320;
|
||||||
|
// Server typically takes 8-20s to boot. We animate the ring through three
|
||||||
|
// phases so it always feels like something is happening:
|
||||||
|
// 0 → 0.75 over ~12s (eased crawl while server starts)
|
||||||
|
// 0.75 → 0.92 over ~8s (slow down near the end, implying "almost there")
|
||||||
|
// jumps to 1.0 the moment the probe succeeds
|
||||||
|
const PHASE1_TARGET = 0.85;
|
||||||
|
const PHASE1_MS = 3000;
|
||||||
|
const PHASE2_TARGET = 0.95;
|
||||||
|
const PHASE2_MS = 10000;
|
||||||
|
|
||||||
let dots = $state("");
|
let dots = $state("");
|
||||||
let ringProg = $state(0.025);
|
let ringProg = $state(0.025);
|
||||||
@@ -35,8 +44,42 @@
|
|||||||
setTimeout(() => cb?.(), EXIT_MS);
|
setTimeout(() => cb?.(), EXIT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Animate ring progress with easing so it never stalls visually
|
||||||
|
let animFrame: number;
|
||||||
|
let animStart: number | null = null;
|
||||||
|
let animPhase = 1;
|
||||||
|
|
||||||
|
function animateRing(ts: number) {
|
||||||
|
if (exitLock) return;
|
||||||
|
if (animStart === null) animStart = ts;
|
||||||
|
const elapsed = ts - animStart;
|
||||||
|
|
||||||
|
if (animPhase === 1) {
|
||||||
|
const t = Math.min(elapsed / PHASE1_MS, 1);
|
||||||
|
// ease-out cubic so it starts fast and slows down
|
||||||
|
const eased = 1 - Math.pow(1 - t, 3);
|
||||||
|
ringProg = 0.025 + eased * (PHASE1_TARGET - 0.025);
|
||||||
|
if (t >= 1) { animPhase = 2; animStart = ts; }
|
||||||
|
} else if (animPhase === 2) {
|
||||||
|
const t = Math.min(elapsed / PHASE2_MS, 1);
|
||||||
|
const eased = 1 - Math.pow(1 - t, 4);
|
||||||
|
ringProg = PHASE1_TARGET + eased * (PHASE2_TARGET - PHASE1_TARGET);
|
||||||
|
// Phase 2 never completes on its own — only ringFull triggers completion
|
||||||
|
}
|
||||||
|
|
||||||
|
animFrame = requestAnimationFrame(animateRing);
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (mode === "loading" && !failed && !notConfigured) {
|
||||||
|
animFrame = requestAnimationFrame(animateRing);
|
||||||
|
return () => cancelAnimationFrame(animFrame);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (ringFull) {
|
if (ringFull) {
|
||||||
|
cancelAnimationFrame(animFrame);
|
||||||
ringProg = 1;
|
ringProg = 1;
|
||||||
setTimeout(() => triggerExit(onReady), 650);
|
setTimeout(() => triggerExit(onReady), 650);
|
||||||
}
|
}
|
||||||
@@ -149,7 +192,7 @@
|
|||||||
const ctx = oc.getContext("2d")!;
|
const ctx = oc.getContext("2d")!;
|
||||||
ctx.scale(dpr, dpr);
|
ctx.scale(dpr, dpr);
|
||||||
const g = ctx.createRadialGradient(vw / 2, vh / 2, 0, vw / 2, vh / 2, Math.max(vw, vh) * 0.65);
|
const g = ctx.createRadialGradient(vw / 2, vh / 2, 0, vw / 2, vh / 2, Math.max(vw, vh) * 0.65);
|
||||||
g.addColorStop(0.15, "rgba(0,0,0,0)"); g.addColorStop(1, "rgba(0,0,0,0.82)");
|
g.addColorStop(0, "rgba(0,0,0,0)"); g.addColorStop(0.4, "rgba(0,0,0,0)"); g.addColorStop(0.7, "rgba(0,0,0,0.25)"); g.addColorStop(1, "rgba(0,0,0,0.65)");
|
||||||
ctx.fillStyle = g; ctx.fillRect(0, 0, vw, vh);
|
ctx.fillStyle = g; ctx.fillRect(0, 0, vw, vh);
|
||||||
return oc;
|
return oc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,13 +337,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if recentHistory.length > 0}
|
<div class="section">
|
||||||
<div class="section">
|
<div class="section-header">
|
||||||
<div class="section-header">
|
<span class="section-title"><Clock size={10} weight="bold" /> Recent Activity</span>
|
||||||
<span class="section-title"><Clock size={10} weight="bold" /> Recent Activity</span>
|
{#if recentHistory.length > 0}
|
||||||
<button class="see-all" onclick={() => setNavPage("history")}>Full History <ArrowRight size={9} weight="bold" /></button>
|
<button class="see-all" onclick={() => setNavPage("history")}>Full History <ArrowRight size={9} weight="bold" /></button>
|
||||||
</div>
|
{/if}
|
||||||
<div class="activity-list">
|
</div>
|
||||||
|
<div class="activity-list">
|
||||||
|
{#if recentHistory.length > 0}
|
||||||
{#each recentHistory as entry (entry.chapterId)}
|
{#each recentHistory as entry (entry.chapterId)}
|
||||||
<button class="activity-row" onclick={() => resumeEntry(entry)}>
|
<button class="activity-row" onclick={() => resumeEntry(entry)}>
|
||||||
<img src={thumbUrl(entry.thumbnailUrl)} alt={entry.mangaTitle} class="activity-thumb" loading="lazy" decoding="async" />
|
<img src={thumbUrl(entry.thumbnailUrl)} alt={entry.mangaTitle} class="activity-thumb" loading="lazy" decoding="async" />
|
||||||
@@ -355,14 +357,27 @@
|
|||||||
<span class="activity-play"><Play size={10} weight="fill" /></span>
|
<span class="activity-play"><Play size={10} weight="fill" /></span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
{:else}
|
||||||
|
<div class="activity-placeholder">
|
||||||
|
{#each Array(5) as _, i}
|
||||||
|
<div class="activity-row activity-row-sk">
|
||||||
|
<div class="sk-thumb"></div>
|
||||||
|
<div class="activity-info">
|
||||||
|
<div class="sk sk-title" style="width: {55 + (i * 7) % 30}%"></div>
|
||||||
|
<div class="sk sk-sub" style="width: {30 + (i * 11) % 25}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="sk sk-time"></div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="activity-placeholder-overlay">
|
||||||
|
<button class="activity-placeholder-cta" onclick={() => setNavPage("library")}>
|
||||||
|
<BookOpen size={12} weight="light" /> Start reading
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
</div>
|
||||||
<div class="empty-state">
|
|
||||||
<p class="empty-text">Start reading to build your activity feed</p>
|
|
||||||
<button class="empty-cta" onclick={() => store.navPage = "library"}>Open Library <ArrowRight size={11} weight="bold" /></button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="bottom-row">
|
<div class="bottom-row">
|
||||||
<div class="bottom-col">
|
<div class="bottom-col">
|
||||||
@@ -506,7 +521,7 @@
|
|||||||
:global(.ch-play-icon) { color: var(--accent-fg); flex-shrink: 0; }
|
:global(.ch-play-icon) { color: var(--accent-fg); flex-shrink: 0; }
|
||||||
.chapter-row-sk { display: flex; gap: var(--sp-2); padding: 7px var(--sp-2); align-items: center; }
|
.chapter-row-sk { display: flex; gap: var(--sp-2); padding: 7px var(--sp-2); align-items: center; }
|
||||||
.sk-info { flex: 1; display: flex; flex-direction: column; gap: 4px; }
|
.sk-info { flex: 1; display: flex; flex-direction: column; gap: 4px; }
|
||||||
.sk { background: rgba(255,255,255,0.08); border-radius: var(--radius-sm); animation: pulse 1.4s ease infinite; }
|
.sk { background: rgba(255,255,255,0.06); border-radius: var(--radius-sm); }
|
||||||
.sk-num { width: 32px; height: 10px; flex-shrink: 0; }
|
.sk-num { width: 32px; height: 10px; flex-shrink: 0; }
|
||||||
.sk-name { height: 11px; width: 85%; }
|
.sk-name { height: 11px; width: 85%; }
|
||||||
.sk-meta { height: 9px; width: 50%; }
|
.sk-meta { height: 9px; width: 50%; }
|
||||||
@@ -529,12 +544,12 @@
|
|||||||
.activity-play { color: var(--accent-fg); flex-shrink: 0; opacity: 0; transition: opacity var(--t-base); }
|
.activity-play { color: var(--accent-fg); flex-shrink: 0; opacity: 0; transition: opacity var(--t-base); }
|
||||||
.bottom-row { display: grid; grid-template-columns: 1fr 1px 1fr; padding: 0 var(--sp-4); border-top: 1px solid var(--border-dim); flex-shrink: 0; }
|
.bottom-row { display: grid; grid-template-columns: 1fr 1px 1fr; padding: 0 var(--sp-4); border-top: 1px solid var(--border-dim); flex-shrink: 0; }
|
||||||
.bottom-divider { background: var(--border-dim); align-self: stretch; }
|
.bottom-divider { background: var(--border-dim); align-self: stretch; }
|
||||||
.bottom-col { display: flex; flex-direction: column; min-width: 0; padding-top: var(--sp-3); padding-bottom: var(--sp-4); }
|
.bottom-col { display: flex; flex-direction: column; min-width: 0; padding-top: var(--sp-4); padding-bottom: var(--sp-5); }
|
||||||
.bottom-col:first-child { padding-right: var(--sp-4); }
|
.bottom-col:first-child { padding-right: var(--sp-4); }
|
||||||
.bottom-col:last-child { padding-left: var(--sp-4); }
|
.bottom-col:last-child { padding-left: var(--sp-4); }
|
||||||
.bottom-section-hd { display: flex; align-items: center; justify-content: space-between; padding-bottom: var(--sp-2); }
|
.bottom-section-hd { display: flex; align-items: center; justify-content: space-between; padding-bottom: var(--sp-2); }
|
||||||
.bottom-empty { font-family: var(--font-ui); font-size: var(--text-sm); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: var(--sp-1) 0; }
|
.bottom-empty { font-family: var(--font-ui); font-size: var(--text-sm); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: var(--sp-1) 0; }
|
||||||
.mini-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: var(--sp-3); }
|
.mini-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--sp-3); }
|
||||||
|
|
||||||
.mini-card { width: 100%; background: none; border: none; padding: 0; cursor: pointer; text-align: left; }
|
.mini-card { width: 100%; background: none; border: none; padding: 0; cursor: pointer; text-align: left; }
|
||||||
.mini-card:hover .mini-cover { filter: brightness(1.08) saturate(1.05); transform: scale(1.02); }
|
.mini-card:hover .mini-cover { filter: brightness(1.08) saturate(1.05); transform: scale(1.02); }
|
||||||
@@ -546,19 +561,25 @@
|
|||||||
.mini-card-title { font-size: var(--text-xs); font-weight: var(--weight-medium); color: rgba(255,255,255,0.92); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-shadow: 0 1px 4px rgba(0,0,0,0.7); }
|
.mini-card-title { font-size: var(--text-xs); font-weight: var(--weight-medium); color: rgba(255,255,255,0.92); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-shadow: 0 1px 4px rgba(0,0,0,0.7); }
|
||||||
.mini-card-source { font-family: var(--font-ui); font-size: 9px; color: rgba(255,255,255,0.45); letter-spacing: var(--tracking-wide); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.mini-card-source { font-family: var(--font-ui); font-size: 9px; color: rgba(255,255,255,0.45); letter-spacing: var(--tracking-wide); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
|
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
|
||||||
.stat-card { display: flex; align-items: center; gap: var(--sp-3); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: var(--sp-2) var(--sp-3); }
|
.stat-card { display: flex; align-items: center; gap: var(--sp-3); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: var(--sp-3) var(--sp-3); }
|
||||||
.stat-icon-wrap { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-sm); flex-shrink: 0; }
|
.stat-icon-wrap { display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; border-radius: var(--radius-sm); flex-shrink: 0; }
|
||||||
.stat-fire { background: rgba(251,146,60,0.15); color: #fb923c; }
|
.stat-fire { background: rgba(251,146,60,0.15); color: #fb923c; }
|
||||||
.stat-accent { background: var(--accent-muted); color: var(--accent-fg); }
|
.stat-accent { background: var(--accent-muted); color: var(--accent-fg); }
|
||||||
.stat-neutral { background: var(--bg-overlay); color: var(--text-faint); }
|
.stat-neutral { background: var(--bg-overlay); color: var(--text-faint); }
|
||||||
.stat-green { background: rgba(34,197,94,0.12); color: #22c55e; }
|
.stat-green { background: rgba(34,197,94,0.12); color: #22c55e; }
|
||||||
.stat-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
.stat-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
||||||
.stat-val { font-family: var(--font-ui); font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-secondary); line-height: 1; }
|
.stat-val { font-family: var(--font-ui); font-size: var(--text-lg, 1.05rem); font-weight: var(--weight-medium); color: var(--text-secondary); line-height: 1; }
|
||||||
.stat-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); white-space: nowrap; }
|
.stat-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); white-space: nowrap; }
|
||||||
.empty-state { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-6); flex-shrink: 0; }
|
.activity-row-sk { cursor: default; pointer-events: none; }
|
||||||
.empty-text { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
.sk-thumb { width: 33px; height: 48px; border-radius: var(--radius-sm); background: rgba(255,255,255,0.06); flex-shrink: 0; }
|
||||||
.empty-cta { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 16px; border-radius: var(--radius-full); background: var(--accent-muted); border: 1px solid var(--accent-dim); color: var(--accent-fg); cursor: pointer; transition: filter var(--t-base); }
|
.sk { background: var(--bg-raised); border-radius: var(--radius-sm); }
|
||||||
.empty-cta:hover { filter: brightness(1.1); }
|
.sk-title { height: 11px; margin-bottom: 5px; }
|
||||||
|
.sk-sub { height: 9px; }
|
||||||
|
.sk-time { width: 32px; height: 9px; flex-shrink: 0; background: rgba(255,255,255,0.06); border-radius: var(--radius-sm); }
|
||||||
|
.activity-placeholder { position: relative; }
|
||||||
|
.activity-placeholder-overlay { position: absolute; left: 0; right: 0; top: 0; bottom: -1px; display: flex; align-items: flex-end; justify-content: center; padding-bottom: var(--sp-4); pointer-events: none; background: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.6) 100%); }
|
||||||
|
.activity-placeholder-cta { pointer-events: all; display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 16px; border-radius: var(--radius-full); background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.14); color: rgba(255,255,255,0.65); cursor: pointer; transition: background var(--t-base), color var(--t-base); }
|
||||||
|
.activity-placeholder-cta:hover { background: rgba(255,255,255,0.13); color: rgba(255,255,255,0.9); }
|
||||||
.picker-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: var(--z-settings); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.1s ease both; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
|
.picker-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: var(--z-settings); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.1s ease both; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
|
||||||
.picker-modal { width: min(460px, calc(100vw - 48px)); max-height: 68vh; display: flex; flex-direction: column; background: var(--bg-surface); border: 1px solid var(--border-base); border-radius: var(--radius-xl); overflow: hidden; box-shadow: 0 24px 64px rgba(0,0,0,0.6); animation: scaleIn 0.14s ease both; }
|
.picker-modal { width: min(460px, calc(100vw - 48px)); max-height: 68vh; display: flex; flex-direction: column; background: var(--bg-surface); border: 1px solid var(--border-base); border-radius: var(--radius-xl); overflow: hidden; box-shadow: 0 24px 64px rgba(0,0,0,0.6); animation: scaleIn 0.14s ease both; }
|
||||||
.picker-header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
|
.picker-header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
|
||||||
|
|||||||
@@ -258,13 +258,13 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<p class="section-title">Interface Scale</p>
|
<p class="section-title">Interface Scale</p>
|
||||||
<div class="scale-row">
|
<div class="scale-row">
|
||||||
<input type="range" min={70} max={150} step={5} value={store.settings.uiScale}
|
<input type="range" min={70} max={200} step={5} value={store.settings.uiScale}
|
||||||
oninput={(e) => updateSettings({ uiScale: Number(e.currentTarget.value) })} class="scale-slider" />
|
oninput={(e) => updateSettings({ uiScale: Number(e.currentTarget.value) })} class="scale-slider" />
|
||||||
<span class="scale-val">{store.settings.uiScale}%</span>
|
<span class="scale-val">{store.settings.uiScale}%</span>
|
||||||
<button class="step-btn" onclick={() => updateSettings({ uiScale: 100 })} disabled={store.settings.uiScale === 100} title="Reset">↺</button>
|
<button class="step-btn" onclick={() => updateSettings({ uiScale: 100 })} disabled={store.settings.uiScale === 100} title="Reset">↺</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="scale-hint">
|
<p class="scale-hint">
|
||||||
{#each [70,80,90,100,110,125,150] as v}
|
{#each [70,80,90,100,110,125,150,175,200] as v}
|
||||||
<button class="scale-preset" class:active={store.settings.uiScale === v} onclick={() => updateSettings({ uiScale: v })}>{v}%</button>
|
<button class="scale-preset" class:active={store.settings.uiScale === v} onclick={() => updateSettings({ uiScale: v })}>{v}%</button>
|
||||||
{/each}
|
{/each}
|
||||||
</p>
|
</p>
|
||||||
@@ -275,10 +275,7 @@
|
|||||||
<div class="toggle-info"><span class="toggle-label">Server URL</span><span class="toggle-desc">Base URL of your Suwayomi instance</span></div>
|
<div class="toggle-info"><span class="toggle-label">Server URL</span><span class="toggle-desc">Base URL of your Suwayomi instance</span></div>
|
||||||
<input class="text-input" value={store.settings.serverUrl ?? "http://localhost:4567"} oninput={(e) => updateSettings({ serverUrl: e.currentTarget.value })} placeholder="http://localhost:4567" spellcheck="false" />
|
<input class="text-input" value={store.settings.serverUrl ?? "http://localhost:4567"} oninput={(e) => updateSettings({ serverUrl: e.currentTarget.value })} placeholder="http://localhost:4567" spellcheck="false" />
|
||||||
</div>
|
</div>
|
||||||
<div class="step-row">
|
|
||||||
<div class="toggle-info"><span class="toggle-label">Server binary</span><span class="toggle-desc">Path or command to launch tachidesk-server</span></div>
|
|
||||||
<input class="text-input" value={store.settings.serverBinary} oninput={(e) => updateSettings({ serverBinary: e.currentTarget.value })} placeholder="tachidesk-server" spellcheck="false" />
|
|
||||||
</div>
|
|
||||||
<label class="toggle-row">
|
<label class="toggle-row">
|
||||||
<div class="toggle-info"><span class="toggle-label">Auto-start server</span><span class="toggle-desc">Launch tachidesk-server when Moku opens</span></div>
|
<div class="toggle-info"><span class="toggle-label">Auto-start server</span><span class="toggle-desc">Launch tachidesk-server when Moku opens</span></div>
|
||||||
<button role="switch" aria-checked={store.settings.autoStartServer} aria-label="Auto-start server" class="toggle" class:on={store.settings.autoStartServer} onclick={() => updateSettings({ autoStartServer: !store.settings.autoStartServer })}><span class="toggle-thumb"></span></button>
|
<button role="switch" aria-checked={store.settings.autoStartServer} aria-label="Auto-start server" class="toggle" class:on={store.settings.autoStartServer} onclick={() => updateSettings({ autoStartServer: !store.settings.autoStartServer })}><span class="toggle-thumb"></span></button>
|
||||||
|
|||||||