Feat: Revamped Logo, QOL Home-Screen Additions, Scaling Logic Revamp

This commit is contained in:
Youwes09
2026-03-22 01:26:40 -05:00
parent d3e62a7a08
commit 06cb70048b
30 changed files with 232 additions and 167 deletions
+2 -2
View File
@@ -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>
+18 -1
View File
@@ -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"
] ]
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 B

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+13 -25
View File
@@ -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,26 +254,10 @@ 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() {
resource_dir.clone()
} else {
resource_dir.join(bundle_rel)
};
let jar = resource_dir.join(jar_rel);
do_log(log, &format!("[resolve] trying bundle_dir = {:?}", bundle_dir));
do_log(log, &format!("[resolve] bundle_dir exists: {}", bundle_dir.exists())); do_log(log, &format!("[resolve] bundle_dir exists: {}", bundle_dir.exists()));
do_log(log, &format!("[resolve] jar = {:?}", jar)); do_log(log, &format!("[resolve] jar = {:?}", jar));
do_log(log, &format!("[resolve] jar exists: {}", jar.exists())); do_log(log, &format!("[resolve] jar exists: {}", jar.exists()));
@@ -287,12 +276,11 @@ fn resolve_server_binary(
working_dir: Some(bundle_dir), working_dir: Some(bundle_dir),
}); });
} else { } else {
do_log(log, "[resolve] java found but jar MISSING — trying next candidate"); do_log(log, "[resolve] java found but jar MISSING — skipping bundled path");
} }
} }
None => { None => {
do_log(log, "[resolve] java NOT found — trying next candidate"); 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| {
+5 -2
View File
@@ -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",
+35 -47
View File
@@ -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,11 +92,26 @@
return () => clearInterval(pollInterval); return () => clearInterval(pollInterval);
}); });
// Probe the server in a loop until it responds or we hit MAX_ATTEMPTS. onMount(async () => {
// Returns a cleanup function that cancels any pending probe. document.addEventListener("contextmenu", e => e.preventDefault());
function startProbe(): () => void { (window as any).__mokuShowSplash = () => devSplash = true;
let cancelled = false, tries = 0;
// 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) {
invoke<void>("spawn_server", { binary: store.settings.serverBinary }).catch((err: any) => {
if (err?.kind === "NotConfigured") {
notConfigured = true;
} else {
console.warn("Could not start server:", err);
}
});
}
if (!serverProbeOk) {
let cancelled = false, tries = 0;
async function probe() { async function probe() {
if (cancelled) return; if (cancelled) return;
tries++; tries++;
@@ -102,44 +124,16 @@
if (res.ok && !cancelled) { serverProbeOk = true; return; } if (res.ok && !cancelled) { serverProbeOk = true; return; }
} catch {} } catch {}
if (tries >= MAX_ATTEMPTS && !cancelled) { failed = true; return; } if (tries >= MAX_ATTEMPTS && !cancelled) { failed = true; return; }
if (!cancelled) setTimeout(probe, 800); if (!cancelled) setTimeout(probe, 500);
} }
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 () => {
document.addEventListener("contextmenu", e => e.preventDefault());
(window as any).__mokuShowSplash = () => devSplash = true;
let cancelProbe = () => {};
if (store.settings.autoStartServer) {
try {
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") {
notConfigured = true;
} else {
// SpawnFailed — process couldn't be launched (permissions, bad path, etc.)
console.error("spawn_server failed:", err);
failed = true;
}
}
} else {
// 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}
Binary file not shown.

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

+21
View File
@@ -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

+21 -26
View File
@@ -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

+1 -1
View File
@@ -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); }
+45 -2
View File
@@ -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;
} }
+37 -16
View File
@@ -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>
{/if}
</div> </div>
<div class="activity-list"> <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>
</div>
{:else} {:else}
<div class="empty-state"> <div class="activity-placeholder">
<p class="empty-text">Start reading to build your activity feed</p> {#each Array(5) as _, i}
<button class="empty-cta" onclick={() => store.navPage = "library"}>Open Library <ArrowRight size={11} weight="bold" /></button> <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> </div>
{/if} {/if}
</div>
</div>
<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; }
+3 -6
View File
@@ -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>