Lazy Image Load
This commit is contained in:
parent
23922ed100
commit
88e81853e6
@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@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",
|
||||||
|
@ -19,19 +19,20 @@ import { TooltipProvider } from "./components/ui/tooltip";
|
|||||||
import Gallery from "./page/galleryPage";
|
import Gallery from "./page/galleryPage";
|
||||||
import Layout from "./components/layout/layout";
|
import Layout from "./components/layout/layout";
|
||||||
import NotFoundPage from "./page/404";
|
import NotFoundPage from "./page/404";
|
||||||
|
import LoginPage from "./page/loginPage";
|
||||||
|
import ProfilePage from "./page/profilesPage";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={() => <Redirect replace to="/search?" />} />
|
<Route path="/" component={() => <Redirect replace to="/search?" />} />
|
||||||
<Route path="/search" component={Gallery} />
|
<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={<DocumentAbout />}></Route>
|
||||||
<Route path="/doc/:id/reader" component={<ReaderPage />}></Route>
|
<Route path="/doc/:id/reader" component={<ReaderPage />}></Route>
|
||||||
<Route path="/login" component={<LoginPage></LoginPage>} />
|
|
||||||
<Route path="/profile" component={<ProfilePage />}></Route>
|
|
||||||
<Route path="/difference" component={<DifferencePage />}></Route>
|
<Route path="/difference" component={<DifferencePage />}></Route>
|
||||||
<Route path="/setting" component={<SettingPage />}></Route>
|
<Route path="/setting" component={<SettingPage />}></Route>
|
||||||
<Route path="/tags" component={<TagsPage />}></Route>*/}
|
<Route path="/tags" component={<TagsPage />}></Route>*/}
|
||||||
|
@ -1,273 +0,0 @@
|
|||||||
import { AccountCircle, ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon } from "@mui/icons-material";
|
|
||||||
import {
|
|
||||||
AppBar,
|
|
||||||
Button,
|
|
||||||
CssBaseline,
|
|
||||||
Divider,
|
|
||||||
Drawer,
|
|
||||||
Hidden,
|
|
||||||
IconButton,
|
|
||||||
InputBase,
|
|
||||||
Link,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
styled,
|
|
||||||
Toolbar,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { alpha, Theme, useTheme } from "@mui/material/styles";
|
|
||||||
import React, { useContext, useState } from "react";
|
|
||||||
|
|
||||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
|
||||||
import { doLogout, UserContext } from "../state";
|
|
||||||
|
|
||||||
const drawerWidth = 270;
|
|
||||||
|
|
||||||
const DrawerHeader = styled("div")(({ theme }) => ({
|
|
||||||
...theme.mixins.toolbar,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDrawer = styled(Drawer)(({ theme }) => ({
|
|
||||||
flexShrink: 0,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
[theme.breakpoints.up("sm")]: {
|
|
||||||
width: drawerWidth,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
const StyledSearchBar = styled("div")(({ theme }) => ({
|
|
||||||
position: "relative",
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: alpha(theme.palette.common.white, 0.25),
|
|
||||||
},
|
|
||||||
marginLeft: 0,
|
|
||||||
width: "100%",
|
|
||||||
[theme.breakpoints.up("sm")]: {
|
|
||||||
marginLeft: theme.spacing(1),
|
|
||||||
width: "auto",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
const StyledInputBase = styled(InputBase)(({ theme }) => ({
|
|
||||||
color: "inherit",
|
|
||||||
"& .MuiInputBase-input": {
|
|
||||||
padding: theme.spacing(1, 1, 1, 0),
|
|
||||||
// vertical padding + font size from searchIcon
|
|
||||||
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
|
||||||
transition: theme.transitions.create("width"),
|
|
||||||
width: "100%",
|
|
||||||
[theme.breakpoints.up("sm")]: {
|
|
||||||
width: "12ch",
|
|
||||||
"&:focus": {
|
|
||||||
width: "20ch",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledNav = styled("nav")(({ theme }) => ({
|
|
||||||
[theme.breakpoints.up("sm")]: {
|
|
||||||
width: theme.spacing(7),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const closedMixin = (theme: Theme) => ({
|
|
||||||
overflowX: "hidden",
|
|
||||||
width: `calc(${theme.spacing(7)} + 1px)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Headline = (prop: {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
classes?: {
|
|
||||||
content?: string;
|
|
||||||
toolbar?: string;
|
|
||||||
};
|
|
||||||
rightAppbar?: React.ReactNode;
|
|
||||||
menu: React.ReactNode;
|
|
||||||
}) => {
|
|
||||||
const [v, setv] = useState(false);
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
||||||
const theme = useTheme();
|
|
||||||
const toggleV = () => setv(!v);
|
|
||||||
const handleProfileMenuOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget);
|
|
||||||
const handleProfileMenuClose = () => setAnchorEl(null);
|
|
||||||
const isProfileMenuOpened = Boolean(anchorEl);
|
|
||||||
const menuId = "primary-search-account-menu";
|
|
||||||
const user_ctx = useContext(UserContext);
|
|
||||||
const isLogin = user_ctx.username !== "";
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
const renderProfileMenu = (
|
|
||||||
<Menu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{ horizontal: "right", vertical: "top" }}
|
|
||||||
id={menuId}
|
|
||||||
open={isProfileMenuOpened}
|
|
||||||
keepMounted
|
|
||||||
transformOrigin={{ horizontal: "right", vertical: "top" }}
|
|
||||||
onClose={handleProfileMenuClose}
|
|
||||||
>
|
|
||||||
<MenuItem component={RouterLink} to="/profile">
|
|
||||||
Profile
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={async () => {
|
|
||||||
handleProfileMenuClose();
|
|
||||||
await doLogout();
|
|
||||||
user_ctx.setUsername("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
const drawer_contents = (
|
|
||||||
<>
|
|
||||||
<DrawerHeader>
|
|
||||||
<IconButton onClick={toggleV}>{theme.direction === "ltr" ? <ChevronLeft /> : <ChevronRight />}</IconButton>
|
|
||||||
</DrawerHeader>
|
|
||||||
<Divider />
|
|
||||||
{prop.menu}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: "flex" }}>
|
|
||||||
<CssBaseline />
|
|
||||||
<AppBar
|
|
||||||
position="fixed"
|
|
||||||
sx={{
|
|
||||||
zIndex: theme.zIndex.drawer + 1,
|
|
||||||
transition: theme.transitions.create(["width", "margin"], {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.leavingScreen,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Toolbar>
|
|
||||||
<IconButton
|
|
||||||
color="inherit"
|
|
||||||
aria-label="open drawer"
|
|
||||||
onClick={toggleV}
|
|
||||||
edge="start"
|
|
||||||
style={{ marginRight: 36 }}
|
|
||||||
>
|
|
||||||
<MenuIcon></MenuIcon>
|
|
||||||
</IconButton>
|
|
||||||
<Link
|
|
||||||
variant="h5"
|
|
||||||
noWrap
|
|
||||||
sx={{
|
|
||||||
display: "none",
|
|
||||||
[theme.breakpoints.up("sm")]: {
|
|
||||||
display: "block",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
color="inherit"
|
|
||||||
component={RouterLink}
|
|
||||||
to="/"
|
|
||||||
>
|
|
||||||
Ionian
|
|
||||||
</Link>
|
|
||||||
<div style={{ flexGrow: 1 }}></div>
|
|
||||||
{prop.rightAppbar}
|
|
||||||
<StyledSearchBar>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: theme.spacing(0, 2),
|
|
||||||
height: "100%",
|
|
||||||
position: "absolute",
|
|
||||||
pointerEvents: "none",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SearchIcon onClick={() => navSearch(search)} />
|
|
||||||
</div>
|
|
||||||
<StyledInputBase
|
|
||||||
placeholder="search"
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
onKeyUp={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
navSearch(search);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={search}
|
|
||||||
/>
|
|
||||||
</StyledSearchBar>
|
|
||||||
{isLogin ? (
|
|
||||||
<IconButton
|
|
||||||
edge="end"
|
|
||||||
aria-label="account of current user"
|
|
||||||
aria-controls={menuId}
|
|
||||||
aria-haspopup="true"
|
|
||||||
onClick={handleProfileMenuOpen}
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<AccountCircle />
|
|
||||||
</IconButton>
|
|
||||||
) : (
|
|
||||||
<Button color="inherit" component={RouterLink} to="/login">
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
{renderProfileMenu}
|
|
||||||
<StyledNav>
|
|
||||||
<Hidden smUp implementation="css">
|
|
||||||
<StyledDrawer
|
|
||||||
variant="temporary"
|
|
||||||
anchor="left"
|
|
||||||
open={v}
|
|
||||||
onClose={toggleV}
|
|
||||||
sx={{
|
|
||||||
width: drawerWidth,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{drawer_contents}
|
|
||||||
</StyledDrawer>
|
|
||||||
</Hidden>
|
|
||||||
<Hidden smDown implementation="css">
|
|
||||||
<StyledDrawer
|
|
||||||
variant="permanent"
|
|
||||||
anchor="left"
|
|
||||||
sx={{
|
|
||||||
...closedMixin(theme),
|
|
||||||
"& .MuiDrawer-paper": closedMixin(theme),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{drawer_contents}
|
|
||||||
</StyledDrawer>
|
|
||||||
</Hidden>
|
|
||||||
</StyledNav>
|
|
||||||
<main
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexFlow: "column",
|
|
||||||
flexGrow: 1,
|
|
||||||
padding: "0px",
|
|
||||||
marginTop: "64px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{prop.children}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
function navSearch(search: string) {
|
|
||||||
let words = search.includes("&") ? search.split("&") : [search];
|
|
||||||
words = words
|
|
||||||
.map((w) => w.trim())
|
|
||||||
.map((w) => (w.includes(":") ? `allow_tag=${w}` : `word=${encodeURIComponent(w)}`));
|
|
||||||
navigate(`/search?${words.join("&")}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Headline;
|
|
@ -1,10 +0,0 @@
|
|||||||
import { Box, CircularProgress } from "@mui/material";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const LoadingCircle = () => {
|
|
||||||
return (
|
|
||||||
<Box style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%,-50%)" }}>
|
|
||||||
<CircularProgress title="loading" />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,54 +0,0 @@
|
|||||||
import {
|
|
||||||
ArrowBack as ArrowBackIcon,
|
|
||||||
Collections as CollectionIcon,
|
|
||||||
Folder as FolderIcon,
|
|
||||||
Home as HomeIcon,
|
|
||||||
List as ListIcon,
|
|
||||||
Settings as SettingIcon,
|
|
||||||
VideoLibrary as VideoIcon,
|
|
||||||
} from "@mui/icons-material";
|
|
||||||
import { Divider, List, ListItem, ListItemIcon, ListItemText, Tooltip } from "@mui/material";
|
|
||||||
import React from "react";
|
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
|
||||||
|
|
||||||
export const NavItem = (props: { name: string; to: string; icon: React.ReactElement<any, any> }) => {
|
|
||||||
return (
|
|
||||||
<ListItem button key={props.name} component={RouterLink} to={props.to}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Tooltip title={props.name.toLocaleLowerCase()} placement="bottom">
|
|
||||||
{props.icon}
|
|
||||||
</Tooltip>
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={props.name}></ListItemText>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NavList = (props: { children?: React.ReactNode }) => {
|
|
||||||
return <List>{props.children}</List>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BackItem = (props: { to?: string }) => {
|
|
||||||
return <NavItem name="Back" to={props.to ?? "/"} icon={<ArrowBackIcon></ArrowBackIcon>} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CommonMenuList(props?: { url?: string }) {
|
|
||||||
let url = props?.url ?? "";
|
|
||||||
return (
|
|
||||||
<NavList>
|
|
||||||
{url !== "" && (
|
|
||||||
<>
|
|
||||||
<BackItem to={url} /> <Divider />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<NavItem name="All" to="/" icon={<HomeIcon />} />
|
|
||||||
<NavItem name="Comic" to="/search?content_type=comic" icon={<CollectionIcon />}></NavItem>
|
|
||||||
<NavItem name="Video" to="/search?content_type=video" icon={<VideoIcon />} />
|
|
||||||
<Divider />
|
|
||||||
<NavItem name="Tags" to="/tags" icon={<ListIcon />} />
|
|
||||||
<Divider />
|
|
||||||
<NavItem name="Difference" to="/difference" icon={<FolderIcon />}></NavItem>
|
|
||||||
<NavItem name="Settings" to="/setting" icon={<SettingIcon />} />
|
|
||||||
</NavList>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import { styled } from "@mui/material";
|
|
||||||
|
|
||||||
export const PagePad = styled("div")(({ theme }) => ({
|
|
||||||
padding: theme.spacing(3),
|
|
||||||
}));
|
|
@ -1,80 +0,0 @@
|
|||||||
import * as colors from "@mui/material/colors";
|
|
||||||
import Chip, { ChipTypeMap } from "@mui/material/Chip";
|
|
||||||
import { emphasize, styled, Theme, useTheme } from "@mui/material/styles";
|
|
||||||
import React from "react";
|
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
|
||||||
|
|
||||||
type TagChipStyleProp = {
|
|
||||||
color: `rgba(${number},${number},${number},${number})` | `#${string}` | "default";
|
|
||||||
};
|
|
||||||
|
|
||||||
const { blue, pink } = colors;
|
|
||||||
const getTagColorName = (tagname: string): TagChipStyleProp["color"] => {
|
|
||||||
if (tagname.startsWith("female")) {
|
|
||||||
return pink[600];
|
|
||||||
} else if (tagname.startsWith("male")) {
|
|
||||||
return blue[600];
|
|
||||||
} else return "default";
|
|
||||||
};
|
|
||||||
|
|
||||||
type ColorChipProp = Omit<ChipTypeMap["props"], "color"> &
|
|
||||||
TagChipStyleProp & {
|
|
||||||
component?: React.ElementType;
|
|
||||||
to?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ColorChip = (props: ColorChipProp) => {
|
|
||||||
const { color, ...rest } = props;
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
let newcolor = color;
|
|
||||||
if (color === "default") {
|
|
||||||
newcolor = "#ebebeb";
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Chip
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.getContrastText(newcolor),
|
|
||||||
backgroundColor: newcolor,
|
|
||||||
["&:hover, &:focus"]: {
|
|
||||||
backgroundColor: emphasize(newcolor, 0.08),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
></Chip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type TagChipProp = Omit<ChipTypeMap["props"], "color"> & {
|
|
||||||
tagname: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TagChip = (props: TagChipProp) => {
|
|
||||||
const { tagname, label, clickable, ...rest } = props;
|
|
||||||
const colorName = getTagColorName(tagname);
|
|
||||||
|
|
||||||
let newlabel: React.ReactNode = label;
|
|
||||||
if (typeof label === "string") {
|
|
||||||
const female = "female:";
|
|
||||||
const male = "male:";
|
|
||||||
if (label.startsWith(female)) {
|
|
||||||
newlabel = "♀ " + label.slice(female.length);
|
|
||||||
} else if (label.startsWith(male)) {
|
|
||||||
newlabel = "♂ " + label.slice(male.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const inner = clickable ? (
|
|
||||||
<ColorChip
|
|
||||||
color={colorName}
|
|
||||||
clickable={clickable}
|
|
||||||
label={newlabel ?? label}
|
|
||||||
{...rest}
|
|
||||||
component={RouterLink}
|
|
||||||
to={`/search?allow_tag=${tagname}`}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ColorChip color={colorName} clickable={clickable} label={newlabel ?? label} {...rest} />
|
|
||||||
);
|
|
||||||
return inner;
|
|
||||||
};
|
|
@ -15,6 +15,43 @@ function clipTagsWhenOverflow(tags: string[], limit: number) {
|
|||||||
return tags;
|
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({
|
export function GalleryCard({
|
||||||
doc: x
|
doc: x
|
||||||
}: { doc: Document; }) {
|
}: { doc: Document; }) {
|
||||||
@ -42,11 +79,12 @@ export function GalleryCard({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <Card key={x.id} className="flex h-[200px]">
|
return <Card className="flex h-[200px]">
|
||||||
<div className="rounded-xl overflow-hidden h-[200px] w-[142px] flex-none bg-[#272733] flex items-center justify-center">
|
<div className="rounded-xl overflow-hidden h-[200px] w-[142px] flex-none bg-[#272733] flex items-center justify-center">
|
||||||
<img src={`/api/doc/${x.id}/comic/thumbnail`}
|
<LazyImage src={`/api/doc/${x.id}/comic/thumbnail`}
|
||||||
alt={x.title}
|
alt={x.title}
|
||||||
className="max-h-full max-w-full object-cover object-center" />
|
className="max-h-full max-w-full object-cover object-center"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<CardHeader className="flex-none">
|
<CardHeader className="flex-none">
|
||||||
@ -56,10 +94,10 @@ export function GalleryCard({
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1" ref={ref}>
|
<CardContent className="flex-1" ref={ref}>
|
||||||
<li className="flex flex-wrap gap-2 items-baseline content-start">
|
<ul className="flex flex-wrap gap-2 items-baseline content-start">
|
||||||
{clippedTags.map(tag => <TagBadge key={tag} tagname={tag} className="" />)}
|
{clippedTags.map(tag => <TagBadge key={tag} tagname={tag} className="" />)}
|
||||||
{clippedTags.length < originalTags.length && <TagBadge tagname="..." className="" />}
|
{clippedTags.length < originalTags.length && <TagBadge tagname="..." className="" disabled />}
|
||||||
</li>
|
</ul>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
</Card>;
|
</Card>;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Badge, badgeVariants } from "@/components/ui/badge";
|
import { badgeVariants } from "@/components/ui/badge";
|
||||||
|
import { Link } from "wouter";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const femaleTagPrefix = "female:";
|
const femaleTagPrefix = "female:";
|
||||||
@ -26,7 +27,7 @@ function toPrettyTagname(tagname: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TagBadge(props: { tagname: string, className?: string}) {
|
export default function TagBadge(props: { tagname: string, className?: string; disabled?: boolean;}) {
|
||||||
const { tagname } = props;
|
const { tagname } = props;
|
||||||
const kind = getTagKind(tagname);
|
const kind = getTagKind(tagname);
|
||||||
return <li className={
|
return <li className={
|
||||||
@ -35,7 +36,8 @@ export default function TagBadge(props: { tagname: string, className?: string})
|
|||||||
kind === "male" && "bg-[#2a7bbf] hover:bg-[#3091e7]",
|
kind === "male" && "bg-[#2a7bbf] hover:bg-[#3091e7]",
|
||||||
kind === "female" && "bg-[#c21f58] hover:bg-[#db2d67]",
|
kind === "female" && "bg-[#c21f58] hover:bg-[#db2d67]",
|
||||||
kind === "default" && "bg-[#4a5568] hover:bg-[#718096]",
|
kind === "default" && "bg-[#4a5568] hover:bg-[#718096]",
|
||||||
|
props.disabled && "opacity-50",
|
||||||
props.className,
|
props.className,
|
||||||
)
|
)
|
||||||
}>{toPrettyTagname(tagname)}</li>;
|
}><Link to={props.disabled ? '': `/search?allow_tag=${tagname}`}>{toPrettyTagname(tagname)}</Link></li>;
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
|
|||||||
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 "../ui/button"
|
import { buttonVariants } from "../ui/button"
|
||||||
|
import { useLogin } from "@/state/user";
|
||||||
|
|
||||||
interface NavItemProps {
|
interface NavItemProps {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
@ -29,6 +30,8 @@ export function NavItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NavList() {
|
export function NavList() {
|
||||||
|
const loginInfo = useLogin();
|
||||||
|
|
||||||
return <aside className="h-screen flex flex-col">
|
return <aside className="h-screen 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">
|
||||||
<NavItem icon={<MagnifyingGlassIcon className="h-5 w-5" />} to="/search" name="Search" />
|
<NavItem icon={<MagnifyingGlassIcon className="h-5 w-5" />} to="/search" name="Search" />
|
||||||
@ -36,7 +39,7 @@ export function NavList() {
|
|||||||
<NavItem icon={<ArchiveIcon className="h-5 w-5" />} to="/difference" name="Difference" />
|
<NavItem icon={<ArchiveIcon className="h-5 w-5" />} to="/difference" name="Difference" />
|
||||||
</nav>
|
</nav>
|
||||||
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5 flex-grow-0">
|
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5 flex-grow-0">
|
||||||
<NavItem icon={<PersonIcon className="h-5 w-5" />} to="/profile" name="Profiles" />
|
<NavItem icon={<PersonIcon className="h-5 w-5" />} to={ loginInfo ? "/profile" : "/login"} name={ loginInfo ? "Profiles" : "Login"} />
|
||||||
<NavItem icon={<GearIcon className="h-5 w-5" />} to="/setting" name="Settings" />
|
<NavItem icon={<GearIcon className="h-5 w-5" />} to="/setting" name="Settings" />
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
24
packages/client/src/components/ui/label.tsx
Normal file
24
packages/client/src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
@ -54,7 +54,7 @@ export default function Gallery() {
|
|||||||
return <div className="p-4">Error: {String(error)}</div>
|
return <div className="p-4">Error: {String(error)}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div className="p-4 grid gap-2 overflow-auto h-screen">
|
return (<div className="p-4 grid gap-2 overflow-auto h-screen items-start content-start">
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Input className="flex-1"/>
|
<Input className="flex-1"/>
|
||||||
<Button className="flex-none">Search</Button>
|
<Button className="flex-none">Search</Button>
|
||||||
@ -66,12 +66,12 @@ export default function Gallery() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
data?.length === 0 && <div className="p-4">No results</div>
|
data?.length === 0 && <div className="p-4 text-3xl">No results</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
data?.map((x) => {
|
data?.map((x) => {
|
||||||
return (
|
return (
|
||||||
<GalleryCard doc={x} />
|
<GalleryCard doc={x} key={x.id} />
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
58
packages/client/src/page/loginPage.tsx
Normal file
58
packages/client/src/page/loginPage.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { doLogin } from "@/state/user";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
|
export function LoginForm() {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [, setLocation] = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl">Login</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your email below to login to your account.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<Input id="username" type="text" placeholder="username" required value={username} onChange={e=> setUsername(e.target.value)}/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input id="password" type="password" required value={password} onChange={e=> setPassword(e.target.value)}/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button className="w-full" onClick={()=>{
|
||||||
|
doLogin({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}).then((r)=>{
|
||||||
|
if (typeof r === "string") {
|
||||||
|
alert(r);
|
||||||
|
} else {
|
||||||
|
setLocation("/");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}>Sign in</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage;
|
@ -1,8 +0,0 @@
|
|||||||
export * from "./404";
|
|
||||||
export * from "./contentinfo";
|
|
||||||
export * from "./difference";
|
|
||||||
export * from "./gallery";
|
|
||||||
export * from "./login";
|
|
||||||
export * from "./profile";
|
|
||||||
export * from "./setting";
|
|
||||||
export * from "./tags";
|
|
32
packages/client/src/page/profilesPage.tsx
Normal file
32
packages/client/src/page/profilesPage.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { useLogin } from "@/state/user";
|
||||||
|
import { Redirect } from "wouter";
|
||||||
|
|
||||||
|
export function ProfilePage() {
|
||||||
|
const userInfo = useLogin();
|
||||||
|
if (!userInfo) {
|
||||||
|
console.error("User session expired. Redirecting to login page.");
|
||||||
|
return <Redirect to="/login" />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="m-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl">Profile</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid">
|
||||||
|
<span className="text-muted-foreground text-sm">Username</span>
|
||||||
|
<span className="text-primary text-lg">{userInfo.username}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid">
|
||||||
|
<span className="text-muted-foreground text-sm">Permission</span>
|
||||||
|
<span className="text-primary text-lg">{userInfo.permission.length > 1 ? userInfo.permission.join(",") : "N/A"}</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProfilePage;
|
@ -1,94 +0,0 @@
|
|||||||
import React, { createContext, useRef, useState } from "react";
|
|
||||||
|
|
||||||
export const UserContext = createContext({
|
|
||||||
username: "",
|
|
||||||
permission: [] as string[],
|
|
||||||
setUsername: (s: string) => {},
|
|
||||||
setPermission: (permission: string[]) => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
type LoginLocalStorage = {
|
|
||||||
username: string;
|
|
||||||
permission: string[];
|
|
||||||
accessExpired: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
let localObj: LoginLocalStorage | null = null;
|
|
||||||
|
|
||||||
export const getInitialValue = async () => {
|
|
||||||
if (localObj === null) {
|
|
||||||
const storagestr = window.localStorage.getItem("UserLoginContext") as string | null;
|
|
||||||
const storage = storagestr !== null ? (JSON.parse(storagestr) as LoginLocalStorage | null) : null;
|
|
||||||
localObj = storage;
|
|
||||||
}
|
|
||||||
if (localObj !== null && localObj.accessExpired > Math.floor(Date.now() / 1000)) {
|
|
||||||
return {
|
|
||||||
username: localObj.username,
|
|
||||||
permission: localObj.permission,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const res = await fetch("/user/refresh", {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
if (res.status !== 200) throw new Error("Maybe Network Error");
|
|
||||||
const r = (await res.json()) as LoginLocalStorage & { refresh: boolean };
|
|
||||||
if (r.refresh) {
|
|
||||||
localObj = {
|
|
||||||
username: r.username,
|
|
||||||
permission: r.permission,
|
|
||||||
accessExpired: r.accessExpired,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
localObj = {
|
|
||||||
accessExpired: 0,
|
|
||||||
username: "",
|
|
||||||
permission: r.permission,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
|
||||||
return {
|
|
||||||
username: r.username,
|
|
||||||
permission: r.permission,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export const doLogout = async () => {
|
|
||||||
const req = await fetch("/user/logout", {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
const res = await req.json();
|
|
||||||
localObj = {
|
|
||||||
accessExpired: 0,
|
|
||||||
username: "",
|
|
||||||
permission: res["permission"],
|
|
||||||
};
|
|
||||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
|
||||||
return {
|
|
||||||
username: localObj.username,
|
|
||||||
permission: localObj.permission,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Server Error ${error}`);
|
|
||||||
return {
|
|
||||||
username: "",
|
|
||||||
permission: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export const doLogin = async (userLoginInfo: {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}): Promise<string | LoginLocalStorage> => {
|
|
||||||
const res = await fetch("/user/login", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(userLoginInfo),
|
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
});
|
|
||||||
const b = await res.json();
|
|
||||||
if (res.status !== 200) {
|
|
||||||
return b.detail as string;
|
|
||||||
}
|
|
||||||
localObj = b;
|
|
||||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
|
||||||
return b;
|
|
||||||
};
|
|
@ -47,7 +47,7 @@ async function refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const doLogout = async () => {
|
export const doLogout = async () => {
|
||||||
const req = await fetch("/user/logout", {
|
const req = await fetch("/api/user/logout", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
const setVal = setAtomValue(userLoginStateAtom);
|
const setVal = setAtomValue(userLoginStateAtom);
|
||||||
@ -76,7 +76,7 @@ export const doLogin = async (userLoginInfo: {
|
|||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}): Promise<string | LoginLocalStorage> => {
|
}): Promise<string | LoginLocalStorage> => {
|
||||||
const res = await fetch("/user/login", {
|
const res = await fetch("/api/user/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(userLoginInfo),
|
body: JSON.stringify(userLoginInfo),
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
|
@ -10,46 +10,67 @@ import { Readable, Writable } from "node:stream";
|
|||||||
/**
|
/**
|
||||||
* zip stream cache.
|
* zip stream cache.
|
||||||
*/
|
*/
|
||||||
const ZipStreamCache: {
|
const ZipStreamCache = new Map<string, {
|
||||||
[path: string]: [{
|
reader: ZipReader<FileHandle>,
|
||||||
reader: ZipReader<FileHandle>,
|
handle: FileHandle,
|
||||||
handle: FileHandle
|
refCount: number,
|
||||||
}, number]
|
}>();
|
||||||
} = {};
|
|
||||||
|
|
||||||
async function acquireZip(path: string) {
|
|
||||||
if (!(path in ZipStreamCache)) {
|
function markUseZip(path: string) {
|
||||||
|
const ret = ZipStreamCache.get(path);
|
||||||
|
if (ret) {
|
||||||
|
ret.refCount++;
|
||||||
|
}
|
||||||
|
return ret !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acquireZip(path: string, marked = false) {
|
||||||
|
const ret = ZipStreamCache.get(path);
|
||||||
|
if (!ret) {
|
||||||
const obj = await readZip(path);
|
const obj = await readZip(path);
|
||||||
ZipStreamCache[path] = [obj, 1];
|
const check = ZipStreamCache.get(path);
|
||||||
// console.log(`acquire ${path} 1`);
|
if (check) {
|
||||||
|
check.refCount++;
|
||||||
|
// if the cache is updated, release the previous one.
|
||||||
|
releaseZip(path);
|
||||||
|
return check.reader;
|
||||||
|
}
|
||||||
|
// if the cache is not updated, set the new one.
|
||||||
|
ZipStreamCache.set(path, {
|
||||||
|
reader: obj.reader,
|
||||||
|
handle: obj.handle,
|
||||||
|
refCount: 1,
|
||||||
|
});
|
||||||
return obj.reader;
|
return obj.reader;
|
||||||
}
|
}
|
||||||
const [ret, refCount] = ZipStreamCache[path];
|
if (!marked) {
|
||||||
ZipStreamCache[path] = [ret, refCount + 1];
|
ret.refCount++;
|
||||||
// console.log(`acquire ${path} ${refCount + 1}`);
|
}
|
||||||
return ret.reader;
|
return ret.reader;
|
||||||
}
|
}
|
||||||
|
|
||||||
function releaseZip(path: string) {
|
function releaseZip(path: string) {
|
||||||
const obj = ZipStreamCache[path];
|
const obj = ZipStreamCache.get(path);
|
||||||
if (obj === undefined) throw new Error("error! key invalid");
|
if (obj === undefined) {
|
||||||
const [ref, refCount] = obj;
|
console.warn(`warning! duplicate release at ${path}`);
|
||||||
// console.log(`release ${path} : ${refCount}`);
|
return;
|
||||||
if (refCount === 1) {
|
}
|
||||||
const { reader, handle } = ref;
|
if (obj.refCount === 1) {
|
||||||
|
const { reader, handle } = obj;
|
||||||
reader.close().then(() => {
|
reader.close().then(() => {
|
||||||
handle.close();
|
handle.close();
|
||||||
});
|
});
|
||||||
delete ZipStreamCache[path];
|
ZipStreamCache.delete(path);
|
||||||
} else {
|
} else {
|
||||||
ZipStreamCache[path] = [ref, refCount - 1];
|
obj.refCount--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderZipImage(ctx: Context, path: string, page: number) {
|
async function renderZipImage(ctx: Context, path: string, page: number) {
|
||||||
const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"];
|
const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"];
|
||||||
// console.log(`opened ${page}`);
|
const marked = markUseZip(path);
|
||||||
const zip = await acquireZip(path);
|
const zip = await acquireZip(path, marked);
|
||||||
const entries = (await entriesByNaturalOrder(zip)).filter((x) => {
|
const entries = (await entriesByNaturalOrder(zip)).filter((x) => {
|
||||||
const ext = x.filename.split(".").pop();
|
const ext = x.filename.split(".").pop();
|
||||||
return ext !== undefined && image_ext.includes(ext);
|
return ext !== undefined && image_ext.includes(ext);
|
||||||
@ -70,9 +91,9 @@ async function renderZipImage(ctx: Context, path: string, page: number) {
|
|||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
nodeReadableStream.push(null);
|
nodeReadableStream.push(null);
|
||||||
setTimeout(() => {
|
releaseZip(path);
|
||||||
releaseZip(path);
|
// setTimeout(() => {
|
||||||
}, 100);
|
// }, 500);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -92,8 +92,8 @@ class ServerApplication {
|
|||||||
this.serve_static_file(router);
|
this.serve_static_file(router);
|
||||||
|
|
||||||
const login_router = createLoginRouter(this.userController);
|
const login_router = createLoginRouter(this.userController);
|
||||||
router.use("/user", login_router.routes());
|
router.use("/api/user", login_router.routes());
|
||||||
router.use("/user", login_router.allowedMethods());
|
router.use("/api/user", login_router.allowedMethods());
|
||||||
|
|
||||||
if (setting.mode === "development") {
|
if (setting.mode === "development") {
|
||||||
let mm_count = 0;
|
let mm_count = 0;
|
||||||
|
@ -34,10 +34,10 @@ export async function readZip(path: string): Promise<{
|
|||||||
reader: ZipReader<FileHandle>
|
reader: ZipReader<FileHandle>
|
||||||
handle: FileHandle
|
handle: FileHandle
|
||||||
}> {
|
}> {
|
||||||
const fd = await open(path);
|
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: false,
|
preventClose: true,
|
||||||
});
|
});
|
||||||
return { reader, handle: fd };
|
return { reader, handle: fd };
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,9 @@ importers:
|
|||||||
'@radix-ui/react-icons':
|
'@radix-ui/react-icons':
|
||||||
specifier: ^1.3.0
|
specifier: ^1.3.0
|
||||||
version: 1.3.0(react@18.2.0)
|
version: 1.3.0(react@18.2.0)
|
||||||
|
'@radix-ui/react-label':
|
||||||
|
specifier: ^2.0.2
|
||||||
|
version: 2.0.2(@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)
|
||||||
@ -30,7 +33,7 @@ importers:
|
|||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
dbtype:
|
dbtype:
|
||||||
specifier: link:..\dbtype
|
specifier: workspace:*
|
||||||
version: link:../dbtype
|
version: link:../dbtype
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
@ -1060,6 +1063,27 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-label@2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==}
|
||||||
|
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-popper@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-popper@1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==}
|
resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user