Rework #6
					 7 changed files with 78 additions and 50 deletions
				
			
		| 
						 | 
				
			
			@ -21,6 +21,7 @@ import Layout from "./components/layout/layout";
 | 
			
		|||
import NotFoundPage from "./page/404";
 | 
			
		||||
import LoginPage from "./page/loginPage";
 | 
			
		||||
import ProfilePage from "./page/profilesPage";
 | 
			
		||||
import ContentInfoPage from "./page/contentInfoPage";
 | 
			
		||||
 | 
			
		||||
const App = () => {
 | 
			
		||||
	return (
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +32,8 @@ const App = () => {
 | 
			
		|||
					<Route path="/search" component={Gallery} />
 | 
			
		||||
					<Route path="/login" component={LoginPage} />
 | 
			
		||||
					<Route path="/profile" component={ProfilePage}/>
 | 
			
		||||
					{/* <Route path="/doc/:id" component={<DocumentAbout />}></Route>
 | 
			
		||||
					<Route path="/doc/:id" component={ContentInfoPage}/>
 | 
			
		||||
					{/* 
 | 
			
		||||
				<Route path="/doc/:id/reader" component={<ReaderPage />}></Route>
 | 
			
		||||
				<Route path="/difference" component={<DifferencePage />}></Route>
 | 
			
		||||
				<Route path="/setting" component={<SettingPage />}></Route>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,8 @@ import type { Document } from "dbtype/api";
 | 
			
		|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 | 
			
		||||
import TagBadge from "@/components/gallery/TagBadge";
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import { Link, useLocation } from "wouter";
 | 
			
		||||
import { LazyImage } from "./LazyImage";
 | 
			
		||||
 | 
			
		||||
function clipTagsWhenOverflow(tags: string[], limit: number) {
 | 
			
		||||
    let l = 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -15,48 +17,12 @@ function clipTagsWhenOverflow(tags: string[], limit: number) {
 | 
			
		|||
    return tags;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function LazyImage({ src, alt, className }: { src: string; alt: string; className?: string; }) {
 | 
			
		||||
    const ref = useRef<HTMLImageElement>(null);
 | 
			
		||||
    const [loaded, setLoaded] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (ref.current) {
 | 
			
		||||
            let toggle = false;
 | 
			
		||||
            const observer = new IntersectionObserver((entries) => {
 | 
			
		||||
                if (entries.some(x => x.isIntersecting)) {
 | 
			
		||||
                    setLoaded(true);
 | 
			
		||||
                    toggle = !toggle;
 | 
			
		||||
                    ref.current?.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500, easing: "ease-in-out" });
 | 
			
		||||
                    observer.disconnect();
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    if (toggle) {
 | 
			
		||||
                        console.log("fade out");
 | 
			
		||||
                        ref.current?.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 500, easing: "ease-in-out" });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            observer.observe(ref.current);
 | 
			
		||||
            return () => {
 | 
			
		||||
                observer.disconnect();
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return <img
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        src={loaded ? src : undefined}
 | 
			
		||||
        alt={alt}
 | 
			
		||||
        className={className}
 | 
			
		||||
        loading="lazy"
 | 
			
		||||
        />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function GalleryCard({
 | 
			
		||||
    doc: x
 | 
			
		||||
}: { doc: Document; }) {
 | 
			
		||||
    const ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
    const [clipCharCount, setClipCharCount] = useState(200);
 | 
			
		||||
    const [location] = useLocation();
 | 
			
		||||
 | 
			
		||||
    const artists = x.tags.filter(x => x.startsWith("artist:")).map(x => x.replace("artist:", ""));
 | 
			
		||||
    const groups = x.tags.filter(x => x.startsWith("group:")).map(x => x.replace("group:", ""));
 | 
			
		||||
| 
						 | 
				
			
			@ -84,11 +50,13 @@ export function GalleryCard({
 | 
			
		|||
            <LazyImage src={`/api/doc/${x.id}/comic/thumbnail`}
 | 
			
		||||
                alt={x.title}
 | 
			
		||||
                className="max-h-full max-w-full object-cover object-center"
 | 
			
		||||
                />
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="flex-1 flex flex-col">
 | 
			
		||||
            <CardHeader className="flex-none">
 | 
			
		||||
                <CardTitle>{x.title}</CardTitle>
 | 
			
		||||
                <CardTitle>
 | 
			
		||||
                    <Link href={`/doc/${x.id}`} state={{fromUrl: location}}>{x.title}</Link>
 | 
			
		||||
                </CardTitle>
 | 
			
		||||
                <CardDescription>
 | 
			
		||||
                    {artists.join(", ")} {groups.length > 0 && "|"} {groups.join(", ")}
 | 
			
		||||
                </CardDescription>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										37
									
								
								packages/client/src/components/gallery/LazyImage.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								packages/client/src/components/gallery/LazyImage.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
 | 
			
		||||
export function LazyImage({ src, alt, className }: { src: string; alt: string; className?: string; }) {
 | 
			
		||||
    const ref = useRef<HTMLImageElement>(null);
 | 
			
		||||
    const [loaded, setLoaded] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (ref.current) {
 | 
			
		||||
            let toggle = false;
 | 
			
		||||
            const observer = new IntersectionObserver((entries) => {
 | 
			
		||||
                if (entries.some(x => x.isIntersecting)) {
 | 
			
		||||
                    setLoaded(true);
 | 
			
		||||
                    toggle = !toggle;
 | 
			
		||||
                    ref.current?.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500, easing: "ease-in-out" });
 | 
			
		||||
                    observer.disconnect();
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    if (toggle) {
 | 
			
		||||
                        console.log("fade out");
 | 
			
		||||
                        ref.current?.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 500, easing: "ease-in-out" });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            observer.observe(ref.current);
 | 
			
		||||
            return () => {
 | 
			
		||||
                observer.disconnect();
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return <img
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        src={loaded ? src : undefined}
 | 
			
		||||
        alt={alt}
 | 
			
		||||
        className={className}
 | 
			
		||||
        loading="lazy" />;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								packages/client/src/page/contentInfoPage.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/client/src/page/contentInfoPage.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
export interface ContentInfoPageProps {
 | 
			
		||||
    params: {
 | 
			
		||||
        id: string;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function ContentInfoPage({params}: ContentInfoPageProps) {
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="p-4">
 | 
			
		||||
            <h1>ContentInfoPage</h1>
 | 
			
		||||
            {params.id}
 | 
			
		||||
            <p>Find me in packages/client/src/page/contentInfoPage.tsx</p>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ContentInfoPage;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 | 
			
		||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 | 
			
		||||
import { useLogin } from "@/state/user";
 | 
			
		||||
import { Redirect } from "wouter";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ export function ProfilePage() {
 | 
			
		|||
        return <Redirect to="/login" />;
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="m-4">
 | 
			
		||||
        <div className="p-4">
 | 
			
		||||
            <Card>
 | 
			
		||||
                <CardHeader>
 | 
			
		||||
                    <CardTitle className="text-2xl">Profile</CardTitle>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
import { type Context, DefaultContext, DefaultState, Next } from "koa";
 | 
			
		||||
import type { Context } from "koa";
 | 
			
		||||
import Router from "koa-router";
 | 
			
		||||
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap";
 | 
			
		||||
import type { ContentContext } from "./context";
 | 
			
		||||
import { since_last_modified } from "./util";
 | 
			
		||||
import type { ZipReader } from "@zip.js/zip.js";
 | 
			
		||||
import type { FileHandle } from "node:fs/promises";
 | 
			
		||||
import { Readable, Writable } from "node:stream";
 | 
			
		||||
import { Readable } from "node:stream";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * zip stream cache.
 | 
			
		||||
| 
						 | 
				
			
			@ -91,11 +91,15 @@ async function renderZipImage(ctx: Context, path: string, page: number) {
 | 
			
		|||
			},
 | 
			
		||||
			close() {
 | 
			
		||||
				nodeReadableStream.push(null);
 | 
			
		||||
				releaseZip(path);
 | 
			
		||||
				// setTimeout(() => {
 | 
			
		||||
				// }, 500);
 | 
			
		||||
			},
 | 
			
		||||
		}));
 | 
			
		||||
		nodeReadableStream.on("error", (err) => {
 | 
			
		||||
			console.error(err);
 | 
			
		||||
			releaseZip(path);
 | 
			
		||||
		});
 | 
			
		||||
		nodeReadableStream.on("close", () => {
 | 
			
		||||
			releaseZip(path);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		ctx.body = nodeReadableStream;
 | 
			
		||||
		ctx.response.length = entry.uncompressedSize;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,15 +4,12 @@ import { ZipReader, Reader, type Entry } from "@zip.js/zip.js";
 | 
			
		|||
 | 
			
		||||
class FileReader extends Reader<FileHandle> {
 | 
			
		||||
	private fd: FileHandle;
 | 
			
		||||
	private offset: number;
 | 
			
		||||
	constructor(fd: FileHandle) {
 | 
			
		||||
		super(fd);
 | 
			
		||||
		this.fd = fd;
 | 
			
		||||
		this.offset = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async init(): Promise<void> {
 | 
			
		||||
		this.offset = 0;
 | 
			
		||||
		this.size = (await this.fd.stat()).size;
 | 
			
		||||
	}
 | 
			
		||||
	close(): void {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue