summaryrefslogtreecommitdiffstatshomepage
path: root/frontend/src/lib/reader
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib/reader')
-rw-r--r--frontend/src/lib/reader/PageView.svelte105
-rw-r--r--frontend/src/lib/reader/Reader.svelte45
-rw-r--r--frontend/src/lib/reader/ReaderPage.svelte43
-rw-r--r--frontend/src/lib/reader/components/CloseReaderButton.svelte17
-rw-r--r--frontend/src/lib/reader/components/PageIndicator.svelte9
-rw-r--r--frontend/src/lib/reader/components/ReaderMenuButton.svelte13
-rw-r--r--frontend/src/lib/reader/components/SliderMargin.svelte11
-rw-r--r--frontend/src/lib/reader/components/SliderTooltip.svelte17
-rw-r--r--frontend/src/lib/reader/components/ToggleFullscreenButton.svelte34
9 files changed, 195 insertions, 99 deletions
diff --git a/frontend/src/lib/reader/PageView.svelte b/frontend/src/lib/reader/PageView.svelte
index 08764b7..2b61a78 100644
--- a/frontend/src/lib/reader/PageView.svelte
+++ b/frontend/src/lib/reader/PageView.svelte
@@ -1,45 +1,45 @@
<script lang="ts">
import { Direction, Layout, type PageFragment } from '$gql/graphql';
- import { getReaderContext, partition, type Chunk } from '$lib/Reader';
+ import { type Chunk, getReaderContext, partition } from '$lib/Reader.svelte';
import { binds } from '$lib/Shortcuts';
import { src } from '$lib/Utils';
import ReaderPage from './ReaderPage.svelte';
+ import SliderMargin from './components/SliderMargin.svelte';
+ import SliderTooltip from './components/SliderTooltip.svelte';
const reader = getReaderContext();
- export let direction: Direction;
- export let layout: Layout;
-
- let chunks: Chunk[] = [];
- let lookup: number[] = [];
-
- let main: PageFragment;
- let secondary: PageFragment | undefined;
+ let { direction, layout }: { direction: Direction; layout: Layout } = $props();
function gotoChunk(to: number) {
if (to < 0 || to >= chunks.length) return;
- $reader.page = chunks[to].index;
+ reader.page = chunks[to].index;
}
function pagesAround(around: number) {
- const peek = (at: number) => {
- if (at < 0 || at >= chunks.length) return [];
+ const pages: PageFragment[] = [];
- const pages = [chunks[at].main];
+ const push = (at: number) => {
+ if (at < 0 || at >= chunks.length) return;
+
+ pages.push(chunks[at].main);
if (chunks[at].secondary) {
pages.push(chunks[at].secondary);
}
-
- return pages;
};
- return [...peek(lookup[around] + 1), ...peek(lookup[around] - 1)];
+ for (let i = 1; i <= 2; i++) {
+ push(lookup[around] + i);
+ push(lookup[around] - i);
+ }
+
+ return pages;
}
- const next = () => gotoChunk(lookup[$reader.page] + 1);
- const prev = () => gotoChunk(lookup[$reader.page] - 1);
+ const next = () => gotoChunk(lookup[reader.page] + 1);
+ const prev = () => gotoChunk(lookup[reader.page] - 1);
const clickLeft = () => (direction === Direction.LeftToRight ? prev() : next());
const clickRight = () => (direction === Direction.RightToLeft ? prev() : next());
@@ -56,8 +56,11 @@
}
}
- $: [chunks, lookup] = partition($reader.pages, layout);
- $: layout, ({ main, secondary } = chunks[lookup[$reader.page]]);
+ let [chunks, lookup] = $derived(partition(reader.pages, layout));
+ let currentChunk = $derived(chunks[lookup[reader.page]]);
+ let { main, secondary } = $derived(currentChunk);
+
+ let reverse = $derived(direction === Direction.RightToLeft);
</script>
<svelte:document
@@ -76,16 +79,64 @@
/>
{#if !secondary}
- <ReaderPage page={main} on:click={clickMain} --justify="center" />
-{:else if direction === Direction.LeftToRight}
- <ReaderPage page={main} on:click={prev} --justify="flex-end" />
- <ReaderPage page={secondary} on:click={next} --justify="flex-start" />
+ <ReaderPage page={main} onclick={clickMain} --justify="center" />
+{:else if reverse}
+ <ReaderPage page={secondary} onclick={next} --justify="flex-end" />
+ <ReaderPage page={main} onclick={prev} --justify="flex-start" />
{:else}
- <ReaderPage page={secondary} on:click={next} --justify="flex-end" />
- <ReaderPage page={main} on:click={prev} --justify="flex-start" />
+ <ReaderPage page={main} onclick={prev} --justify="flex-end" />
+ <ReaderPage page={secondary} onclick={next} --justify="flex-start" />
{/if}
+
+{#snippet pagesIn(chunk: Chunk)}
+ {#if chunk.secondary}
+ {chunk.index + 1} - {chunk.index + 2}
+ {:else}
+ {chunk.index + 1}
+ {/if}
+{/snippet}
+
+<div class="group/slider absolute bottom-0 z-1 flex w-full pt-20">
+ <div class:reverse class="flex h-1 w-full transition-[height] group-hover/slider:h-8">
+ <SliderMargin>
+ {@render pagesIn(currentChunk)}
+ </SliderMargin>
+ <div class:reverse class="flex w-full bg-gray-400/60 backdrop-blur-2xl">
+ <!-- eslint-disable-next-line svelte/require-each-key -->
+ {#each chunks as chunk, index}
+ <button
+ type="button"
+ class:read={index <= lookup[reader.page]}
+ class="group/page relative grow"
+ onclick={() => reader.open(chunk.index)}
+ aria-label={`Open page ${chunk.index + 1}`}
+ >
+ <SliderTooltip>
+ {@render pagesIn(chunk)}
+ </SliderTooltip>
+ </button>
+ {/each}
+ </div>
+ <SliderMargin>
+ {reader.pages.length}
+ </SliderMargin>
+ </div>
+</div>
+
<div class="invisible absolute">
- {#each pagesAround($reader.page) as page}
+ {#each pagesAround(reader.page) as page (page.id)}
<img src={src(page.image, 'full')} alt="" />
{/each}
</div>
+
+<style lang="postcss">
+ @reference "tailwindcss/theme";
+
+ .reverse {
+ flex-direction: row-reverse;
+ }
+
+ button.read {
+ @apply bg-blue-600/60;
+ }
+</style>
diff --git a/frontend/src/lib/reader/Reader.svelte b/frontend/src/lib/reader/Reader.svelte
index 15ebdf4..a720a77 100644
--- a/frontend/src/lib/reader/Reader.svelte
+++ b/frontend/src/lib/reader/Reader.svelte
@@ -1,43 +1,52 @@
<script lang="ts">
import { trapFocus } from '$lib/Actions';
- import { getReaderContext } from '$lib/Reader';
+ import { getReaderContext } from '$lib/Reader.svelte';
import { fadeDefault, slideXDefault } from '$lib/Transitions';
+ import type { Snippet } from 'svelte';
import { fade, slide } from 'svelte/transition';
import CloseReaderButton from './components/CloseReaderButton.svelte';
- import PageIndicator from './components/PageIndicator.svelte';
import ReaderMenuButton from './components/ReaderMenuButton.svelte';
+ import ToggleFullscreenButton from './components/ToggleFullscreenButton.svelte';
+
+ let { sidebar, children }: { sidebar?: Snippet; children?: Snippet } = $props();
const reader = getReaderContext();
+
+ let dialog: HTMLDivElement | undefined = $state();
</script>
-{#if $reader.visible}
+{#if reader.visible}
<div
role="dialog"
- class="fixed bottom-0 left-0 right-0 top-0 z-10 flex h-full w-full bg-black"
+ class="fixed top-0 right-0 bottom-0 left-0 z-10 flex h-full w-full bg-black"
transition:fade={fadeDefault}
use:trapFocus
+ bind:this={dialog}
>
- {#if $$slots.sidebar && $reader.sidebar}
- <aside class="w-[36rem] shrink-0 bg-slate-800" transition:slide={slideXDefault}>
+ {#if sidebar && reader.sidebar}
+ <aside
+ class="z-10 w-[36rem] shrink-0 bg-slate-800 shadow-md shadow-slate-800"
+ transition:slide={slideXDefault}
+ >
<div class="flex h-full min-w-[36rem] flex-col gap-4 overflow-auto p-4">
- <slot name="sidebar" />
+ {@render sidebar?.()}
</div>
</aside>
{/if}
<main class="relative flex grow">
- <div class="absolute z-10 flex w-full p-1 text-lg [&>*:last-child]:ml-auto">
- {#if $$slots.sidebar}
- <ReaderMenuButton />
- {/if}
- <CloseReaderButton />
- </div>
- <div class="absolute bottom-0 right-0 z-10 flex p-1 text-lg">
- <PageIndicator />
+ <div class="absolute flex w-full p-1 text-lg">
+ <div class="flex flex-col gap-1">
+ {#if sidebar}
+ <ReaderMenuButton />
+ {/if}
+ </div>
+ <div class="ml-auto flex flex-col gap-1">
+ <CloseReaderButton />
+ <ToggleFullscreenButton {dialog} />
+ </div>
</div>
- <div class="flex grow">
- <slot />
- </div>
+ {@render children?.()}
</main>
</div>
{/if}
diff --git a/frontend/src/lib/reader/ReaderPage.svelte b/frontend/src/lib/reader/ReaderPage.svelte
index c86414d..4a19c6e 100644
--- a/frontend/src/lib/reader/ReaderPage.svelte
+++ b/frontend/src/lib/reader/ReaderPage.svelte
@@ -1,48 +1,25 @@
<script lang="ts">
import type { PageFragment } from '$gql/graphql';
- import Spinner from '$lib/components/Spinner.svelte';
import { src } from '$lib/Utils';
- import { onDestroy } from 'svelte';
+ import type { MouseEventHandler } from 'svelte/elements';
- export let page: PageFragment;
-
- let loading = false;
- let loadingTimeout: NodeJS.Timeout;
- let lastId = -1;
-
- $: page.id, updateLoadingState();
-
- function updateLoadingState() {
- if (page.id === lastId) return;
- lastId = page.id;
-
- loadingTimeout = setTimeout(() => (loading = true), 150);
- }
-
- function finishLoading() {
- clearTimeout(loadingTimeout);
- loading = false;
+ interface Props {
+ page: PageFragment;
+ onclick: MouseEventHandler<HTMLDivElement>;
}
- onDestroy(() => clearTimeout(loadingTimeout));
+ let { page, onclick }: Props = $props();
</script>
-<!-- svelte-ignore a11y-click-events-have-key-events -->
-<!-- svelte-ignore a11y-no-static-element-interactions -->
-<div class="relative flex grow" on:click>
- <div class="absolute right-0 top-0 z-0 h-full w-full">
- {#if loading}
- <Spinner />
- {/if}
- </div>
+<!-- svelte-ignore a11y_click_events_have_key_events -->
+<!-- svelte-ignore a11y_no_static_element_interactions -->
+<div class="flex w-full" {onclick}>
<img
- class="h-auto w-auto object-contain transition-opacity duration-200"
- class:opacity-0={loading}
+ class="h-auto w-auto object-contain"
width={page.image.width}
height={page.image.height}
src={src(page.image, 'full')}
- alt=""
- on:load={finishLoading}
+ alt={page.path}
/>
</div>
diff --git a/frontend/src/lib/reader/components/CloseReaderButton.svelte b/frontend/src/lib/reader/components/CloseReaderButton.svelte
index 0c88323..6a31fd2 100644
--- a/frontend/src/lib/reader/components/CloseReaderButton.svelte
+++ b/frontend/src/lib/reader/components/CloseReaderButton.svelte
@@ -1,19 +1,22 @@
<script lang="ts">
- import { getReaderContext } from '$lib/Reader';
+ import { getReaderContext } from '$lib/Reader.svelte';
import { accelerator } from '$lib/Shortcuts';
const reader = getReaderContext();
+
+ function onclick() {
+ reader.visible = false;
+ reader.sidebar = false;
+ }
</script>
<button
type="button"
- class="btn floating"
+ class="btn-transparent"
title="Close reader"
- on:click={() => {
- $reader.visible = false;
- $reader.sidebar = false;
- }}
+ aria-label="Close reader"
+ {onclick}
use:accelerator={'Escape'}
>
- <span class="icon-lg icon-[material-symbols--close]" />
+ <span class="icon-lg icon-[material-symbols--close]"></span>
</button>
diff --git a/frontend/src/lib/reader/components/PageIndicator.svelte b/frontend/src/lib/reader/components/PageIndicator.svelte
deleted file mode 100644
index f79fc00..0000000
--- a/frontend/src/lib/reader/components/PageIndicator.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-<script lang="ts">
- import { getReaderContext } from '$lib/Reader';
-
- const reader = getReaderContext();
-</script>
-
-<div class="floating !p-2">
- {$reader.page + 1}/{$reader.pages.length}
-</div>
diff --git a/frontend/src/lib/reader/components/ReaderMenuButton.svelte b/frontend/src/lib/reader/components/ReaderMenuButton.svelte
index aa20206..924342f 100644
--- a/frontend/src/lib/reader/components/ReaderMenuButton.svelte
+++ b/frontend/src/lib/reader/components/ReaderMenuButton.svelte
@@ -1,16 +1,19 @@
<script lang="ts">
- import { getReaderContext } from '$lib/Reader';
+ import { getReaderContext } from '$lib/Reader.svelte';
import { accelerator } from '$lib/Shortcuts';
const reader = getReaderContext();
+
+ let title = $derived(`${reader.sidebar ? 'Hide' : 'Show'} menu`);
</script>
<button
type="button"
- class="btn floating invisible xl:visible"
- title={`${$reader.sidebar ? 'Hide' : 'Show'} menu`}
- on:click={() => ($reader.sidebar = !$reader.sidebar)}
+ class="btn-transparent hidden xl:flex"
+ {title}
+ aria-label={title}
+ onclick={() => (reader.sidebar = !reader.sidebar)}
use:accelerator={'z'}
>
- <span class="icon-lg icon-[material-symbols--dock-to-right]" />
+ <span class="icon-lg icon-[material-symbols--dock-to-right]"></span>
</button>
diff --git a/frontend/src/lib/reader/components/SliderMargin.svelte b/frontend/src/lib/reader/components/SliderMargin.svelte
new file mode 100644
index 0000000..c2f9a55
--- /dev/null
+++ b/frontend/src/lib/reader/components/SliderMargin.svelte
@@ -0,0 +1,11 @@
+<script lang="ts">
+ import type { Snippet } from 'svelte';
+
+ let { children }: { children: Snippet } = $props();
+</script>
+
+<div
+ class="flex h-full w-22 items-center justify-center px-1 font-semibold text-white/0 transition-colors group-hover/slider:bg-black group-hover/slider:text-white"
+>
+ {@render children()}
+</div>
diff --git a/frontend/src/lib/reader/components/SliderTooltip.svelte b/frontend/src/lib/reader/components/SliderTooltip.svelte
new file mode 100644
index 0000000..9e0322d
--- /dev/null
+++ b/frontend/src/lib/reader/components/SliderTooltip.svelte
@@ -0,0 +1,17 @@
+<script lang="ts">
+ import type { Snippet } from 'svelte';
+
+ let { children }: { children: Snippet } = $props();
+</script>
+
+<div
+ class="invisible absolute bottom-10 w-20 rounded-xl bg-blue-500 p-1 font-semibold text-white drop-shadow-md group-hover/page:visible"
+>
+ {@render children()}
+</div>
+
+<style lang="postcss">
+ div {
+ left: calc(50% - 2.5rem);
+ }
+</style>
diff --git a/frontend/src/lib/reader/components/ToggleFullscreenButton.svelte b/frontend/src/lib/reader/components/ToggleFullscreenButton.svelte
new file mode 100644
index 0000000..9ad4ce6
--- /dev/null
+++ b/frontend/src/lib/reader/components/ToggleFullscreenButton.svelte
@@ -0,0 +1,34 @@
+<script lang="ts">
+ import { accelerator } from '$lib/Shortcuts';
+ import { toastFinally } from '$lib/Toasts';
+
+ let { dialog }: { dialog?: HTMLElement } = $props();
+
+ function onclick() {
+ if (isFullscreen) {
+ document.exitFullscreen().catch(toastFinally);
+ } else if (dialog?.requestFullscreen) {
+ dialog.requestFullscreen().catch(toastFinally);
+ }
+ }
+
+ let fullscreenElement: HTMLElement | null = $state(null);
+ let isFullscreen = $derived(fullscreenElement !== null);
+</script>
+
+<svelte:document bind:fullscreenElement />
+
+<button
+ type="button"
+ class="btn-transparent"
+ title="Toggle fullscreen"
+ aria-label="Toggle fullscreen"
+ {onclick}
+ use:accelerator={'f'}
+>
+ {#if isFullscreen}
+ <span class="icon-lg icon-[material-symbols--fullscreen-exit]"></span>
+ {:else}
+ <span class="icon-lg icon-[material-symbols--fullscreen]"></span>
+ {/if}
+</button>