merge: feature/34-reader-zoom dans develop (option de zoom du lecteur PDF/CBZ)
This commit is contained in:
@ -5,7 +5,8 @@ import { CbzReader } from "../reader/CbzReader";
|
|||||||
import { EpubReader } from "../reader/EpubReader";
|
import { EpubReader } from "../reader/EpubReader";
|
||||||
import { pageLocator, parseCbrPageLocator, parseCbzPageLocator, parsePdfPageLocator, pdfPagePercent } from "../reader/locators";
|
import { pageLocator, parseCbrPageLocator, parseCbzPageLocator, parsePdfPageLocator, pdfPagePercent } from "../reader/locators";
|
||||||
import { PdfReader } from "../reader/PdfReader";
|
import { PdfReader } from "../reader/PdfReader";
|
||||||
import { ReaderShell, type ReaderControls } from "../reader/ReaderShell";
|
import { ReaderShell, type ReaderControls, type ReaderZoomControls } from "../reader/ReaderShell";
|
||||||
|
import { clampReaderZoom, READER_ZOOM_DEFAULT, READER_ZOOM_STEP } from "../reader/readerLayout";
|
||||||
import { useReaderProgress } from "../reader/useReaderProgress";
|
import { useReaderProgress } from "../reader/useReaderProgress";
|
||||||
|
|
||||||
const idleControls: ReaderControls = {
|
const idleControls: ReaderControls = {
|
||||||
@ -21,6 +22,7 @@ export function ReaderPage({ bookId }: { bookId: number }) {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
const [zoom, setZoom] = useState(READER_ZOOM_DEFAULT);
|
||||||
const [readerControls, setReaderControls] = useState<ReaderControls>(idleControls);
|
const [readerControls, setReaderControls] = useState<ReaderControls>(idleControls);
|
||||||
const { progress, error: progressError, save, queueSave } = useReaderProgress(bookId);
|
const { progress, error: progressError, save, queueSave } = useReaderProgress(bookId);
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ export function ReaderPage({ bookId }: { bookId: number }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setReaderControls(idleControls);
|
setReaderControls(idleControls);
|
||||||
|
setZoom(READER_ZOOM_DEFAULT);
|
||||||
}, [bookId]);
|
}, [bookId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -77,6 +80,34 @@ export function ReaderPage({ bookId }: { bookId: number }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const readerError = error ?? progressError;
|
const readerError = error ?? progressError;
|
||||||
|
const supportsZoom = book?.format === "pdf" || book?.format === "cbz" || book?.format === "cbr";
|
||||||
|
const changeZoom = useCallback((nextZoom: number | ((currentZoom: number) => number)) => {
|
||||||
|
const stage = document.querySelector(".reader-stage") as HTMLElement | null;
|
||||||
|
const scrollRatioX = stage && stage.scrollWidth > stage.clientWidth ? (stage.scrollLeft + stage.clientWidth / 2) / stage.scrollWidth : 0.5;
|
||||||
|
const scrollRatioY = stage && stage.scrollHeight > stage.clientHeight ? (stage.scrollTop + stage.clientHeight / 2) / stage.scrollHeight : 0.5;
|
||||||
|
|
||||||
|
setZoom((currentZoom) => clampReaderZoom(typeof nextZoom === "function" ? nextZoom(currentZoom) : nextZoom));
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!stage) return;
|
||||||
|
stage.scrollLeft = Math.max(0, stage.scrollWidth * scrollRatioX - stage.clientWidth / 2);
|
||||||
|
stage.scrollTop = Math.max(0, stage.scrollHeight * scrollRatioY - stage.clientHeight / 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const zoomControls = useMemo<ReaderZoomControls | undefined>(
|
||||||
|
() =>
|
||||||
|
supportsZoom
|
||||||
|
? {
|
||||||
|
zoom,
|
||||||
|
onZoomOut: () => changeZoom((currentZoom) => currentZoom - READER_ZOOM_STEP),
|
||||||
|
onZoomIn: () => changeZoom((currentZoom) => currentZoom + READER_ZOOM_STEP),
|
||||||
|
onZoomReset: () => changeZoom(READER_ZOOM_DEFAULT)
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
[changeZoom, supportsZoom, zoom]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReaderShell
|
<ReaderShell
|
||||||
@ -85,6 +116,7 @@ export function ReaderPage({ bookId }: { bookId: number }) {
|
|||||||
error={readerError}
|
error={readerError}
|
||||||
onRetry={error ? () => void loadBook() : undefined}
|
onRetry={error ? () => void loadBook() : undefined}
|
||||||
controls={readerControls}
|
controls={readerControls}
|
||||||
|
zoomControls={zoomControls}
|
||||||
>
|
>
|
||||||
{!book ? (
|
{!book ? (
|
||||||
<div className="reader-fallback">
|
<div className="reader-fallback">
|
||||||
@ -98,6 +130,7 @@ export function ReaderPage({ bookId }: { bookId: number }) {
|
|||||||
url={fileUrl}
|
url={fileUrl}
|
||||||
page={page}
|
page={page}
|
||||||
backHref={backHref}
|
backHref={backHref}
|
||||||
|
zoom={zoom}
|
||||||
onPageCommit={savePdfPage}
|
onPageCommit={savePdfPage}
|
||||||
onControlsChange={setReaderControls}
|
onControlsChange={setReaderControls}
|
||||||
/>
|
/>
|
||||||
@ -105,6 +138,7 @@ export function ReaderPage({ bookId }: { bookId: number }) {
|
|||||||
<CbzReader
|
<CbzReader
|
||||||
bookId={book.id}
|
bookId={book.id}
|
||||||
page={page}
|
page={page}
|
||||||
|
zoom={zoom}
|
||||||
onPageCommit={saveComicPage}
|
onPageCommit={saveComicPage}
|
||||||
onControlsChange={setReaderControls}
|
onControlsChange={setReaderControls}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { api } from "../api/client";
|
import { api } from "../api/client";
|
||||||
import type { CbzPagesDto } from "../api/types";
|
import type { CbzPagesDto } from "../api/types";
|
||||||
import { clampReaderPage, orientedContainPageSize, readableViewportSize, readerPositionLabel, type ReaderSize } from "./readerLayout";
|
import { clampReaderPage, orientedContainPageSize, readableViewportSize, readerPositionLabel, zoomReaderSize, type ReaderSize } from "./readerLayout";
|
||||||
import type { ReaderControls } from "./ReaderShell";
|
import type { ReaderControls } from "./ReaderShell";
|
||||||
|
|
||||||
type PageCommitStrategy = "immediate" | "queued";
|
type PageCommitStrategy = "immediate" | "queued";
|
||||||
@ -9,11 +9,13 @@ type PageCommitStrategy = "immediate" | "queued";
|
|||||||
export function CbzReader({
|
export function CbzReader({
|
||||||
bookId,
|
bookId,
|
||||||
page,
|
page,
|
||||||
|
zoom,
|
||||||
onPageCommit,
|
onPageCommit,
|
||||||
onControlsChange
|
onControlsChange
|
||||||
}: {
|
}: {
|
||||||
bookId: number;
|
bookId: number;
|
||||||
page: number;
|
page: number;
|
||||||
|
zoom: number;
|
||||||
onPageCommit: (page: number, pages: number, anchor?: number, strategy?: PageCommitStrategy) => void;
|
onPageCommit: (page: number, pages: number, anchor?: number, strategy?: PageCommitStrategy) => void;
|
||||||
onControlsChange: (controls: ReaderControls) => void;
|
onControlsChange: (controls: ReaderControls) => void;
|
||||||
}) {
|
}) {
|
||||||
@ -50,7 +52,7 @@ export function CbzReader({
|
|||||||
const pageCount = pages?.pageCount ?? 1;
|
const pageCount = pages?.pageCount ?? 1;
|
||||||
const currentPage = clampReaderPage(page, pageCount);
|
const currentPage = clampReaderPage(page, pageCount);
|
||||||
const currentName = pages?.pages.find((item) => item.page === currentPage)?.name;
|
const currentName = pages?.pages.find((item) => item.page === currentPage)?.name;
|
||||||
const fittedSize = viewportSize && imageSize ? orientedContainPageSize(viewportSize, imageSize) : null;
|
const fittedSize = viewportSize && imageSize ? zoomReaderSize(orientedContainPageSize(viewportSize, imageSize), zoom) : null;
|
||||||
const imageStyle = fittedSize ? { width: `${fittedSize.width}px`, height: `${fittedSize.height}px` } : undefined;
|
const imageStyle = fittedSize ? { width: `${fittedSize.width}px`, height: `${fittedSize.height}px` } : undefined;
|
||||||
|
|
||||||
const go = useCallback(
|
const go = useCallback(
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { ReaderError } from "./ReaderError";
|
|||||||
import { pdfDocumentOptions } from "./pdfDocumentOptions";
|
import { pdfDocumentOptions } from "./pdfDocumentOptions";
|
||||||
import { classifyPdfCanvas, pdfCanvasVisible, pdfRenderScale, pdfSentinelBackground, type PdfRenderResult } from "./pdfRender";
|
import { classifyPdfCanvas, pdfCanvasVisible, pdfRenderScale, pdfSentinelBackground, type PdfRenderResult } from "./pdfRender";
|
||||||
import { configurePdfWorker } from "./pdfWorker";
|
import { configurePdfWorker } from "./pdfWorker";
|
||||||
import { clampReaderPage, readableViewportSize, readerPositionLabel, type ReaderSize } from "./readerLayout";
|
import { clampReaderPage, readableViewportSize, readerPositionLabel, readerZoomFactor, type ReaderSize } from "./readerLayout";
|
||||||
import type { ReaderControls } from "./ReaderShell";
|
import type { ReaderControls } from "./ReaderShell";
|
||||||
|
|
||||||
configurePdfWorker(pdfjs);
|
configurePdfWorker(pdfjs);
|
||||||
@ -15,6 +15,7 @@ type PdfReaderProps = {
|
|||||||
url: string;
|
url: string;
|
||||||
page: number;
|
page: number;
|
||||||
backHref: string;
|
backHref: string;
|
||||||
|
zoom: number;
|
||||||
onPageCommit: (page: number, pages: number, anchor?: number, strategy?: PageCommitStrategy) => void;
|
onPageCommit: (page: number, pages: number, anchor?: number, strategy?: PageCommitStrategy) => void;
|
||||||
onControlsChange: (controls: ReaderControls) => void;
|
onControlsChange: (controls: ReaderControls) => void;
|
||||||
};
|
};
|
||||||
@ -59,7 +60,7 @@ function afterNextPaint() {
|
|||||||
return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PdfReader({ url, page, backHref, onPageCommit, onControlsChange }: PdfReaderProps) {
|
export function PdfReader({ url, page, backHref, zoom, onPageCommit, onControlsChange }: PdfReaderProps) {
|
||||||
const frameRef = useRef<HTMLDivElement>(null);
|
const frameRef = useRef<HTMLDivElement>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const [documentProxy, setDocumentProxy] = useState<pdfjs.PDFDocumentProxy | null>(null);
|
const [documentProxy, setDocumentProxy] = useState<pdfjs.PDFDocumentProxy | null>(null);
|
||||||
@ -206,7 +207,8 @@ export function PdfReader({ url, page, backHref, onPageCommit, onControlsChange
|
|||||||
const baseViewport = pdfPage.getViewport({ scale: 1 });
|
const baseViewport = pdfPage.getViewport({ scale: 1 });
|
||||||
const fitScale = pdfRenderScale(readerViewport, { width: baseViewport.width, height: baseViewport.height });
|
const fitScale = pdfRenderScale(readerViewport, { width: baseViewport.width, height: baseViewport.height });
|
||||||
const pixelRatio = Math.min(2, window.devicePixelRatio || 1);
|
const pixelRatio = Math.min(2, window.devicePixelRatio || 1);
|
||||||
const renderScale = Math.max(0.25, Math.min(3, fitScale)) * pixelRatio;
|
const zoomedScale = fitScale * readerZoomFactor(zoom);
|
||||||
|
const renderScale = Math.max(0.25, zoomedScale) * pixelRatio;
|
||||||
const viewport = pdfPage.getViewport({ scale: renderScale });
|
const viewport = pdfPage.getViewport({ scale: renderScale });
|
||||||
const cssWidth = Math.floor(viewport.width / pixelRatio);
|
const cssWidth = Math.floor(viewport.width / pixelRatio);
|
||||||
const cssHeight = Math.floor(viewport.height / pixelRatio);
|
const cssHeight = Math.floor(viewport.height / pixelRatio);
|
||||||
@ -287,7 +289,7 @@ export function PdfReader({ url, page, backHref, onPageCommit, onControlsChange
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
renderTask?.cancel();
|
renderTask?.cancel();
|
||||||
};
|
};
|
||||||
}, [currentPage, documentProxy, pageAttempt, viewportSize]);
|
}, [currentPage, documentProxy, pageAttempt, viewportSize, zoom]);
|
||||||
|
|
||||||
if (documentError) {
|
if (documentError) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { ArrowLeft, ArrowRight } from "lucide-react";
|
|||||||
import { useEffect, type ReactNode } from "react";
|
import { useEffect, type ReactNode } from "react";
|
||||||
import { ErrorRibbon } from "../components/ui";
|
import { ErrorRibbon } from "../components/ui";
|
||||||
import { navigate } from "../router";
|
import { navigate } from "../router";
|
||||||
|
import { readerZoomLabel, READER_ZOOM_DEFAULT, READER_ZOOM_MAX, READER_ZOOM_MIN } from "./readerLayout";
|
||||||
|
|
||||||
export type ReaderControls = {
|
export type ReaderControls = {
|
||||||
canPrevious: boolean;
|
canPrevious: boolean;
|
||||||
@ -11,12 +12,20 @@ export type ReaderControls = {
|
|||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ReaderZoomControls = {
|
||||||
|
zoom: number;
|
||||||
|
onZoomOut: () => void;
|
||||||
|
onZoomIn: () => void;
|
||||||
|
onZoomReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
type ReaderShellProps = {
|
type ReaderShellProps = {
|
||||||
title: string;
|
title: string;
|
||||||
backHref: string;
|
backHref: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
controls: ReaderControls;
|
controls: ReaderControls;
|
||||||
|
zoomControls?: ReaderZoomControls;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,7 +35,7 @@ function isFormField(target: EventTarget | null) {
|
|||||||
return Boolean(element?.isContentEditable || tagName === "input" || tagName === "textarea" || tagName === "select");
|
return Boolean(element?.isContentEditable || tagName === "input" || tagName === "textarea" || tagName === "select");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReaderShell({ title, backHref, error, onRetry, controls, children }: ReaderShellProps) {
|
export function ReaderShell({ title, backHref, error, onRetry, controls, zoomControls, children }: ReaderShellProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (isFormField(event.target)) return;
|
if (isFormField(event.target)) return;
|
||||||
@ -60,6 +69,20 @@ export function ReaderShell({ title, backHref, error, onRetry, controls, childre
|
|||||||
Suivant
|
Suivant
|
||||||
<ArrowRight size={16} />
|
<ArrowRight size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
{zoomControls && (
|
||||||
|
<div className="reader-zoom-controls" aria-label="Zoom du lecteur">
|
||||||
|
<button className="ghost-button reader-zoom-button" onClick={zoomControls.onZoomOut} disabled={zoomControls.zoom <= READER_ZOOM_MIN} aria-label="Reduire le zoom">
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<span className="reader-zoom-label">{readerZoomLabel(zoomControls.zoom)}</span>
|
||||||
|
<button className="ghost-button reader-zoom-button" onClick={zoomControls.onZoomIn} disabled={zoomControls.zoom >= READER_ZOOM_MAX} aria-label="Augmenter le zoom">
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<button className="ghost-button reader-zoom-reset" onClick={zoomControls.onZoomReset} disabled={zoomControls.zoom === READER_ZOOM_DEFAULT}>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="reader-status">
|
<div className="reader-status">
|
||||||
|
|||||||
@ -3,6 +3,24 @@ export type ReaderSize = {
|
|||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const READER_ZOOM_MIN = 50;
|
||||||
|
export const READER_ZOOM_MAX = 300;
|
||||||
|
export const READER_ZOOM_STEP = 25;
|
||||||
|
export const READER_ZOOM_DEFAULT = 100;
|
||||||
|
|
||||||
|
export function clampReaderZoom(zoom: number) {
|
||||||
|
const steppedZoom = Math.round(zoom / READER_ZOOM_STEP) * READER_ZOOM_STEP;
|
||||||
|
return Math.max(READER_ZOOM_MIN, Math.min(steppedZoom, READER_ZOOM_MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readerZoomLabel(zoom: number) {
|
||||||
|
return `${clampReaderZoom(zoom)} %`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readerZoomFactor(zoom: number) {
|
||||||
|
return clampReaderZoom(zoom) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
export function clampReaderPage(page: number, pageCount: number) {
|
export function clampReaderPage(page: number, pageCount: number) {
|
||||||
const safePageCount = Math.max(1, Math.floor(pageCount));
|
const safePageCount = Math.max(1, Math.floor(pageCount));
|
||||||
return Math.max(1, Math.min(Math.floor(page), safePageCount));
|
return Math.max(1, Math.min(Math.floor(page), safePageCount));
|
||||||
@ -45,6 +63,14 @@ export function orientedContainPageSize(viewport: ReaderSize, page: ReaderSize):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function zoomReaderSize(size: ReaderSize, zoom: number): ReaderSize {
|
||||||
|
const factor = readerZoomFactor(zoom);
|
||||||
|
return {
|
||||||
|
width: Math.max(1, Math.floor(size.width * factor)),
|
||||||
|
height: Math.max(1, Math.floor(size.height * factor))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function readableViewportSize(size: ReaderSize): ReaderSize | null {
|
export function readableViewportSize(size: ReaderSize): ReaderSize | null {
|
||||||
if (size.width < 32 || size.height < 32) return null;
|
if (size.width < 32 || size.height < 32) return null;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,7 +1,19 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { epubFileName } from "./EpubReader";
|
import { epubFileName } from "./EpubReader";
|
||||||
import { pdfDocumentOptions } from "./pdfDocumentOptions";
|
import { pdfDocumentOptions } from "./pdfDocumentOptions";
|
||||||
import { clampReaderPage, containPageSize, nextReaderPage, orientedContainPageSize, previousReaderPage, readableViewportSize, readerPositionLabel } from "./readerLayout";
|
import {
|
||||||
|
clampReaderPage,
|
||||||
|
clampReaderZoom,
|
||||||
|
containPageSize,
|
||||||
|
nextReaderPage,
|
||||||
|
orientedContainPageSize,
|
||||||
|
previousReaderPage,
|
||||||
|
readableViewportSize,
|
||||||
|
readerPositionLabel,
|
||||||
|
readerZoomFactor,
|
||||||
|
readerZoomLabel,
|
||||||
|
zoomReaderSize
|
||||||
|
} from "./readerLayout";
|
||||||
import { classifyPdfCanvas, pdfCanvasHasVisibleContent, pdfCanvasVisible, pdfRenderScale } from "./pdfRender";
|
import { classifyPdfCanvas, pdfCanvasHasVisibleContent, pdfCanvasVisible, pdfRenderScale } from "./pdfRender";
|
||||||
import { configurePdfWorker, pdfWorkerSrc } from "./pdfWorker";
|
import { configurePdfWorker, pdfWorkerSrc } from "./pdfWorker";
|
||||||
import { readerErrorMessage } from "./ReaderError";
|
import { readerErrorMessage } from "./ReaderError";
|
||||||
@ -118,6 +130,20 @@ describe("reader runtime helpers", () => {
|
|||||||
expect(orientedContainPageSize({ width: 500, height: 900 }, { width: 1200, height: 900 })).toEqual({ width: 500, height: 375 });
|
expect(orientedContainPageSize({ width: 500, height: 900 }, { width: 1200, height: 900 })).toEqual({ width: 500, height: 375 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clamps reader zoom to 25 percent steps between 50 and 300", () => {
|
||||||
|
expect(clampReaderZoom(20)).toBe(50);
|
||||||
|
expect(clampReaderZoom(63)).toBe(75);
|
||||||
|
expect(clampReaderZoom(186)).toBe(175);
|
||||||
|
expect(clampReaderZoom(340)).toBe(300);
|
||||||
|
expect(readerZoomLabel(125)).toBe("125 %");
|
||||||
|
expect(readerZoomFactor(175)).toBe(1.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies reader zoom above the contained media size", () => {
|
||||||
|
expect(zoomReaderSize({ width: 800, height: 600 }, 50)).toEqual({ width: 400, height: 300 });
|
||||||
|
expect(zoomReaderSize({ width: 800, height: 600 }, 200)).toEqual({ width: 1600, height: 1200 });
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores unusable reader viewport measurements", () => {
|
it("ignores unusable reader viewport measurements", () => {
|
||||||
expect(readableViewportSize({ width: 0, height: 833 })).toBeNull();
|
expect(readableViewportSize({ width: 0, height: 833 })).toBeNull();
|
||||||
expect(readableViewportSize({ width: 1, height: 1 })).toBeNull();
|
expect(readableViewportSize({ width: 1, height: 1 })).toBeNull();
|
||||||
|
|||||||
@ -890,6 +890,27 @@ main.app-main-reader .reader-page {
|
|||||||
background: rgba(38, 26, 18, 0.84);
|
background: rgba(38, 26, 18, 0.84);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reader-zoom-controls {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-zoom-button {
|
||||||
|
width: 34px;
|
||||||
|
min-width: 34px;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-zoom-label {
|
||||||
|
min-width: 58px;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.reader-title {
|
.reader-title {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -928,17 +949,16 @@ main.app-main-reader .reader-page {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-content {
|
.reader-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
min-height: 0;
|
min-width: 100%;
|
||||||
min-width: 0;
|
min-height: 100%;
|
||||||
width: 100%;
|
width: max-content;
|
||||||
height: 100%;
|
height: max-content;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-reader,
|
.pdf-reader,
|
||||||
@ -946,10 +966,8 @@ main.app-main-reader .reader-page {
|
|||||||
.cbz-reader {
|
.cbz-reader {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
min-height: 0;
|
min-width: 100%;
|
||||||
min-width: 0;
|
min-height: 100%;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.epub-host {
|
.epub-host {
|
||||||
@ -978,9 +996,10 @@ main.app-main-reader .reader-page {
|
|||||||
.comic-page-frame {
|
.comic-page-frame {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 100%;
|
width: max-content;
|
||||||
height: 100%;
|
height: max-content;
|
||||||
min-height: 64px;
|
min-width: 100%;
|
||||||
|
min-height: max(64px, 100%);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -989,8 +1008,6 @@ main.app-main-reader .reader-page {
|
|||||||
display: block;
|
display: block;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
@ -998,11 +1015,6 @@ main.app-main-reader .reader-page {
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cbz-reader img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-fallback {
|
.reader-fallback {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -1161,6 +1173,10 @@ main.app-main-reader .reader-page {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reader-zoom-reset {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.epub-host {
|
.epub-host {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user