|
|
|
|
@@ -53,6 +53,8 @@
|
|
|
|
|
type ReadWidth = 'narrow' | 'normal' | 'wide';
|
|
|
|
|
type ParaStyle = 'spaced' | 'indented';
|
|
|
|
|
type PlayerStyle = 'standard' | 'compact';
|
|
|
|
|
/** Controls how many lines fit on a page by adjusting the container height offset. */
|
|
|
|
|
type PageLines = 'less' | 'normal' | 'more';
|
|
|
|
|
|
|
|
|
|
interface LayoutPrefs {
|
|
|
|
|
readMode: ReadMode;
|
|
|
|
|
@@ -61,12 +63,19 @@
|
|
|
|
|
paraStyle: ParaStyle;
|
|
|
|
|
focusMode: boolean;
|
|
|
|
|
playerStyle: PlayerStyle;
|
|
|
|
|
pageLines: PageLines;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const LAYOUT_KEY = 'reader_layout_v1';
|
|
|
|
|
const LINE_HEIGHTS: Record<LineSpacing, number> = { compact: 1.55, normal: 1.85, relaxed: 2.2 };
|
|
|
|
|
const READ_WIDTHS: Record<ReadWidth, string> = { narrow: '58ch', normal: '72ch', wide: 'min(90ch, 100%)' };
|
|
|
|
|
const DEFAULT_LAYOUT: LayoutPrefs = { readMode: 'scroll', lineSpacing: 'normal', readWidth: 'normal', paraStyle: 'spaced', focusMode: false, playerStyle: 'standard' };
|
|
|
|
|
const LINE_HEIGHTS: Record<LineSpacing, number> = { compact: 1.55, normal: 1.85, relaxed: 2.2 };
|
|
|
|
|
const READ_WIDTHS: Record<ReadWidth, string> = { narrow: '58ch', normal: '72ch', wide: 'min(90ch, 100%)' };
|
|
|
|
|
/**
|
|
|
|
|
* Extra rem subtracted from (or added to) the paginated container height.
|
|
|
|
|
* Normal (0rem) keeps the existing calc(); Less (-4rem) makes the container
|
|
|
|
|
* shorter so fewer lines fit per page; More (+4rem) grows it for more lines.
|
|
|
|
|
*/
|
|
|
|
|
const PAGE_LINES_OFFSET: Record<PageLines, string> = { less: '4rem', normal: '0rem', more: '-4rem' };
|
|
|
|
|
const DEFAULT_LAYOUT: LayoutPrefs = { readMode: 'scroll', lineSpacing: 'normal', readWidth: 'normal', paraStyle: 'spaced', focusMode: false, playerStyle: 'standard', pageLines: 'normal' };
|
|
|
|
|
|
|
|
|
|
function loadLayout(): LayoutPrefs {
|
|
|
|
|
if (!browser) return DEFAULT_LAYOUT;
|
|
|
|
|
@@ -114,8 +123,10 @@
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (layout.readMode !== 'paginated') { pageIndex = 0; totalPages = 1; return; }
|
|
|
|
|
// Re-run when html changes or container is bound
|
|
|
|
|
// Re-run when html, container refs, or mini-player visibility changes
|
|
|
|
|
// (mini-player adds pb-24 which reduces the available viewport height)
|
|
|
|
|
void html; void paginatedContainerEl; void paginatedContentEl;
|
|
|
|
|
void audioStore.active; void audioExpanded;
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
if (!paginatedContainerEl || !paginatedContentEl) return;
|
|
|
|
|
const h = paginatedContainerEl.clientHeight;
|
|
|
|
|
@@ -542,7 +553,9 @@
|
|
|
|
|
role="none"
|
|
|
|
|
bind:this={paginatedContainerEl}
|
|
|
|
|
class="paginated-container mt-8"
|
|
|
|
|
style="height: {layout.focusMode ? 'calc(100svh - 8rem)' : 'calc(100svh - 26rem)'};"
|
|
|
|
|
style="height: {layout.focusMode
|
|
|
|
|
? 'calc(100svh - 8rem)'
|
|
|
|
|
: `calc(100svh - 26rem - ${PAGE_LINES_OFFSET[layout.pageLines]})`};"
|
|
|
|
|
onclick={handlePaginatedClick}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
@@ -636,40 +649,44 @@
|
|
|
|
|
|
|
|
|
|
<!-- ── Focus mode floating nav ───────────────────────────────────────────── -->
|
|
|
|
|
{#if layout.focusMode}
|
|
|
|
|
<div class="fixed bottom-[4.5rem] left-1/2 -translate-x-1/2 z-50 flex items-center gap-2">
|
|
|
|
|
{#if data.prev}
|
|
|
|
|
<a
|
|
|
|
|
href="/books/{data.book.slug}/chapters/{data.prev}"
|
|
|
|
|
class="flex items-center gap-1 px-3 py-1.5 rounded-full bg-(--color-surface-2)/90 backdrop-blur border border-(--color-border) text-(--color-muted) hover:text-(--color-text) text-xs transition-colors shadow-md"
|
|
|
|
|
<div class="fixed {audioStore.active ? 'bottom-[4.5rem]' : 'bottom-6'} left-1/2 -translate-x-1/2 z-50">
|
|
|
|
|
<div class="flex items-center divide-x divide-(--color-border) rounded-full bg-(--color-surface-2)/95 backdrop-blur border border-(--color-border) shadow-lg text-xs text-(--color-muted) overflow-hidden">
|
|
|
|
|
{#if data.prev}
|
|
|
|
|
<a
|
|
|
|
|
href="/books/{data.book.slug}/chapters/{data.prev}"
|
|
|
|
|
class="flex items-center gap-1 px-3 py-2 hover:text-(--color-text) hover:bg-(--color-surface-3) transition-colors"
|
|
|
|
|
aria-label="Previous chapter"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{m.reader_chapter_n({ n: String(data.prev) })}
|
|
|
|
|
</a>
|
|
|
|
|
{/if}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => setLayout('focusMode', false)}
|
|
|
|
|
class="flex items-center gap-1 px-3 py-2 hover:text-(--color-brand) hover:bg-(--color-surface-3) transition-colors"
|
|
|
|
|
aria-label="Exit focus mode"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{m.reader_chapter_n({ n: String(data.prev) })}
|
|
|
|
|
</a>
|
|
|
|
|
{/if}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => setLayout('focusMode', false)}
|
|
|
|
|
class="flex items-center gap-1 px-3 py-1.5 rounded-full bg-(--color-surface-2)/90 backdrop-blur border border-(--color-border) text-(--color-muted) hover:text-(--color-brand) text-xs transition-colors shadow-md"
|
|
|
|
|
aria-label="Exit focus mode"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
|
|
|
</svg>
|
|
|
|
|
Exit focus
|
|
|
|
|
</button>
|
|
|
|
|
{#if data.next}
|
|
|
|
|
<a
|
|
|
|
|
href="/books/{data.book.slug}/chapters/{data.next}"
|
|
|
|
|
class="flex items-center gap-1 px-3 py-1.5 rounded-full bg-(--color-surface-2)/90 backdrop-blur border border-(--color-border) text-(--color-muted) hover:text-(--color-text) text-xs transition-colors shadow-md"
|
|
|
|
|
>
|
|
|
|
|
{m.reader_chapter_n({ n: String(data.next) })}
|
|
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</a>
|
|
|
|
|
{/if}
|
|
|
|
|
Exit focus
|
|
|
|
|
</button>
|
|
|
|
|
{#if data.next}
|
|
|
|
|
<a
|
|
|
|
|
href="/books/{data.book.slug}/chapters/{data.next}"
|
|
|
|
|
class="flex items-center gap-1 px-3 py-2 hover:text-(--color-text) hover:bg-(--color-surface-3) transition-colors"
|
|
|
|
|
aria-label="Next chapter"
|
|
|
|
|
>
|
|
|
|
|
{m.reader_chapter_n({ n: String(data.next) })}
|
|
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</a>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
@@ -783,6 +800,25 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if layout.readMode === 'paginated'}
|
|
|
|
|
<div class="flex items-center gap-3 px-3 py-2.5">
|
|
|
|
|
<span class="text-xs text-(--color-muted) w-16 shrink-0">Lines</span>
|
|
|
|
|
<div class="flex gap-1.5 flex-1">
|
|
|
|
|
{#each ([['less', 'Few'], ['normal', 'Normal'], ['more', 'Many']] as const) as [v, lbl]}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => setLayout('pageLines', v)}
|
|
|
|
|
class="flex-1 py-1.5 rounded-lg border text-xs font-medium transition-colors
|
|
|
|
|
{layout.pageLines === v
|
|
|
|
|
? 'border-(--color-brand) bg-(--color-brand)/10 text-(--color-brand)'
|
|
|
|
|
: 'border-(--color-border) bg-(--color-surface-2) text-(--color-muted) hover:border-(--color-brand)/50 hover:text-(--color-text)'}"
|
|
|
|
|
aria-pressed={layout.pageLines === v}
|
|
|
|
|
>{lbl}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-3 px-3 py-2.5">
|
|
|
|
|
<span class="text-xs text-(--color-muted) w-16 shrink-0">Spacing</span>
|
|
|
|
|
<div class="flex gap-1.5 flex-1">
|
|
|
|
|
|