Skip to content

Commit

Permalink
✨ feat(ebook): support adjust fontsize
Browse files Browse the repository at this point in the history
  • Loading branch information
summerscar committed Jan 1, 2025
1 parent d239f37 commit ec03755
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 73 deletions.
131 changes: 95 additions & 36 deletions app/(home)/article/[slug]/_components/ebook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import FullscreenIcon from "@/assets/svg/full-screen.svg";
import NextIcon from "@/assets/svg/next.svg";
import PrevIcon from "@/assets/svg/prev.svg";
import { timeOut } from "@/utils/time-out";
import { useFullscreen, useMemoizedFn } from "ahooks";
import { useClickAway, useFullscreen, useMemoizedFn } from "ahooks";
import clsx from "clsx";
import type { Book, Contents, Location, NavItem, Rendition } from "epubjs";
import type Section from "epubjs/types/section";
import dynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";

const debug = false;

const EBookRender = dynamic(
() => import("./ebook-render").then((mod) => mod.EBookRender),
{
Expand Down Expand Up @@ -42,6 +44,7 @@ const EBook = ({
const containerRef = useRef<HTMLDivElement>(null);
const [scrollLeft, setScrollLeft] = useState(0);
const wrapperRef = useRef<HTMLDivElement>(null);
const tocWrapperRef = useRef<HTMLDivElement>(null);
const initPromiseRef = useRef<Promise<void> | null>(null);
const ebookRenderContainerRef = useRef<HTMLDivElement>(null);
const [isFullScreen, { enterFullscreen, exitFullscreen }] = useFullscreen(
Expand All @@ -64,10 +67,13 @@ const EBook = ({
);

const onRender = useMemoizedFn((contents: Contents) => {
contents.document.querySelectorAll("p").forEach((p, index) => {
p.setAttribute("data-paragraph-index", `${index}`);
});
contents.document
.querySelectorAll("p:not([data-paragraph-index])")
.forEach((p, index) => {
p.setAttribute("data-paragraph-index", `${index}`);
});
(async () => {
await timeOut(16);
await Promise.all(
[...contents.document.querySelectorAll("img")].map(async (img) => {
await timeOut(16);
Expand All @@ -82,7 +88,13 @@ const EBook = ({
"style",
contents.document.body.getAttribute("style") || "",
);
wrapper.classList.add("overflow-hidden");
const scrollLeft =
containerRef.current?.querySelector(".epub-container")?.scrollLeft || 0;

Object.assign(wrapper.style, {
overflowY: "visible",
transform: `translateX(-${scrollLeft}px)`,
});
[...clonedBody.childNodes].forEach((node) => {
wrapper.appendChild(node);
});
Expand All @@ -106,6 +118,7 @@ const EBook = ({
spread: "always",
allowScriptedContent: true,
});

setBook(newBook);
setRendition(newRendition);
window.book = newBook;
Expand Down Expand Up @@ -141,15 +154,26 @@ const EBook = ({
});
}, [bookURL, onRender]);

const updateScrollLeft = () => {
const scrollLeft =
containerRef.current?.querySelector(".epub-container")?.scrollLeft;
setScrollLeft(scrollLeft || 0);
};

const onRelocated = useMemoizedFn((location: Location) => {
setCurrentLocation(location);
setTimeout(() => {
const scrollLeft =
containerRef.current?.querySelector(".epub-container")?.scrollLeft;
setScrollLeft(scrollLeft || 0);
});
updateScrollLeft();
}, 16);
});

useEffect(() => {
if (!ebookRenderContainerRef.current?.firstChild) return;
(
ebookRenderContainerRef.current.firstChild as HTMLDivElement
).style.transform = `translateX(-${scrollLeft}px)`;
}, [scrollLeft]);

const handleSectionChange = useMemoizedFn((location: Location) => {
const idref = spines.find(
(item) => item.href === location.start.href,
Expand All @@ -174,14 +198,6 @@ const EBook = ({
}
}, [rendition, onRelocated]);

useEffect(() => {
if (!ebookRenderContainerRef.current?.firstChild) return;
(ebookRenderContainerRef.current.firstChild as HTMLDivElement).scrollTo(
scrollLeft,
0,
);
}, [scrollLeft]);

const handleNextPage = () => {
if (rendition) {
rendition.next();
Expand All @@ -200,11 +216,23 @@ const EBook = ({
}
};

const changeFontSize = (fontSize: string) => () => {
rendition?.themes.fontSize(fontSize);
onThemeChange();
};
const onThemeChange = () => {
if (!rendition) return;
const contents = rendition.getContents() as unknown as Contents[];
onRender(contents[0]);
};

const clean = useMemoizedFn(() => {
rendition?.destroy();
book?.destroy();
});

useClickAway(() => setShowTOC(false), tocWrapperRef);

useEffect(() => clean, [clean]);

return (
Expand All @@ -222,7 +250,9 @@ const EBook = ({
<div
ref={containerRef}
className={clsx(
"w-full pointer-events-none invisible h-full opacity-0 absolute left-0 top-0 z-[-1]",
"w-full h-full absolute left-0 top-0 z-[-1]",
"pointer-events-none invisible opacity-0",
debug && "pointer-events-auto opacity-50 -translate-x-0.5 z-[1]",
)}
/>
{clonedDoms && (
Expand All @@ -246,22 +276,51 @@ const EBook = ({
</div>
)}
{showTOC && tableOfContents.length > 0 && (
<div className="w-72 h-full overflow-y-auto p-4 bg-white/30 dark:bg-black/50 backdrop-blur-lg absolute left-0 top-0 z-[1]">
<h2 className="text-xl font-bold mb-4">
{bookTitle}
<button
type="button"
onClick={() => setShowTOC(!showTOC)}
className="btn btn-ghost btn-xs float-right"
>
<CloseIcon className="size-4" />
</button>
</h2>

<TOCItems
items={tableOfContents}
onTOCItemClick={handleTOCItemClick}
/>
<div
ref={tocWrapperRef}
className="w-72 h-full overflow-y-auto bg-[#F5F5DC] dark:bg-gray-800 shadow-lg absolute left-0 top-0 z-[1]"
>
<div className="sticky top-0 bg-[#F5F5DC] dark:dark:bg-gray-800 p-4 shadow-md">
<h2 className="text-xl font-bold mb-4">
{bookTitle}
<button
type="button"
onClick={() => setShowTOC(!showTOC)}
className="btn btn-ghost btn-xs float-right"
>
<CloseIcon className="size-4" />
</button>
</h2>
<div className="flex gap-2">
<button
type="button"
onClick={changeFontSize("14px")}
className="btn btn-ghost btn-xs"
>
<span className="text-sm"></span>
</button>
<button
type="button"
onClick={changeFontSize("16px")}
className="btn btn-ghost btn-xs"
>
<span className="text-base"></span>
</button>
<button
type="button"
onClick={changeFontSize("18px")}
className="btn btn-ghost btn-xs"
>
<span className="text-lg"></span>
</button>
</div>
</div>
<div className="px-4">
<TOCItems
items={tableOfContents}
onTOCItemClick={handleTOCItemClick}
/>
</div>
</div>
)}
<PrevIcon
Expand All @@ -284,15 +343,15 @@ const EBook = ({
/>
)}
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 select-none">
<span>
<span className="text-sm text-base-content/70">
{currentLocation
? `${currentLocation.start.displayed.page} /
${currentLocation.start.displayed.total}`
: ""}
</span>
</div>
<div className="absolute bottom-2 right-2 -translate-x-1/2 select-none">
<span>
<span className="text-sm text-base-content/50">
{spines.findIndex((s) => s.href === currentLocation?.start.href) +
1}{" "}
/ {spines.length}
Expand Down
9 changes: 6 additions & 3 deletions app/actions/annotate-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import type {
AnnotationUpdateInput,
} from ".keystone/types";

const listAnnotationAction = async (bookId: string, chapterId: string) => {
const listAnnotationAction = async ({
articleId,
chapterId,
}: { articleId?: string; chapterId?: string }) => {
const session = await auth();
const userId = session?.user?.id;
if (!userId) {
return [];
}
const res = (await KSwithSession(session).db.Annotation.findMany({
where: {
bookId: { id: { equals: bookId } },
createdBy: { id: { equals: userId } },
chapterId: { equals: chapterId },
...(articleId ? { articleId: { id: { equals: articleId } } } : null),
...(chapterId ? { chapterId: { equals: chapterId } } : null),
},
})) as AnnotationItem[];

Expand Down
10 changes: 5 additions & 5 deletions app/components/annotation-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const Annotation = ({
}: { annotation?: AnnotationItem; range?: Range }) => {
const [value, setValue] = useState(annotation?.content || "");
// TODO: 作为组件参数
const [bookId] = useState(() => location.pathname.split("/").pop());
const [articleId] = useState(() => location.pathname.split("/").pop());
const searchParams = useSearchParams();
const chapterId =
searchParams.get("ep") || searchParams.get("section") || "0";
Expand Down Expand Up @@ -116,7 +116,7 @@ const Annotation = ({

const newAnnotation = {
type: "NOTE",
bookId: { connect: { id: bookId } },
articleId: { connect: { id: articleId } },
chapterId,
content: value,
text: rangeText,
Expand All @@ -138,7 +138,7 @@ const Annotation = ({
.then(() => {})
.finally(async () => {
unmountPromise.current = null;
await refreshSWRUserAnnotationItems(bookId!, chapterId);
await refreshSWRUserAnnotationItems({ chapterId, articleId });
isDev && console.log("[create annotation][success]", newAnnotation);
cancel();
});
Expand All @@ -148,7 +148,7 @@ const Annotation = ({
const remove = async (annotation: AnnotationItem) => {
const cancel = createLoadingToast("Removing…");
await removeAnnotationAction(annotation.id);
await refreshSWRUserAnnotationItems(bookId!, chapterId);
await refreshSWRUserAnnotationItems({ chapterId, articleId });
cancel();
isDev && console.log("[remove annotation][success]", annotation);
};
Expand All @@ -157,7 +157,7 @@ const Annotation = ({
if (value === annotation.content) return;
const cancel = createLoadingToast("Updating…");
await updateAnnotationAction(annotation.id, { content: value });
await refreshSWRUserAnnotationItems(bookId!, chapterId);
await refreshSWRUserAnnotationItems({ chapterId, articleId });
cancel();
isDev && console.log("[update annotation][success]");
};
Expand Down
19 changes: 11 additions & 8 deletions app/components/high-lighted-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import useSWRImmutable from "swr/immutable";

const SWR_DICT_KEY = "user-dict-items";
const SWR_ANNOTATION_KEY = "user-annotation-items";
const getAnnotationRevalidateKey = (articleId: string, chapterId?: string) =>
const getAnnotationRevalidateKey = ({
articleId,
chapterId,
}: { articleId?: string; chapterId?: string }) =>
[SWR_ANNOTATION_KEY, articleId, chapterId].filter(Boolean).join("|");

let increment = 0;
Expand All @@ -32,9 +35,9 @@ const useUserDictItems = () => {
const useUserAnnotationItems = (articleId: string, chapterId = "0") => {
const { isLogin } = useUser();
const { data = emptyArray } = useSWRImmutable(
isLogin ? getAnnotationRevalidateKey(articleId, chapterId) : null,
isLogin ? getAnnotationRevalidateKey({ articleId, chapterId }) : null,
async () => {
return await listAnnotationAction(articleId, chapterId);
return await listAnnotationAction({ articleId, chapterId });
},
);

Expand All @@ -45,11 +48,11 @@ export const refreshSWRUserDictItems = async () => {
await mutate(SWR_DICT_KEY);
};

export const refreshSWRUserAnnotationItems = async (
articleId: string,
chapterId?: string,
) => {
await mutate(getAnnotationRevalidateKey(articleId, chapterId));
export const refreshSWRUserAnnotationItems = async ({
articleId,
chapterId,
}: { articleId?: string; chapterId?: string }) => {
await mutate(getAnnotationRevalidateKey({ articleId, chapterId }));
};

const useHighlightedText = (
Expand Down
11 changes: 9 additions & 2 deletions app/styles/_ebook.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,18 @@
font-size: 0.8em;
color: #6f6f6f;
}

img {
display: inline;
}
div.sgc-2 {
text-align: center;
}
p.sgc-3 {
font-weight: bold;
}

p.sgc-5 {
text-align: center;
}
h2.sgc-4 {
text-align: center;
}
Expand Down
2 changes: 1 addition & 1 deletion keystone/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export const lists = {
validation: { isRequired: true },
label: "类型",
}),
bookId: relationship({ ref: "Article", many: false }),
articleId: relationship({ ref: "Article", many: false }),
chapterId: text({ validation: { isRequired: true } }),
text: text({ validation: { isRequired: true } }),
content: text({ validation: { isRequired: false } }),
Expand Down
Loading

0 comments on commit ec03755

Please sign in to comment.