mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 17:29:55 -05:00
Feat: Settings Button in Reader + Dropdown Overhaul + Settings Listener (#46)
This commit is contained in:
@@ -465,6 +465,7 @@
|
|||||||
onDeleteMarker={deleteCurrentMarker}
|
onDeleteMarker={deleteCurrentMarker}
|
||||||
onClampZoom={clampZoom}
|
onClampZoom={clampZoom}
|
||||||
onDlOpen={() => readerState.dlOpen = true}
|
onDlOpen={() => readerState.dlOpen = true}
|
||||||
|
onSettingsOpen={() => setSettingsOpen(true)}
|
||||||
{win}
|
{win}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
Square, Rows, BookOpen, MonitorPlay,
|
Square, Rows, BookOpen, MonitorPlay,
|
||||||
ArrowsLeftRight, ArrowsIn, ArrowsOut, ArrowsVertical,
|
ArrowsLeftRight, ArrowsIn, ArrowsOut, ArrowsVertical,
|
||||||
MagnifyingGlassMinus, MagnifyingGlassPlus,
|
MagnifyingGlassMinus, MagnifyingGlassPlus,
|
||||||
Bookmark, MapPin, Download, Check,
|
Bookmark, MapPin, Download, Check, GearSix,
|
||||||
} from "phosphor-svelte";
|
} from "phosphor-svelte";
|
||||||
import { store, updateSettings } from "@store/state.svelte";
|
import { store, updateSettings } from "@store/state.svelte";
|
||||||
import { openReader, closeReader } from "@store/state.svelte";
|
import { openReader, closeReader } from "@store/state.svelte";
|
||||||
import { readerState, MARKER_COLORS, MARKER_COLOR_HEX, ZOOM_STEP, ZOOM_MIN, ZOOM_MAX, PAGE_STYLES } from "../store/readerState.svelte";
|
import { readerState, MARKER_COLORS, MARKER_COLOR_HEX, ZOOM_STEP, ZOOM_MIN, ZOOM_MAX, PAGE_STYLES } from "../store/readerState.svelte";
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
import { cubicOut, cubicIn } from "svelte/easing";
|
||||||
import type { FitMode } from "@store/state.svelte";
|
import type { FitMode } from "@store/state.svelte";
|
||||||
import type { Chapter } from "@types";
|
import type { Chapter } from "@types";
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
onDeleteMarker: () => void;
|
onDeleteMarker: () => void;
|
||||||
onClampZoom: (z: number) => number;
|
onClampZoom: (z: number) => number;
|
||||||
onDlOpen: () => void;
|
onDlOpen: () => void;
|
||||||
|
onSettingsOpen: () => void;
|
||||||
win: import("@tauri-apps/api/window").Window;
|
win: import("@tauri-apps/api/window").Window;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@
|
|||||||
autoNext, markOnNext, uiVisible, hideTimer,
|
autoNext, markOnNext, uiVisible, hideTimer,
|
||||||
onCaptureZoomAnchor, onRestoreZoomAnchor,
|
onCaptureZoomAnchor, onRestoreZoomAnchor,
|
||||||
onMaybeMarkRead, onToggleBookmark, onCommitMarker, onDeleteMarker,
|
onMaybeMarkRead, onToggleBookmark, onCommitMarker, onDeleteMarker,
|
||||||
onClampZoom, onDlOpen, win,
|
onClampZoom, onDlOpen, onSettingsOpen, win,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
function adjustZoom(delta: number) {
|
function adjustZoom(delta: number) {
|
||||||
@@ -78,6 +81,19 @@
|
|||||||
if (hideTimer) clearTimeout(hideTimer);
|
if (hideTimer) clearTimeout(hideTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let wcTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
function wcResetTimer() {
|
||||||
|
if (wcTimer) clearTimeout(wcTimer);
|
||||||
|
wcTimer = setTimeout(() => { readerState.winOpen = false; }, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (readerState.winOpen) wcResetTimer();
|
||||||
|
else if (wcTimer) { clearTimeout(wcTimer); wcTimer = null; }
|
||||||
|
return () => { if (wcTimer) clearTimeout(wcTimer); };
|
||||||
|
});
|
||||||
|
|
||||||
function openMarkerPopover() {
|
function openMarkerPopover() {
|
||||||
if (currentPageMarkers.length > 0) {
|
if (currentPageMarkers.length > 0) {
|
||||||
const first = currentPageMarkers[0];
|
const first = currentPageMarkers[0];
|
||||||
@@ -268,32 +284,41 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{#if readerState.winOpen}
|
{#if readerState.winOpen}
|
||||||
<div class="wc-dropdown" role="presentation" onclick={(e) => e.stopPropagation()}>
|
<div class="wc-clip" onmouseenter={wcResetTimer} onmousemove={wcResetTimer}>
|
||||||
<button class="wc-btn" onclick={() => { readerState.winOpen = false; win.minimize(); }}>
|
<div
|
||||||
<svg width="10" height="1" viewBox="0 0 10 1"><line x1="0" y1="0.5" x2="10" y2="0.5" stroke="currentColor" stroke-width="1.5"/></svg>
|
class="wc-bar"
|
||||||
<span>Minimize</span>
|
role="presentation"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
in:fly={{ y: '-100%', duration: 200, easing: cubicOut }}
|
||||||
|
out:fly={{ y: '-100%', duration: 150, easing: cubicIn }}
|
||||||
|
>
|
||||||
|
<button class="wc-icon-btn" onclick={() => { readerState.winOpen = false; onSettingsOpen(); }} title="Settings">
|
||||||
|
<GearSix size={13} weight="regular" />
|
||||||
</button>
|
</button>
|
||||||
<button class="wc-btn" onclick={() => { readerState.winOpen = false; win.toggleMaximize(); }}>
|
<div class="wc-bar-sep"></div>
|
||||||
|
<button class="wc-icon-btn" onclick={() => { readerState.winOpen = false; win.minimize(); }} title="Minimize">
|
||||||
|
<svg width="10" height="2" viewBox="0 0 10 2"><line x1="0" y1="1" x2="10" y2="1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="wc-icon-btn" onclick={() => { readerState.winOpen = false; win.toggleMaximize(); }} title={isFullscreen ? "Exit Fullscreen" : "Fullscreen"}>
|
||||||
{#if isFullscreen}
|
{#if isFullscreen}
|
||||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
<svg width="11" height="11" viewBox="0 0 11 11">
|
||||||
<polyline points="1,4 1,1 4,1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<polyline points="1,4 1,1 4,1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<polyline points="6,1 9,1 9,4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<polyline points="7,1 10,1 10,4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<polyline points="9,6 9,9 6,9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<polyline points="10,7 10,10 7,10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<polyline points="4,9 1,9 1,6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<polyline points="4,10 1,10 1,7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
{:else}
|
{:else}
|
||||||
<svg width="9" height="9" viewBox="0 0 9 9"><rect x="0.75" y="0.75" width="7.5" height="7.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.75" y="0.75" width="8.5" height="8.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||||
{/if}
|
{/if}
|
||||||
<span>{isFullscreen ? "Exit Fullscreen" : "Fullscreen"}</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button class="wc-btn wc-close" onclick={() => { readerState.winOpen = false; win.close(); }}>
|
<button class="wc-icon-btn wc-icon-close" onclick={() => { readerState.winOpen = false; win.close(); }} title="Close">
|
||||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||||
<line x1="1" y1="1" x2="9" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<line x1="1" y1="1" x2="9" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
<line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Close</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,7 +326,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.topbar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-1); padding: 0 var(--sp-3); height: 40px; background: var(--bg-void); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; position: relative; z-index: 2; transition: opacity 0.25s ease; }
|
.topbar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-1); padding: 0 var(--sp-3); height: 40px; background: var(--bg-void); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; position: relative; z-index: 2; transition: opacity 0.25s ease; overflow: visible; }
|
||||||
.topbar.hidden { opacity: 0; pointer-events: none; }
|
.topbar.hidden { opacity: 0; pointer-events: none; }
|
||||||
|
|
||||||
.topbar-left { display: flex; align-items: center; gap: var(--sp-1); min-width: 0; flex: 1; overflow: hidden; }
|
.topbar-left { display: flex; align-items: center; gap: var(--sp-1); min-width: 0; flex: 1; overflow: hidden; }
|
||||||
@@ -362,12 +387,42 @@
|
|||||||
.marker-cancel-btn { flex: 1; padding: 6px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); cursor: pointer; transition: color var(--t-base), border-color var(--t-base); text-align: center; }
|
.marker-cancel-btn { flex: 1; padding: 6px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); cursor: pointer; transition: color var(--t-base), border-color var(--t-base); text-align: center; }
|
||||||
.marker-cancel-btn:hover { color: var(--text-muted); border-color: var(--border-strong); }
|
.marker-cancel-btn:hover { color: var(--text-muted); border-color: var(--border-strong); }
|
||||||
|
|
||||||
.wc-wrap { position: relative; flex-shrink: 0; }
|
.wc-wrap { position: static; flex-shrink: 0; }
|
||||||
.wc-dropdown { position: absolute; top: calc(100% + 6px); right: 0; background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-lg); padding: var(--sp-1); display: flex; flex-direction: column; gap: 2px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); z-index: 100; min-width: 148px; animation: scaleIn 0.1s ease both; transform-origin: top right; }
|
.wc-clip {
|
||||||
.wc-btn { display: flex; align-items: center; gap: var(--sp-2); padding: 6px var(--sp-2); border-radius: var(--radius-sm); background: none; border: none; color: var(--text-muted); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); cursor: pointer; width: 100%; transition: color var(--t-base), background var(--t-base); }
|
position: absolute;
|
||||||
.wc-btn svg { flex-shrink: 0; opacity: 0.75; }
|
top: 100%;
|
||||||
.wc-btn:hover { color: var(--text-primary); background: var(--bg-overlay); }
|
right: var(--sp-3);
|
||||||
.wc-close:hover { color: #fff; background: #c0392b; }
|
z-index: 100;
|
||||||
|
clip-path: inset(0 -20px -20px -20px);
|
||||||
|
}
|
||||||
|
.wc-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1px;
|
||||||
|
padding: 3px 10px 4px;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
border-top: none;
|
||||||
|
box-shadow: 0 6px 16px rgba(0,0,0,0.45);
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
.wc-icon-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 26px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--t-base), background var(--t-base);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.wc-icon-btn:hover { color: var(--text-primary); background: var(--bg-overlay); }
|
||||||
|
.wc-icon-close:hover { color: #fff; background: #c0392b; }
|
||||||
|
.wc-bar-sep { width: 1px; height: 12px; background: var(--border-dim); margin: 0 2px; flex-shrink: 0; }
|
||||||
|
|
||||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.97) } to { opacity: 1; transform: scale(1) } }
|
@keyframes scaleIn { from { opacity: 0; transform: scale(0.97) } to { opacity: 1; transform: scale(1) } }
|
||||||
</style>
|
</style>
|
||||||
@@ -65,9 +65,9 @@
|
|||||||
let listeningKey: keyof Keybinds | null = $state(null);
|
let listeningKey: keyof Keybinds | null = $state(null);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && !listeningKey) close(); };
|
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && !listeningKey) { e.stopPropagation(); close(); } };
|
||||||
window.addEventListener("keydown", onKey);
|
window.addEventListener("keydown", onKey, true);
|
||||||
return () => window.removeEventListener("keydown", onKey);
|
return () => window.removeEventListener("keydown", onKey, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
|
|
||||||
<div class="s-backdrop" role="presentation" tabindex="-1"
|
<div class="s-backdrop" role="presentation" tabindex="-1"
|
||||||
onclick={(e) => { if (e.target === e.currentTarget) close(); }}
|
onclick={(e) => { if (e.target === e.currentTarget) close(); }}
|
||||||
onkeydown={(e) => { if (e.key === "Escape") close(); }}>
|
onkeydown={(e) => { if (e.key === "Escape") { e.stopPropagation(); close(); } }}>
|
||||||
<div class="s-modal" role="dialog" aria-label="Settings">
|
<div class="s-modal" role="dialog" aria-label="Settings">
|
||||||
|
|
||||||
<div class="s-sidebar">
|
<div class="s-sidebar">
|
||||||
|
|||||||
Reference in New Issue
Block a user