Handle mobile viewer
This commit is contained in:
@@ -1,8 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { catalog } from '$lib/catalog';
|
import { catalog } from '$lib/catalog';
|
||||||
import { Panzoom, toggleFullScreen, zoomDefault } from '$lib/panzoom';
|
import {
|
||||||
|
Panzoom,
|
||||||
|
panzoomStore,
|
||||||
|
toggleFullScreen,
|
||||||
|
zoomDefault,
|
||||||
|
zoomFitToScreen
|
||||||
|
} from '$lib/panzoom';
|
||||||
import { progress, settings, updateProgress } from '$lib/settings';
|
import { progress, settings, updateProgress } from '$lib/settings';
|
||||||
import { clamp } from '$lib/util';
|
import { clamp, debounce } from '$lib/util';
|
||||||
import { Input, Popover, Range, Spinner } from 'flowbite-svelte';
|
import { Input, Popover, Range, Spinner } from 'flowbite-svelte';
|
||||||
import MangaPage from './MangaPage.svelte';
|
import MangaPage from './MangaPage.svelte';
|
||||||
import {
|
import {
|
||||||
@@ -181,9 +187,64 @@
|
|||||||
|
|
||||||
return maxCharCount;
|
return maxCharCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
|
||||||
|
function handleTouchStart(event: TouchEvent) {
|
||||||
|
if ($settings.mobile) {
|
||||||
|
const { clientX, clientY } = event.touches[0];
|
||||||
|
|
||||||
|
startX = clientX;
|
||||||
|
startY = clientY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(event: TouchEvent) {
|
||||||
|
if ($settings.mobile) {
|
||||||
|
debounce(() => {
|
||||||
|
if (event.touches.length === 0) {
|
||||||
|
const { clientX, clientY } = event.changedTouches[0];
|
||||||
|
|
||||||
|
const distanceX = clientX - startX;
|
||||||
|
const distanceY = clientY - startY;
|
||||||
|
|
||||||
|
const isSwipe = distanceY < 200 && distanceY > 200 * -1;
|
||||||
|
|
||||||
|
if (isSwipe) {
|
||||||
|
const swipeThreshold = Math.abs(($settings.swipeThreshold / 100) * window.innerWidth);
|
||||||
|
|
||||||
|
if (distanceX > swipeThreshold) {
|
||||||
|
left(event, true);
|
||||||
|
} else if (distanceX < swipeThreshold * -1) {
|
||||||
|
right(event, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDoubleTap(event: MouseEvent) {
|
||||||
|
if ($panzoomStore && $settings.mobile) {
|
||||||
|
const { clientX, clientY } = event;
|
||||||
|
const { scale } = $panzoomStore.getTransform();
|
||||||
|
|
||||||
|
if (scale < 0.5) {
|
||||||
|
$panzoomStore.zoomTo(clientX, clientY, 3);
|
||||||
|
} else {
|
||||||
|
zoomFitToScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:resize={zoomDefault} on:keyup|preventDefault={handleShortcuts} />
|
<svelte:window
|
||||||
|
on:resize={zoomDefault}
|
||||||
|
on:keyup|preventDefault={handleShortcuts}
|
||||||
|
on:touchstart={handleTouchStart}
|
||||||
|
on:touchend={handlePointerUp}
|
||||||
|
/>
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{volume?.mokuroData.volume || 'Volume'}</title>
|
<title>{volume?.mokuroData.volume || 'Volume'}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
@@ -227,7 +288,6 @@
|
|||||||
max={pages.length}
|
max={pages.length}
|
||||||
bind:value={manualPage}
|
bind:value={manualPage}
|
||||||
on:change={onManualPageChange}
|
on:change={onManualPageChange}
|
||||||
cla
|
|
||||||
defaultClass=""
|
defaultClass=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,8 +299,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="flex" style:background-color={$settings.backgroundColor}>
|
<div class="flex" style:background-color={$settings.backgroundColor}>
|
||||||
<Panzoom>
|
<Panzoom>
|
||||||
|
{#if !$settings.mobile}
|
||||||
<button
|
<button
|
||||||
class="h-full fixed -left-1/2 z-10 w-1/2 hover:bg-slate-400 opacity-[0.01] justify-items-center"
|
class="h-full fixed -left-1/2 z-10 w-1/2 hover:bg-slate-400 opacity-[0.01]"
|
||||||
on:mousedown={mouseDown}
|
on:mousedown={mouseDown}
|
||||||
on:mouseup={left}
|
on:mouseup={left}
|
||||||
/>
|
/>
|
||||||
@@ -249,7 +310,24 @@
|
|||||||
on:mousedown={mouseDown}
|
on:mousedown={mouseDown}
|
||||||
on:mouseup={right}
|
on:mouseup={right}
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-row" class:flex-row-reverse={!$settings.rightToLeft}>
|
{:else}
|
||||||
|
<button
|
||||||
|
class="h-screen fixed top-full left-0 z-10 w-1/2 hover:bg-slate-400 opacity-[0.01]"
|
||||||
|
on:mousedown={mouseDown}
|
||||||
|
on:mouseup={left}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="h-screen fixed top-full right-0 z-10 w-1/2 hover:bg-slate-400 opacity-[0.01]"
|
||||||
|
on:mousedown={mouseDown}
|
||||||
|
on:mouseup={right}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="flex flex-row"
|
||||||
|
class:flex-row-reverse={!$settings.rightToLeft}
|
||||||
|
on:dblclick={onDoubleTap}
|
||||||
|
role="none"
|
||||||
|
>
|
||||||
{#if showSecondPage()}
|
{#if showSecondPage()}
|
||||||
<MangaPage page={pages[index + 1]} src={Object.values(volume?.files)[index + 1]} />
|
<MangaPage page={pages[index + 1]} src={Object.values(volume?.files)[index + 1]} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -257,16 +335,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</Panzoom>
|
</Panzoom>
|
||||||
</div>
|
</div>
|
||||||
|
{#if !$settings.mobile}
|
||||||
<button
|
<button
|
||||||
on:mousedown={mouseDown}
|
on:mousedown={mouseDown}
|
||||||
on:mouseup={left}
|
on:mouseup={left}
|
||||||
class="left-0 top-0 absolute h-full w-10 hover:bg-slate-400 opacity-[0.01]"
|
class="left-0 top-0 absolute h-full w-16 hover:bg-slate-400 opacity-[0.01]"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
on:mousedown={mouseDown}
|
on:mousedown={mouseDown}
|
||||||
on:mouseup={right}
|
on:mouseup={right}
|
||||||
class="right-0 top-0 absolute h-full w-10 hover:bg-slate-400 opacity-[0.01]"
|
class="right-0 top-0 absolute h-full w-16 hover:bg-slate-400 opacity-[0.01]"
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="fixed z-50 left-1/2 top-1/2">
|
<div class="fixed z-50 left-1/2 top-1/2">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each textBoxes as { fontSize, height, left, lines, top, width, writingMode }, index (`text-box-${index}`)}
|
{#each textBoxes as { fontSize, height, left, lines, top, width, writingMode }, index (`text-box-${index}`)}
|
||||||
<button
|
<div
|
||||||
class="text-box"
|
class="text-box"
|
||||||
style:width
|
style:width
|
||||||
style:height
|
style:height
|
||||||
@@ -65,15 +65,15 @@
|
|||||||
style:font-weight={fontWeight}
|
style:font-weight={fontWeight}
|
||||||
style:display
|
style:display
|
||||||
style:border
|
style:border
|
||||||
|
style:writing-mode={writingMode}
|
||||||
|
role="none"
|
||||||
on:dblclick={() => onUpdateCard(lines)}
|
on:dblclick={() => onUpdateCard(lines)}
|
||||||
{contenteditable}
|
{contenteditable}
|
||||||
>
|
>
|
||||||
<div style:writing-mode={writingMode}>
|
|
||||||
{#each lines as line}
|
{#each lines as line}
|
||||||
<p>{line}</p>
|
<p>{line}</p>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -85,6 +85,7 @@
|
|||||||
font-size: 16pt;
|
font-size: 16pt;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 1px solid rgba(0, 0, 0, 0);
|
border: 1px solid rgba(0, 0, 0, 0);
|
||||||
|
z-index: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-box:focus,
|
.text-box:focus,
|
||||||
@@ -101,6 +102,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: rgb(255, 255, 255);
|
background-color: rgb(255, 255, 255);
|
||||||
font-weight: var(--bold);
|
font-weight: var(--bold);
|
||||||
|
z-index: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-box:focus p,
|
.text-box:focus p,
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AccordionItem } from 'flowbite-svelte';
|
import { AccordionItem, Label, Range } from 'flowbite-svelte';
|
||||||
import ReaderSelects from './ReaderSelects.svelte';
|
import ReaderSelects from './ReaderSelects.svelte';
|
||||||
import ReaderToggles from './ReaderToggles.svelte';
|
import ReaderToggles from './ReaderToggles.svelte';
|
||||||
import { isReader } from '$lib/util';
|
import { isReader } from '$lib/util';
|
||||||
|
import { settings, updateSetting } from '$lib/settings';
|
||||||
|
|
||||||
|
let value = $settings.swipeThreshold;
|
||||||
|
function onChange() {
|
||||||
|
updateSetting('swipeThreshold', value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AccordionItem open={isReader()}>
|
<AccordionItem open={isReader()}>
|
||||||
@@ -11,5 +17,9 @@
|
|||||||
<ReaderSelects />
|
<ReaderSelects />
|
||||||
<hr class="border-gray-100 opacity-10" />
|
<hr class="border-gray-100 opacity-10" />
|
||||||
<ReaderToggles />
|
<ReaderToggles />
|
||||||
|
<div>
|
||||||
|
<Label>Swipe threshold</Label>
|
||||||
|
<Range on:change={onChange} min={20} max={90} disabled={!$settings.mobile} bind:value />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -167,14 +167,23 @@ export function keepInBounds() {
|
|||||||
let minY = innerHeight - height - marginY;
|
let minY = innerHeight - height - marginY;
|
||||||
let maxY = marginY;
|
let maxY = marginY;
|
||||||
|
|
||||||
|
let forceCenterY = false;
|
||||||
|
|
||||||
if (width + 2 * marginX <= innerWidth) {
|
if (width + 2 * marginX <= innerWidth) {
|
||||||
minX = marginX;
|
minX = marginX;
|
||||||
maxX = innerWidth - width - marginX;
|
maxX = innerWidth - width - marginX;
|
||||||
|
} else {
|
||||||
|
minX = innerWidth - width - marginX;
|
||||||
|
maxX = marginX;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height + 2 * marginY <= innerHeight) {
|
if (height + 2 * marginY <= innerHeight) {
|
||||||
minY = marginY;
|
minY = marginY;
|
||||||
maxY = innerHeight - height - marginY;
|
maxY = innerHeight - height - marginY;
|
||||||
|
forceCenterY = true;
|
||||||
|
} else {
|
||||||
|
minY = innerHeight - height - marginY;
|
||||||
|
maxY = marginY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (x < minX) {
|
if (x < minX) {
|
||||||
@@ -185,6 +194,9 @@ export function keepInBounds() {
|
|||||||
transform.x = maxX;
|
transform.x = maxX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (forceCenterY) {
|
||||||
|
transform.y = innerHeight / 2 - height / 2;
|
||||||
|
} else {
|
||||||
if (y < minY) {
|
if (y < minY) {
|
||||||
transform.y = minY;
|
transform.y = minY;
|
||||||
}
|
}
|
||||||
@@ -192,6 +204,7 @@ export function keepInBounds() {
|
|||||||
transform.y = maxY;
|
transform.y = maxY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function toggleFullScreen() {
|
export function toggleFullScreen() {
|
||||||
if (!document.fullscreenElement) {
|
if (!document.fullscreenElement) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type Settings = {
|
|||||||
hasCover: boolean;
|
hasCover: boolean;
|
||||||
mobile: boolean;
|
mobile: boolean;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
|
swipeThreshold: number;
|
||||||
fontSize: FontSize;
|
fontSize: FontSize;
|
||||||
zoomDefault: ZoomModes;
|
zoomDefault: ZoomModes;
|
||||||
ankiConnectSettings: AnkiConnectSettings;
|
ankiConnectSettings: AnkiConnectSettings;
|
||||||
@@ -67,6 +68,7 @@ const defaultSettings: Settings = {
|
|||||||
charCount: false,
|
charCount: false,
|
||||||
mobile: false,
|
mobile: false,
|
||||||
backgroundColor: '#030712',
|
backgroundColor: '#030712',
|
||||||
|
swipeThreshold: 50,
|
||||||
fontSize: 'auto',
|
fontSize: 'auto',
|
||||||
zoomDefault: 'zoomFitToScreen',
|
zoomDefault: 'zoomFitToScreen',
|
||||||
ankiConnectSettings: {
|
ankiConnectSettings: {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export async function unzipManga(file: File) {
|
|||||||
function getDetails(file: File) {
|
function getDetails(file: File) {
|
||||||
const { webkitRelativePath, name } = file
|
const { webkitRelativePath, name } = file
|
||||||
const split = name.split('.');
|
const split = name.split('.');
|
||||||
const ext = split.pop()
|
const ext = split.pop();
|
||||||
const filename = split.join('.');
|
const filename = split.join('.');
|
||||||
let path = filename
|
let path = filename
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ export async function processFiles(files: File[]) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zipTypes.includes(ext)) {
|
if (ext && zipTypes.includes(ext)) {
|
||||||
const unzippedFiles = await unzipManga(file);
|
const unzippedFiles = await unzipManga(file);
|
||||||
|
|
||||||
volumes[path] = {
|
volumes[path] = {
|
||||||
|
|||||||
@@ -8,3 +8,18 @@ export function clamp(num: number, min: number, max: number) {
|
|||||||
export function isReader() {
|
export function isReader() {
|
||||||
return get(page).route.id === '/[manga]/[volume]'
|
return get(page).route.id === '/[manga]/[volume]'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let timer: any;
|
||||||
|
|
||||||
|
export function debounce(func: () => void, timeout = 50) {
|
||||||
|
if (!timer) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
func();
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = undefined;
|
||||||
|
}, timeout);
|
||||||
|
} else {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user