feat: comic viewer nav

This commit is contained in:
monoid 2024-04-16 23:22:26 +09:00
parent 83731e7aac
commit 7620d64854
7 changed files with 146 additions and 69 deletions

View File

@ -0,0 +1,26 @@
import StyledLink from "@/components/gallery/StyledLink";
import { cn } from "@/lib/utils";
export function DescItem({ name, children, className }: {
name: string;
className?: string;
children?: React.ReactNode;
}) {
return <div className={cn("grid content-start", className)}>
<span className="text-muted-foreground text-sm">{name}</span>
<span className="text-primary leading-4 font-medium">{children}</span>
</div>;
}
export function DescTagItem({
items, name, className,
}: {
name: string;
items: string[];
className?: string;
}) {
return <DescItem name={name} className={className}>
{items.length === 0 ? "N/A" : items.map(
(x) => <StyledLink key={x} to={`/search?allow_tag=${name}:${x}`}>{x}</StyledLink>
)}
</DescItem>;
}

View File

@ -1,8 +1,10 @@
import { Link } from "wouter" import { Link } from "wouter"
import { MagnifyingGlassIcon, GearIcon, ActivityLogIcon, ArchiveIcon, PersonIcon } from "@radix-ui/react-icons" import { MagnifyingGlassIcon, GearIcon, ActivityLogIcon, ArchiveIcon, PersonIcon } from "@radix-ui/react-icons"
import { buttonVariants } from "@/components/ui/button.tsx" import { Button, buttonVariants } from "@/components/ui/button.tsx"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx"
import { useLogin } from "@/state/user.ts"; import { useLogin } from "@/state/user.ts";
import { useNavItems } from "./navAtom";
import { Separator } from "../ui/separator";
interface NavItemProps { interface NavItemProps {
icon: React.ReactNode; icon: React.ReactNode;
@ -29,11 +31,41 @@ export function NavItem({
</Tooltip> </Tooltip>
} }
interface NavItemButtonProps {
icon: React.ReactNode;
onClick: () => void;
name: string;
className?: string;
}
export function NavItemButton({
icon,
onClick,
name,
className
}: NavItemButtonProps) {
return <Tooltip>
<TooltipTrigger asChild>
<Button
onClick={onClick}
variant="ghost"
className={className}
>
{icon}
<span className="sr-only">{name}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right">{name}</TooltipContent>
</Tooltip>
}
export function NavList() { export function NavList() {
const loginInfo = useLogin(); const loginInfo = useLogin();
const navItems = useNavItems();
return <aside className="h-dvh flex flex-col"> return <aside className="h-dvh flex flex-col">
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1"> <nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
{navItems && <>{navItems} <Separator/> </>}
<NavItem icon={<MagnifyingGlassIcon className="h-5 w-5" />} to="/search" name="Search" /> <NavItem icon={<MagnifyingGlassIcon className="h-5 w-5" />} to="/search" name="Search" />
<NavItem icon={<ActivityLogIcon className="h-5 w-5" />} to="/tags" name="Tags" /> <NavItem icon={<ActivityLogIcon className="h-5 w-5" />} to="/tags" name="Tags" />
<NavItem icon={<ArchiveIcon className="h-5 w-5" />} to="/difference" name="Difference" /> <NavItem icon={<ArchiveIcon className="h-5 w-5" />} to="/difference" name="Difference" />

View File

@ -0,0 +1,23 @@
import { atom, useAtomValue, setAtomValue, getAtomState } from "@/lib/atom";
import { useLayoutEffect } from "react";
const NavItems = atom<React.ReactNode>("NavItems", null);
// eslint-disable-next-line react-refresh/only-export-components
export function useNavItems() {
return useAtomValue(NavItems);
}
export function PageNavItem({items, children}:{items: React.ReactNode, children: React.ReactNode}) {
useLayoutEffect(() => {
const prev = getAtomState(NavItems).value;
const setter = setAtomValue(NavItems);
setter(items);
return () => {
setter(prev);
};
}, [items]);
return children;
}

View File

@ -15,7 +15,7 @@ export function atom<T>(key: string, defaultVal: T): Atom<T> {
return { key, default: defaultVal }; return { key, default: defaultVal };
} }
function getAtomState<T>(atom: Atom<T>): AtomState<T> { export function getAtomState<T>(atom: Atom<T>): AtomState<T> {
let atomState = atomStateMap.get(atom); let atomState = atomStateMap.get(atom);
if (!atomState) { if (!atomState) {
atomState = { atomState = {

View File

@ -0,0 +1,33 @@
interface TagClassifyResult {
artist: string[];
group: string[];
series: string[];
type: string[];
character: string[];
rest: string[];
}
export function classifyTags(tags: string[]): TagClassifyResult {
const result = {
artist: [],
group: [],
series: [],
type: [],
character: [],
rest: [],
} as TagClassifyResult;
const tagKind = new Set(["artist", "group", "series", "type", "character"]);
for (const tag of tags) {
const split = tag.split(":");
if (split.length !== 2) {
continue;
}
const [prefix, name] = split;
if (tagKind.has(prefix)) {
result[prefix as keyof TagClassifyResult].push(name);
} else {
result.rest.push(tag);
}
}
return result;
}

View File

@ -2,8 +2,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { useGalleryDoc } from "../hook/useGalleryDoc.ts"; import { useGalleryDoc } from "../hook/useGalleryDoc.ts";
import TagBadge from "@/components/gallery/TagBadge"; import TagBadge from "@/components/gallery/TagBadge";
import StyledLink from "@/components/gallery/StyledLink"; import StyledLink from "@/components/gallery/StyledLink";
import { cn } from "@/lib/utils";
import { Link } from "wouter"; import { Link } from "wouter";
import { classifyTags } from "../lib/classifyTags.tsx";
import { DescTagItem, DescItem } from "../components/gallery/DescItem.tsx";
export interface ContentInfoPageProps { export interface ContentInfoPageProps {
params: { params: {
@ -11,40 +12,6 @@ export interface ContentInfoPageProps {
}; };
} }
interface TagClassifyResult {
artist: string[];
group: string[];
series: string[];
type: string[];
character: string[];
rest: string[];
}
function classifyTags(tags: string[]): TagClassifyResult {
const result = {
artist: [],
group: [],
series: [],
type: [],
character: [],
rest: [],
} as TagClassifyResult;
const tagKind = new Set(["artist", "group", "series", "type", "character"]);
for (const tag of tags) {
const split = tag.split(":");
if (split.length !== 2) {
continue;
}
const [prefix, name] = split;
if (tagKind.has(prefix)) {
result[prefix as keyof TagClassifyResult].push(name);
} else {
result.rest.push(tag);
}
}
return result;
}
export function ContentInfoPage({ params }: ContentInfoPageProps) { export function ContentInfoPage({ params }: ContentInfoPageProps) {
const { data, error, isLoading } = useGalleryDoc(params.id); const { data, error, isLoading } = useGalleryDoc(params.id);
@ -113,30 +80,3 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
} }
export default ContentInfoPage; export default ContentInfoPage;
function DescItem({ name, children, className }: {
name: string,
className?: string,
children?: React.ReactNode
}) {
return <div className={cn("grid content-start", className)}>
<span className="text-muted-foreground text-sm">{name}</span>
<span className="text-primary leading-4 font-medium">{children}</span>
</div>;
}
function DescTagItem({
items,
name,
className,
}: {
name: string;
items: string[];
className?: string;
}) {
return <DescItem name={name} className={className}>
{items.length === 0 ? "N/A" : items.map(
(x) => <StyledLink key={x} to={`/search?allow_tag=${name}:${x}`}>{x}</StyledLink>
)}
</DescItem>
}

View File

@ -1,5 +1,8 @@
import { NavItem, NavItemButton } from "@/components/layout/nav";
import { PageNavItem } from "@/components/layout/navAtom";
import { useGalleryDoc } from "@/hook/useGalleryDoc.ts"; import { useGalleryDoc } from "@/hook/useGalleryDoc.ts";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { EnterFullScreenIcon, ExitIcon } from "@radix-ui/react-icons";
import type { Document } from "dbtype/api"; import type { Document } from "dbtype/api";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
@ -12,14 +15,17 @@ interface ComicPageProps {
function ComicViewer({ function ComicViewer({
doc, doc,
totalPage, totalPage,
curPage,
onChangePage: setCurPage,
}: { }: {
doc: Document; doc: Document;
totalPage: number; totalPage: number;
curPage: number;
onChangePage: (page: number) => void;
}) { }) {
const [curPage, setCurPage] = useState(0);
const [fade, setFade] = useState(false); const [fade, setFade] = useState(false);
const PageDown = useCallback((step: number) => setCurPage(Math.max(curPage - step, 0)), [curPage]); const PageDown = useCallback((step: number) => setCurPage(Math.max(curPage - step, 0)), [curPage, setCurPage]);
const PageUp = useCallback((step: number) => setCurPage(Math.min(curPage + step, totalPage - 1)), [curPage, totalPage]); const PageUp = useCallback((step: number) => setCurPage(Math.min(curPage + step, totalPage - 1)), [curPage, setCurPage, totalPage]);
const currentImageRef = useRef<HTMLImageElement>(null); const currentImageRef = useRef<HTMLImageElement>(null);
useEffect(() => { useEffect(() => {
@ -84,6 +90,7 @@ export default function ComicPage({
params params
}: ComicPageProps) { }: ComicPageProps) {
const { data, error, isLoading } = useGalleryDoc(params.id); const { data, error, isLoading } = useGalleryDoc(params.id);
const [curPage, setCurPage] = useState(0);
if (isLoading) { if (isLoading) {
// TODO: Add a loading spinner // TODO: Add a loading spinner
@ -107,6 +114,22 @@ export default function ComicPage({
} }
return ( return (
<ComicViewer doc={data} totalPage={data.additional.page as number} /> <PageNavItem items={<>
<NavItem to={`/doc/${params.id}`} name="Back" icon={<ExitIcon />}/>
<NavItemButton name="fullscreen" icon={<EnterFullScreenIcon/>} onClick={()=>{
const elem = document.documentElement;
if (elem.requestFullscreen) {
elem.requestFullscreen();
}
}} />
<div className="">
<span className="text-sm text-ellipsis" >{curPage + 1}/{data.additional.page as number}</span>
</div>
</>}>
<ComicViewer
curPage={curPage}
onChangePage={setCurPage}
doc={data} totalPage={data.additional.page as number} />
</PageNavItem>
) )
} }