mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
[BETA] QOL Updates (Reader AutoScroll WIP)
This commit is contained in:
@@ -159,20 +159,45 @@
|
||||
min-width: 28px; text-align: center; letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
|
||||
/* ─── Select ── */
|
||||
.select {
|
||||
/* ─── Select (custom) ── */
|
||||
.selectWrap { position: relative; flex-shrink: 0; min-width: 130px; }
|
||||
|
||||
.selectBtn {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
|
||||
width: 100%; padding: 5px 10px;
|
||||
background: var(--bg-raised); border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-md); padding: 5px 10px; color: var(--text-secondary);
|
||||
border-radius: var(--radius-md); color: var(--text-secondary);
|
||||
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
||||
outline: none; cursor: pointer; flex-shrink: 0; transition: border-color var(--t-base);
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M0 0l5 6 5-6' fill='%23888'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
padding-right: 24px;
|
||||
cursor: pointer; transition: border-color var(--t-base), background var(--t-base);
|
||||
text-align: left;
|
||||
}
|
||||
.select:focus { border-color: var(--border-focus); }
|
||||
.select option { background: var(--bg-raised); color: var(--text-secondary); }
|
||||
.selectBtn:hover { border-color: var(--border-focus); }
|
||||
|
||||
.selectCaret {
|
||||
color: var(--text-faint); flex-shrink: 0;
|
||||
transition: transform var(--t-base);
|
||||
}
|
||||
.selectCaretOpen { transform: rotate(180deg); }
|
||||
|
||||
.selectMenu {
|
||||
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
|
||||
background: var(--bg-raised); border: 1px solid var(--border-base);
|
||||
border-radius: var(--radius-md); padding: var(--sp-1);
|
||||
display: flex; flex-direction: column; gap: 1px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
z-index: 200; animation: scaleIn 0.1s ease both; transform-origin: top center;
|
||||
}
|
||||
|
||||
.selectOption {
|
||||
padding: 6px 10px; border-radius: var(--radius-sm);
|
||||
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
||||
color: var(--text-secondary); background: none; border: none;
|
||||
cursor: pointer; text-align: left;
|
||||
transition: background var(--t-fast), color var(--t-fast);
|
||||
}
|
||||
.selectOption:hover { background: var(--bg-overlay); color: var(--text-primary); }
|
||||
.selectOptionActive { color: var(--accent-fg); background: var(--accent-muted); }
|
||||
.selectOptionActive:hover { background: var(--accent-muted); color: var(--accent-fg); }
|
||||
|
||||
/* ─── Scale ── */
|
||||
.scaleRow {
|
||||
|
||||
@@ -62,15 +62,51 @@ function SelectRow({ value, options, onChange, label, description }: {
|
||||
label: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const selected = options.find((o) => o.value === value);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); };
|
||||
document.addEventListener("mousedown", handler);
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handler);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className={s.stepRow}>
|
||||
<div className={s.toggleInfo}>
|
||||
<span className={s.toggleLabel}>{label}</span>
|
||||
{description && <span className={s.toggleDesc}>{description}</span>}
|
||||
</div>
|
||||
<select className={s.select} value={value} onChange={(e) => onChange(e.target.value)}>
|
||||
{options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
<div className={s.selectWrap} ref={ref}>
|
||||
<button className={s.selectBtn} onClick={() => setOpen((o) => !o)}>
|
||||
<span>{selected?.label ?? value}</span>
|
||||
<svg className={[s.selectCaret, open ? s.selectCaretOpen : ""].join(" ")} width="10" height="6" viewBox="0 0 10 6" fill="none">
|
||||
<path d="M0 0l5 6 5-6" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && (
|
||||
<div className={s.selectMenu}>
|
||||
{options.map((o) => (
|
||||
<button
|
||||
key={o.value}
|
||||
className={[s.selectOption, o.value === value ? s.selectOptionActive : ""].join(" ")}
|
||||
onClick={() => { onChange(o.value); setOpen(false); }}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -143,11 +179,10 @@ function ReaderTab({ settings, update }: { settings: Settings; update: (p: Parti
|
||||
<p className={s.sectionTitle}>Page Layout</p>
|
||||
<SelectRow label="Default layout"
|
||||
description="How chapters open by default"
|
||||
value={settings.pageStyle}
|
||||
value={settings.pageStyle === "double" ? "single" : settings.pageStyle}
|
||||
options={[
|
||||
{ value: "single", label: "Single page" },
|
||||
{ value: "double", label: "Double page" },
|
||||
{ value: "longstrip", label: "Long strip" },
|
||||
{ value: "single", label: "Single page" },
|
||||
{ value: "longstrip", label: "Long strip" },
|
||||
]}
|
||||
onChange={(v) => update({ pageStyle: v as Settings["pageStyle"] })} />
|
||||
<SelectRow label="Reading direction"
|
||||
@@ -158,12 +193,8 @@ function ReaderTab({ settings, update }: { settings: Settings; update: (p: Parti
|
||||
{ value: "rtl", label: "Right to left" },
|
||||
]}
|
||||
onChange={(v) => update({ readingDirection: v as Settings["readingDirection"] })} />
|
||||
<Toggle label="Offset double spreads"
|
||||
description="Shift double-page groups so spreads align correctly"
|
||||
checked={settings.offsetDoubleSpreads}
|
||||
onChange={(v) => update({ offsetDoubleSpreads: v })} />
|
||||
<Toggle label="Page gap"
|
||||
description="Add spacing between pages in double and longstrip modes"
|
||||
description="Add spacing between pages in longstrip mode"
|
||||
checked={settings.pageGap}
|
||||
onChange={(v) => update({ pageGap: v })} />
|
||||
</div>
|
||||
@@ -174,9 +205,9 @@ function ReaderTab({ settings, update }: { settings: Settings; update: (p: Parti
|
||||
description="How pages are sized to fit the screen"
|
||||
value={settings.fitMode ?? "width"}
|
||||
options={[
|
||||
{ value: "width", label: "Fit width" },
|
||||
{ value: "height", label: "Fit height" },
|
||||
{ value: "screen", label: "Fit screen" },
|
||||
{ value: "width", label: "Fit width" },
|
||||
{ value: "height", label: "Fit height" },
|
||||
{ value: "screen", label: "Fit screen" },
|
||||
{ value: "original", label: "Original (1:1)" },
|
||||
]}
|
||||
onChange={(v) => update({ fitMode: v as FitMode })} />
|
||||
@@ -203,6 +234,10 @@ function ReaderTab({ settings, update }: { settings: Settings; update: (p: Parti
|
||||
description="Mark a chapter as read when you reach the last page"
|
||||
checked={settings.autoMarkRead}
|
||||
onChange={(v) => update({ autoMarkRead: v })} />
|
||||
<Toggle label="Auto-advance chapters"
|
||||
description="Automatically open the next chapter at the end of a long strip"
|
||||
checked={settings.autoNextChapter ?? false}
|
||||
onChange={(v) => update({ autoNextChapter: v })} />
|
||||
<Stepper label="Pages to preload"
|
||||
description="Images loaded ahead of the current page"
|
||||
value={settings.preloadPages} min={0} max={10}
|
||||
@@ -252,24 +287,24 @@ function LibraryTab({ settings, update }: { settings: Settings; update: (p: Part
|
||||
description="Language variant shown first when an extension has multiple"
|
||||
value={settings.preferredExtensionLang ?? "en"}
|
||||
options={[
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "es", label: "Spanish" },
|
||||
{ value: "fr", label: "French" },
|
||||
{ value: "de", label: "German" },
|
||||
{ value: "pt-br", label: "Portuguese (BR)" },
|
||||
{ value: "it", label: "Italian" },
|
||||
{ value: "ru", label: "Russian" },
|
||||
{ value: "ar", label: "Arabic" },
|
||||
{ value: "tr", label: "Turkish" },
|
||||
{ value: "zh", label: "Chinese (Simplified)" },
|
||||
{ value: "zh-hant", label: "Chinese (Traditional)" },
|
||||
{ value: "ko", label: "Korean" },
|
||||
{ value: "ja", label: "Japanese" },
|
||||
{ value: "id", label: "Indonesian" },
|
||||
{ value: "vi", label: "Vietnamese" },
|
||||
{ value: "th", label: "Thai" },
|
||||
{ value: "pl", label: "Polish" },
|
||||
{ value: "nl", label: "Dutch" },
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "es", label: "Spanish" },
|
||||
{ value: "fr", label: "French" },
|
||||
{ value: "de", label: "German" },
|
||||
{ value: "pt-br", label: "Portuguese (BR)" },
|
||||
{ value: "it", label: "Italian" },
|
||||
{ value: "ru", label: "Russian" },
|
||||
{ value: "ar", label: "Arabic" },
|
||||
{ value: "tr", label: "Turkish" },
|
||||
{ value: "zh", label: "Chinese (Simplified)" },
|
||||
{ value: "zh-hant", label: "Chinese (Traditional)" },
|
||||
{ value: "ko", label: "Korean" },
|
||||
{ value: "ja", label: "Japanese" },
|
||||
{ value: "id", label: "Indonesian" },
|
||||
{ value: "vi", label: "Vietnamese" },
|
||||
{ value: "th", label: "Thai" },
|
||||
{ value: "pl", label: "Polish" },
|
||||
{ value: "nl", label: "Dutch" },
|
||||
]}
|
||||
onChange={(v) => update({ preferredExtensionLang: v })} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user