Rework #6
@ -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…
Reference in New Issue
Block a user