feat: add difference page

This commit is contained in:
monoid 2024-04-07 23:50:39 +09:00
parent a69cadf0a1
commit 7598df6018
17 changed files with 185 additions and 154 deletions

View File

@ -14,6 +14,7 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",

View File

@ -14,6 +14,7 @@ import ProfilePage from "@/page/profilesPage.tsx";
import ContentInfoPage from "@/page/contentInfoPage.tsx"; import ContentInfoPage from "@/page/contentInfoPage.tsx";
import SettingPage from "@/page/settingPage.tsx"; import SettingPage from "@/page/settingPage.tsx";
import ComicPage from "@/page/reader/comicPage.tsx"; import ComicPage from "@/page/reader/comicPage.tsx";
import DifferencePage from "./page/differencePage.tsx";
const App = () => { const App = () => {
const { isDarkMode } = useTernaryDarkMode(); const { isDarkMode } = useTernaryDarkMode();
@ -38,9 +39,7 @@ const App = () => {
<Route path="/doc/:id" component={ContentInfoPage}/> <Route path="/doc/:id" component={ContentInfoPage}/>
<Route path="/setting" component={SettingPage} /> <Route path="/setting" component={SettingPage} />
<Route path="/doc/:id/reader" component={ComicPage}/> <Route path="/doc/:id/reader" component={ComicPage}/>
{/* <Route path="/difference" component={DifferencePage}/>
<Route path="/difference" component={<DifferencePage />}/>
*/}
<Route component={NotFoundPage} /> <Route component={NotFoundPage} />
</Switch> </Switch>
</Layout> </Layout>

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -1,4 +1,4 @@
export const BASE_API_URL = 'http://localhost:8080/'; export const BASE_API_URL = 'http://localhost:5173/';
export async function fetcher(url: string) { export async function fetcher(url: string) {
const u = new URL(url, BASE_API_URL); const u = new URL(url, BASE_API_URL);

View File

@ -0,0 +1,38 @@
import useSWR, { mutate } from "swr";
import { fetcher } from "./fetcher";
type FileDifference = {
type: string;
value: {
type: string;
path: string;
}[];
};
export function useDifferenceDoc() {
return useSWR<FileDifference[]>("/api/diff/list", fetcher);
}
export async function commit(path: string, type: string) {
const res = await fetch("/api/diff/commit", {
method: "POST",
body: JSON.stringify([{ path, type }]),
headers: {
"Content-Type": "application/json",
},
});
mutate("/api/diff/list");
return res.ok;
}
export async function commitAll(type: string) {
const res = await fetch("/api/diff/commitall", {
method: "POST",
body: JSON.stringify({ type }),
headers: {
"Content-Type": "application/json",
},
});
mutate("/api/diff/list");
return res.ok;
}

View File

@ -1,126 +0,0 @@
// import { Box, Button, Paper, Typography } from "@mui/material";
// import React, { useContext, useEffect, useState } from "react";
// import { CommonMenuList, Headline } from "../component/mod";
// import { UserContext } from "../state";
// import { PagePad } from "../component/pagepad";
// type FileDifference = {
// type: string;
// value: {
// type: string;
// path: string;
// }[];
// };
// function TypeDifference(prop: {
// content: FileDifference;
// onCommit: (v: { type: string; path: string }) => void;
// onCommitAll: (type: string) => void;
// }) {
// // const classes = useStyles();
// const x = prop.content;
// const [button_disable, set_disable] = useState(false);
// return (
// <Paper /*className={classes.paper}*/>
// <Box /*className={classes.contentTitle}*/>
// <Typography variant="h3">{x.type}</Typography>
// <Button
// variant="contained"
// key={x.type}
// onClick={() => {
// set_disable(true);
// prop.onCommitAll(x.type);
// set_disable(false);
// }}
// >
// Commit all
// </Button>
// </Box>
// {x.value.map((y) => (
// <Box sx={{ display: "flex" }} key={y.path}>
// <Button
// variant="contained"
// onClick={() => {
// set_disable(true);
// prop.onCommit(y);
// set_disable(false);
// }}
// disabled={button_disable}
// >
// Commit
// </Button>
// <Typography variant="h5">{y.path}</Typography>
// </Box>
// ))}
// </Paper>
// );
// }
// export function DifferencePage() {
// const ctx = useContext(UserContext);
// // const classes = useStyles();
// const [diffList, setDiffList] = useState<FileDifference[]>([]);
// const doLoad = async () => {
// const list = await fetch("/api/diff/list");
// if (list.ok) {
// const inner = await list.json();
// setDiffList(inner);
// } else {
// // setDiffList([]);
// }
// };
// const Commit = async (x: { type: string; path: string }) => {
// const res = await fetch("/api/diff/commit", {
// method: "POST",
// body: JSON.stringify([{ ...x }]),
// headers: {
// "content-type": "application/json",
// },
// });
// const bb = await res.json();
// if (bb.ok) {
// doLoad();
// } else {
// console.error("fail to add document");
// }
// };
// const CommitAll = async (type: string) => {
// const res = await fetch("/api/diff/commitall", {
// method: "POST",
// body: JSON.stringify({ type: type }),
// headers: {
// "content-type": "application/json",
// },
// });
// const bb = await res.json();
// if (bb.ok) {
// doLoad();
// } else {
// console.error("fail to add document");
// }
// };
// useEffect(() => {
// doLoad();
// const i = setInterval(doLoad, 5000);
// return () => {
// clearInterval(i);
// };
// }, []);
// const menu = CommonMenuList();
// return (
// <Headline menu={menu}>
// <PagePad>
// {ctx.username == "admin" ? (
// <div>
// {diffList.map((x) => (
// <TypeDifference key={x.type} content={x} onCommit={Commit} onCommitAll={CommitAll} />
// ))}
// </div>
// ) : (
// <Typography variant="h2">Not Allowed : please login as an admin</Typography>
// )}
// </PagePad>
// </Headline>
// );
// }

View File

@ -0,0 +1,63 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { useDifferenceDoc, commit, commitAll } from "@/hook/useDifference";
import { useLogin } from "@/state/user";
import { Fragment } from "react/jsx-runtime";
export function DifferencePage() {
const { data, isLoading, error } = useDifferenceDoc();
const userInfo = useLogin();
if (!userInfo) {
return <div className="p-4">
<h2 className="text-3xl">
Not logged in
</h2>
</div>
}
if (error) {
return <div>Error: {String(error)}</div>
}
return (
<div className="p-4">
<Card>
<CardHeader className="relative">
<Button className="absolute right-2 top-8" variant="ghost"
onClick={() => commitAll("comic")}
>Commit All</Button>
<CardTitle className="text-2xl">Difference</CardTitle>
<CardDescription>Scanned Files List</CardDescription>
</CardHeader>
<CardContent>
<Separator decorative />
{isLoading && <div>Loading...</div>}
{data?.map((c) => {
const x = c.value;
return (
<Fragment key={c.type}>
{x.map((y) => (
<div key={y.path} className="flex items-center mt-2">
<p
className="flex-1 text-sm text-wrap">{y.path}</p>
<Button
className="flex-none ml-2"
variant="outline"
onClick={() => commit(y.path, y.type)}
>
Commit
</Button>
</div>
))}
</Fragment>
)
})}
</CardContent>
</Card>
</div>
)
}
export default DifferencePage;

View File

@ -24,9 +24,10 @@ function getUserSessions() {
} }
export async function refresh() { export async function refresh() {
const u = new URL("/user/refresh", BASE_API_URL); const u = new URL("/api/user/refresh", BASE_API_URL);
const res = await fetch(u, { const res = await fetch(u, {
method: "POST", method: "POST",
credentials: "include",
}); });
if (res.status !== 200) throw new Error("Maybe Network Error"); if (res.status !== 200) throw new Error("Maybe Network Error");
const r = (await res.json()) as LoginLocalStorage & { refresh: boolean }; const r = (await res.json()) as LoginLocalStorage & { refresh: boolean };
@ -49,9 +50,10 @@ export async function refresh() {
} }
export const doLogout = async () => { export const doLogout = async () => {
const u = new URL("/user/refresh", BASE_API_URL); const u = new URL("/api/user/logout", BASE_API_URL);
const req = await fetch(u, { const req = await fetch(u, {
method: "POST", method: "POST",
credentials: "include",
}); });
const setVal = setAtomValue(userLoginStateAtom); const setVal = setAtomValue(userLoginStateAtom);
try { try {
@ -79,11 +81,12 @@ export const doLogin = async (userLoginInfo: {
username: string; username: string;
password: string; password: string;
}): Promise<string | LoginLocalStorage> => { }): Promise<string | LoginLocalStorage> => {
const u = new URL("/user/refresh", BASE_API_URL); const u = new URL("/api/user/login", BASE_API_URL);
const res = await fetch(u, { const res = await fetch(u, {
method: "POST", method: "POST",
body: JSON.stringify(userLoginInfo), body: JSON.stringify(userLoginInfo),
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
credentials: "include",
}); });
const b = await res.json(); const b = await res.json();
if (res.status !== 200) { if (res.status !== 200) {

View File

@ -1,3 +1,3 @@
{ {
"watch": [] "watch": ["testdata"]
} }

View File

@ -1,5 +1,5 @@
import { extname } from "node:path"; import { extname } from "node:path";
import type { DocumentBody } from "../model/doc"; import type { DocumentBody } from "dbtype/api";
import { readZip } from "../util/zipwrap"; import { readZip } from "../util/zipwrap";
import { type ContentConstructOption, createDefaultClass, registerContentReferrer } from "./file"; import { type ContentConstructOption, createDefaultClass, registerContentReferrer } from "./file";
import { TextWriter } from "@zip.js/zip.js"; import { TextWriter } from "@zip.js/zip.js";
@ -29,19 +29,18 @@ export class ComicReferrer extends createDefaultClass("comic") {
const zip = await readZip(this.path); const zip = await readZip(this.path);
const entries = await zip.reader.getEntries(); const entries = await zip.reader.getEntries();
this.pagenum = entries.filter((x) => ImageExt.includes(extname(x.filename))).length; this.pagenum = entries.filter((x) => ImageExt.includes(extname(x.filename))).length;
const entry = entries.find(x=> x.filename === "desc.json"); const descEntry = entries.find(x=> x.filename === "desc.json");
if (entry === undefined) { if (descEntry === undefined) {
return; return;
} }
if (entry.getData === undefined) { if (descEntry.getData === undefined) {
throw new Error("entry.getData is undefined"); throw new Error("entry.getData is undefined");
} }
const textWriter = new TextWriter(); const textWriter = new TextWriter();
const data = (await entry.getData(textWriter)); const data = (await descEntry.getData(textWriter));
this.desc = JSON.parse(data); this.desc = JSON.parse(data);
if (this.desc === undefined) { zip.reader.close()
throw new Error(`JSON.parse is returning undefined. ${this.path} desc.json format error`); .then(() => zip.handle.close());
}
} }
async createDocumentBody(): Promise<DocumentBody> { async createDocumentBody(): Promise<DocumentBody> {

View File

@ -1,9 +1,7 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { promises, type Stats } from "node:fs"; import { promises, type Stats } from "node:fs";
import { Context, DefaultContext, DefaultState, Middleware, Next } from "koa";
import Router from "koa-router";
import path, { extname } from "node:path"; import path, { extname } from "node:path";
import type { DocumentBody } from "../model/mod"; import type { DocumentBody } from "dbtype/api";
/** /**
* content file or directory referrer * content file or directory referrer
*/ */
@ -40,8 +38,9 @@ export const createDefaultClass = (type: string): ContentFileConstructor => {
this.stat = undefined; this.stat = undefined;
} }
async createDocumentBody(): Promise<DocumentBody> { async createDocumentBody(): Promise<DocumentBody> {
console.log(`createDocumentBody: ${this.path}`);
const { base, dir, name } = path.parse(this.path); const { base, dir, name } = path.parse(this.path);
const ret = { const ret = {
title: name, title: name,
basepath: dir, basepath: dir,

View File

@ -73,11 +73,13 @@ class SqliteDocumentAccessor implements DocumentAccessor {
await trx.insertInto("tags") await trx.insertInto("tags")
.values(tags.map((x) => ({ name: x }))) .values(tags.map((x) => ({ name: x })))
// on conflict is supported in sqlite and postgresql. // on conflict is supported in sqlite and postgresql.
.onConflict((oc) => oc.doNothing()); .onConflict((oc) => oc.doNothing())
.execute();
if (tags.length > 0) { if (tags.length > 0) {
await trx.insertInto("doc_tag_relation") await trx.insertInto("doc_tag_relation")
.values(tags.map((x) => ({ doc_id: id, tag_name: x }))); .values(tags.map((x) => ({ doc_id: id, tag_name: x })))
.execute();
} }
return id; return id;
}); });

View File

@ -24,6 +24,7 @@ export class DiffManager {
throw new Error("path is not exist"); throw new Error("path is not exist");
} }
await list.delete(c); await list.delete(c);
console.log(`commit: ${c.path} ${c.type}`);
const body = await c.createDocumentBody(); const body = await c.createDocumentBody();
const id = await this.doc_cntr.add(body); const id = await this.doc_cntr.add(body);
return id; return id;

View File

@ -23,7 +23,7 @@ type PostAddedBody = {
path: string; path: string;
}[]; }[];
function checkPostAddedBody(body: any): body is PostAddedBody { function checkPostAddedBody(body: unknown): body is PostAddedBody {
if (Array.isArray(body)) { if (Array.isArray(body)) {
return body.map((x) => "type" in x && "path" in x).every((x) => x); return body.map((x) => "type" in x && "path" in x).every((x) => x);
} }
@ -43,6 +43,7 @@ export const postAdded = (diffmgr: DiffManager) => async (ctx: Router.IRouterCon
docs: results, docs: results,
}; };
ctx.type = "json"; ctx.type = "json";
await next();
}; };
export const postAddedAll = (diffmgr: DiffManager) => async (ctx: Router.IRouterContext, next: Koa.Next) => { export const postAddedAll = (diffmgr: DiffManager) => async (ctx: Router.IRouterContext, next: Koa.Next) => {
if (!ctx.is("json")) { if (!ctx.is("json")) {
@ -64,6 +65,7 @@ export const postAddedAll = (diffmgr: DiffManager) => async (ctx: Router.IRouter
ok: true, ok: true,
}; };
ctx.type = "json"; ctx.type = "json";
await next();
}; };
/* /*
export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContext,next:Koa.Next)=>{ export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContext,next:Koa.Next)=>{

View File

@ -1,9 +1,6 @@
import { request } from "node:http"; import { sign, TokenExpiredError, verify } from "jsonwebtoken";
import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken";
import Knex from "knex";
import type Koa from "koa"; import type Koa from "koa";
import Router from "koa-router"; import Router from "koa-router";
import { createSqliteUserController } from "./db/mod";
import type { IUser, UserAccessor } from "./model/mod"; import type { IUser, UserAccessor } from "./model/mod";
import { sendError } from "./route/error_handler"; import { sendError } from "./route/error_handler";
import { get_setting } from "./SettingConfig"; import { get_setting } from "./SettingConfig";

View File

@ -34,7 +34,7 @@ export async function readZip(path: string): Promise<{
const fd = await open(path, "r"); const fd = await open(path, "r");
const reader = new ZipReader(new FileReader(fd), { const reader = new ZipReader(new FileReader(fd), {
useCompressionStream: true, useCompressionStream: true,
preventClose: true, preventClose: false,
}); });
return { reader, handle: fd }; return { reader, handle: fd };
} }

View File

@ -23,6 +23,9 @@ importers:
'@radix-ui/react-radio-group': '@radix-ui/react-radio-group':
specifier: ^1.1.3 specifier: ^1.1.3
version: 1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0) version: 1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-separator':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': '@radix-ui/react-slot':
specifier: ^1.0.2 specifier: ^1.0.2
version: 1.0.2(@types/react@18.2.71)(react@18.2.0) version: 1.0.2(@types/react@18.2.71)(react@18.2.0)
@ -1284,6 +1287,27 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.24.1
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.71
'@types/react-dom': 18.2.22
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-slot@1.0.2(@types/react@18.2.71)(react@18.2.0): /@radix-ui/react-slot@1.0.2(@types/react@18.2.71)(react@18.2.0):
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies: peerDependencies: