1
0
forked from YaBL/app
app/apps/web/src/pages/ReaderFB2.jsx
2025-06-21 12:42:09 +03:00

258 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios from "axios";
import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router";
import "./reader.css";
import OutlinedButton from "../md-components/OutlinedButton";
import FilledButton from "../md-components/FilledButton";
import Contents from "../components/contents/Contents";
import Icon from "../md-components/Icon";
import { Link } from "react-router-dom";
import Skeleton from "react-loading-skeleton";
import LinearProgress from "../md-components/LinearProgress";
import {
addBook,
getBook,
getReadBook,
putReadBook,
removeBook,
saveReadBook,
updateReadBook,
} from "../db";
import IconButton from "../md-components/IconButton";
const ReaderFB2 = () => {
const { id } = useParams();
const readerRef = useRef();
const containerRef = useRef();
const progressRef = useRef();
const contentsRef = useRef();
const contentsBackdropRef = useRef();
const navigate = useNavigate();
const [totalHeight, setTotalHeight] = useState(0);
const [currentHeight, setCurrentHeight] = useState(0);
const [readerHeight, setReaderHeight] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const [currentPage, setCurrentPage] = useState(0);
const [isLocal, setIsLocal] = useState(false);
const [onBookshelf, setOnBookshelf] = useState(false);
const [bookInfo, setBookInfo] = useState({ title: "" });
const [contents, setContents] = useState([]);
function nextPage() {
setCurrentPage((prev) => {
if (totalPages - prev < 1) {
return prev;
}
return prev + 1;
});
}
function prevPage() {
setCurrentPage((prev) => {
if (prev <= 1) {
return prev;
}
return prev - 1;
});
}
const leftRef = useRef();
const rightRef = useRef();
function keyboardPagination(e) {
if (!(leftRef.current && rightRef.current)) {
console.log("harakiri");
document.removeEventListener("keydown", keyboardPagination, false);
return;
}
if (e.key === "ArrowRight") {
rightRef.current.click();
}
if (e.key === "ArrowLeft") {
leftRef.current.click();
}
}
useEffect(() => {
if (!(leftRef.current && rightRef.current)) return;
console.log("add listener");
document.addEventListener("keydown", keyboardPagination, false);
}, [leftRef, rightRef]);
function toggleFloatingContents() {
if (!contentsRef.current || !contentsBackdropRef.current) return;
contentsRef.current.classList.toggle("show");
contentsBackdropRef.current.classList.toggle("show");
}
useEffect(() => {
if (currentPage === 0) return;
if (readerHeight === totalHeight) return;
setCurrentHeight(readerHeight * (currentPage - 1));
readerRef.current.scrollTop = readerHeight * (currentPage - 1);
progressRef.current.style.width = `${(currentPage / totalPages) * 100}%`;
//console.log(readerHeight * (currentPage-1) / totalHeight, "update read book", currentPage)
//console.log(readerHeight, currentPage, totalHeight)
updateReadBook(id, (readerHeight * (currentPage - 1)) / totalHeight);
}, [currentPage]);
async function loadBook() {
let readBook = await getReadBook(id);
if (readBook) {
setOnBookshelf(readBook.onBookshelf === true);
}
let caches = undefined;
let localHTML = undefined;
if (window.caches !== undefined) {
caches = await window.caches.open("books");
localHTML = await caches.match(`/api/book/${id}/reader`);
}
let localBook = await getBook(id);
let innerHTML = "";
if (localBook !== undefined && localHTML !== undefined) {
setIsLocal(true);
setBookInfo(localBook);
setContents(localBook.contents);
await localHTML.text().then((html) => {
innerHTML = html;
});
} else {
if (!window.onLine) {
localStorage.removeItem("lastReadBook");
navigate("/offline");
}
setIsLocal(false);
axios
.get(`/book/${id}`)
.then((res) => setBookInfo(res.data))
.catch(() => navigate("/notfound"));
axios.get(`/book/${id}/contents`).then((res) => setContents(res.data));
await axios.get(`/book/${id}/reader`).then((res) => {
innerHTML = res.data;
});
}
localStorage.setItem("lastReadBook", id);
readerRef.current.innerHTML =
innerHTML +
"<br>".repeat(Math.floor(containerRef.current.clientHeight / 23) * 2); // заполнение двух страниц пустотой для возможности прокрутки не до конца
resizingReader(true);
}
async function resizingReader(firstLoad) {
if (!readerRef.current) return;
if (readerRef.current.scrollHeight === 0) return;
let progressRatio;
if (firstLoad) {
let readBook = await getReadBook(id);
if (readBook === undefined) {
progressRatio = 0;
updateReadBook(id, 0);
} else {
progressRatio = readBook.progress;
}
}
readerRef.current.style.height = `${
Math.floor(containerRef.current.clientHeight / 23) * 23 - 23
}px`; // подгоняем блок контента под окно ридера, и не забывает учитывать прогресс
setTotalHeight(readerRef.current.scrollHeight);
setReaderHeight(readerRef.current.clientHeight);
setTotalPages(
Math.ceil(
readerRef.current.scrollHeight / readerRef.current.clientHeight
) - 2
);
setCurrentPage(
Math.ceil(
Math.floor(progressRatio * readerRef.current.scrollHeight) /
readerRef.current.clientHeight
) + 1
);
}
useEffect(() => {
if (!readerRef.current) return;
loadBook();
}, [id]);
// изменение размера изображений для соответствия с шириной строки (23px)
useEffect(() => {
if (!readerRef.current) return;
let images = Array.from(readerRef.current.getElementsByTagName("img"));
images.map((img) => {
img.height = Math.floor(img.height / 23) * 23;
});
}, [totalHeight]);
return (
<>
<div
className="reader_contents_backdrop"
ref={contentsBackdropRef}
onClick={toggleFloatingContents}
></div>
<div className="reader_contents" ref={contentsRef}>
<Contents
data={contents}
readerRef={readerRef}
readerHeight={readerHeight}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
/>
</div>
<div style={{ display: "flex", gap: "10px" }} className="mb-4">
{!(contents.length === 1 && contents[0].title === "") &&
contents.length !== 0 ? (
<OutlinedButton
className="contents_button reader_action_button"
onClick={toggleFloatingContents}
>
<Icon slot="icon">article</Icon>
Содержание
</OutlinedButton>
) : (
<></>
)}
</div>
<div className="reader_container" ref={containerRef}>
<div className="book_content" ref={readerRef} id="reader">
<LinearProgress indeterminate />
<br />
<Skeleton
count={
containerRef.current
? Math.floor(containerRef.current.clientHeight / 23) - 2
: 0
}
/>
</div>
<div className="reader_progress" ref={progressRef}></div>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "20px",
}}
>
<IconButton
onClick={prevPage}
disabled={currentHeight === 0}
ref={leftRef}
>
<Icon>arrow_back</Icon>
</IconButton>
<span>
{currentPage} из {totalPages}
</span>
<IconButton
onClick={nextPage}
disabled={totalPages - currentPage < 1}
ref={rightRef}
>
<Icon>arrow_forward</Icon>
</IconButton>
</div>
</>
);
};
export default ReaderFB2;