Rework #6
					 7 changed files with 146 additions and 69 deletions
				
			
		
							
								
								
									
										26
									
								
								packages/client/src/components/gallery/DescItem.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								packages/client/src/components/gallery/DescItem.tsx
									
										
									
									
									
										Normal 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>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,10 @@
 | 
			
		|||
import { Link } from "wouter"
 | 
			
		||||
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 { useLogin } from "@/state/user.ts";
 | 
			
		||||
import { useNavItems } from "./navAtom";
 | 
			
		||||
import { Separator } from "../ui/separator";
 | 
			
		||||
 | 
			
		||||
interface NavItemProps {
 | 
			
		||||
    icon: React.ReactNode;
 | 
			
		||||
| 
						 | 
				
			
			@ -29,11 +31,41 @@ export function NavItem({
 | 
			
		|||
    </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() {
 | 
			
		||||
    const loginInfo = useLogin();
 | 
			
		||||
    const navItems = useNavItems();
 | 
			
		||||
 | 
			
		||||
    return <aside className="h-dvh flex flex-col">
 | 
			
		||||
        <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={<ActivityLogIcon className="h-5 w-5" />} to="/tags" name="Tags" />
 | 
			
		||||
            <NavItem icon={<ArchiveIcon className="h-5 w-5" />} to="/difference" name="Difference" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								packages/client/src/components/layout/navAtom.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/client/src/components/layout/navAtom.tsx
									
										
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ export function atom<T>(key: string, defaultVal: T): Atom<T> {
 | 
			
		|||
    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);
 | 
			
		||||
    if (!atomState) {
 | 
			
		||||
        atomState = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										33
									
								
								packages/client/src/lib/classifyTags.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								packages/client/src/lib/classifyTags.tsx
									
										
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,8 +2,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
 | 
			
		|||
import { useGalleryDoc } from "../hook/useGalleryDoc.ts";
 | 
			
		||||
import TagBadge from "@/components/gallery/TagBadge";
 | 
			
		||||
import StyledLink from "@/components/gallery/StyledLink";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import { Link } from "wouter";
 | 
			
		||||
import { classifyTags } from "../lib/classifyTags.tsx";
 | 
			
		||||
import { DescTagItem, DescItem } from "../components/gallery/DescItem.tsx";
 | 
			
		||||
 | 
			
		||||
export interface ContentInfoPageProps {
 | 
			
		||||
    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) {
 | 
			
		||||
    const { data, error, isLoading } = useGalleryDoc(params.id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -112,31 +79,4 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
 | 
			
		|||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
}
 | 
			
		||||
export default ContentInfoPage;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,8 @@
 | 
			
		|||
import { NavItem, NavItemButton } from "@/components/layout/nav";
 | 
			
		||||
import { PageNavItem } from "@/components/layout/navAtom";
 | 
			
		||||
import { useGalleryDoc } from "@/hook/useGalleryDoc.ts";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import { EnterFullScreenIcon, ExitIcon } from "@radix-ui/react-icons";
 | 
			
		||||
import type { Document } from "dbtype/api";
 | 
			
		||||
import { useCallback, useEffect, useRef, useState } from "react";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12,14 +15,17 @@ interface ComicPageProps {
 | 
			
		|||
function ComicViewer({
 | 
			
		||||
    doc,
 | 
			
		||||
    totalPage,
 | 
			
		||||
    curPage,
 | 
			
		||||
    onChangePage: setCurPage,
 | 
			
		||||
}: {
 | 
			
		||||
    doc: Document;
 | 
			
		||||
    totalPage: number;
 | 
			
		||||
    curPage: number;
 | 
			
		||||
    onChangePage: (page: number) => void;
 | 
			
		||||
}) {
 | 
			
		||||
    const [curPage, setCurPage] = useState(0);
 | 
			
		||||
    const [fade, setFade] = useState(false);
 | 
			
		||||
    const PageDown = useCallback((step: number) => setCurPage(Math.max(curPage - step, 0)), [curPage]);
 | 
			
		||||
    const PageUp = useCallback((step: number) => setCurPage(Math.min(curPage + step, totalPage - 1)), [curPage, totalPage]);
 | 
			
		||||
    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, setCurPage, totalPage]);
 | 
			
		||||
    const currentImageRef = useRef<HTMLImageElement>(null);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -84,6 +90,7 @@ export default function ComicPage({
 | 
			
		|||
    params
 | 
			
		||||
}: ComicPageProps) {
 | 
			
		||||
    const { data, error, isLoading } = useGalleryDoc(params.id);
 | 
			
		||||
    const [curPage, setCurPage] = useState(0);
 | 
			
		||||
 | 
			
		||||
    if (isLoading) {
 | 
			
		||||
        // TODO: Add a loading spinner
 | 
			
		||||
| 
						 | 
				
			
			@ -107,6 +114,22 @@ export default function ComicPage({
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    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>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue