move file
This commit is contained in:
parent
790df54525
commit
4e03649880
@ -2,7 +2,8 @@ import {Content, ContentAccessor, ContentContent, QueryListOption} from "../../m
|
|||||||
import {toQueryString} from './util';
|
import {toQueryString} from './util';
|
||||||
const baseurl = "/content";
|
const baseurl = "/content";
|
||||||
|
|
||||||
export class ClientContentAccessesor implements ContentAccessor{
|
export * from "../../model/contents";
|
||||||
|
export class ClientContentAccessor implements ContentAccessor{
|
||||||
async findList(option?: QueryListOption | undefined): Promise<Content[]>{
|
async findList(option?: QueryListOption | undefined): Promise<Content[]>{
|
||||||
let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`);
|
let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`);
|
||||||
//if(res.status !== 200);
|
//if(res.status !== 200);
|
||||||
@ -65,3 +66,9 @@ export class ClientContentAccessesor implements ContentAccessor{
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export const CContentAccessor = new ClientContentAccessor;
|
||||||
|
export const makeThumbnailUrl = (x: Content)=>{
|
||||||
|
return `/content/${x.id}/${x.content_type}/thumbnail`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CContentAccessor;
|
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDom from 'react-dom';
|
import ReactDom from 'react-dom';
|
||||||
import {BrowserRouter, Route, Switch as RouterSwitch} from 'react-router-dom';
|
import {BrowserRouter, Route, Switch as RouterSwitch} from 'react-router-dom';
|
||||||
import {Headline} from './headline';
|
import {Headline} from './page/headline';
|
||||||
import {Gallery} from './gallery';
|
import {Gallery} from './page/gallery';
|
||||||
import {ContentInfo} from './contentinfo';
|
import {ContentAbout} from './page/contentinfo';
|
||||||
|
|
||||||
import '../css/style.css';
|
import './css/style.css';
|
||||||
|
|
||||||
const FooProfile = ()=><div>test profile</div>;
|
const FooProfile = ()=><div>test profile</div>;
|
||||||
const App = ()=> (
|
const App = ()=> (
|
||||||
@ -13,10 +13,8 @@ const App = ()=> (
|
|||||||
<Headline>
|
<Headline>
|
||||||
<RouterSwitch>
|
<RouterSwitch>
|
||||||
<Route path="/" exact component={Gallery}></Route>
|
<Route path="/" exact component={Gallery}></Route>
|
||||||
<Route path="/content" component={ContentInfo}>
|
<Route path="/doc" component={ContentAbout}></Route>
|
||||||
</Route>
|
<Route path="/profile" component={FooProfile}></Route>
|
||||||
<Route path="/profile" component={FooProfile}>
|
|
||||||
</Route>
|
|
||||||
<Route>
|
<Route>
|
||||||
<div>404 Not Found</div>
|
<div>404 Not Found</div>
|
||||||
</Route>
|
</Route>
|
8
src/client/component/loading.tsx
Normal file
8
src/client/component/loading.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Box, CircularProgress} from '@material-ui/core';
|
||||||
|
|
||||||
|
export const LoadingCircle = ()=>{
|
||||||
|
return (<div><Box>
|
||||||
|
<CircularProgress title="loading"/>
|
||||||
|
</Box></div>);
|
||||||
|
}
|
64
src/client/component/tagchip.tsx
Normal file
64
src/client/component/tagchip.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {ChipProps} from '@material-ui/core/Chip';
|
||||||
|
import { Chip, colors } from '@material-ui/core';
|
||||||
|
import {useTheme, makeStyles, Theme, emphasize, fade} from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
type TagChipStyleProp = {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const useTagStyles = makeStyles((theme:Theme)=>({
|
||||||
|
root:(props:TagChipStyleProp)=>({
|
||||||
|
color: theme.palette.getContrastText(props.color),
|
||||||
|
backgroundColor: props.color,
|
||||||
|
}),
|
||||||
|
clickable:(props:TagChipStyleProp)=>({
|
||||||
|
'&:hover, &:focus':{
|
||||||
|
backgroundColor:emphasize(props.color,0.08)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
deletable: {
|
||||||
|
'&:focus': {
|
||||||
|
backgroundColor: (props:TagChipStyleProp)=>emphasize(props.color, 0.2),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
outlined:{
|
||||||
|
color: (props:TagChipStyleProp)=>props.color,
|
||||||
|
border: (props:TagChipStyleProp)=> `1px solid ${props.color}`,
|
||||||
|
'$clickable&:hover, $clickable&:focus, $deletable&:focus': {
|
||||||
|
backgroundColor:(props:TagChipStyleProp)=>fade(props.color,theme.palette.action.hoverOpacity),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon:{
|
||||||
|
color:"inherit",
|
||||||
|
},
|
||||||
|
deleteIcon:{
|
||||||
|
color:(props:TagChipStyleProp)=>fade(theme.palette.getContrastText(props.color),0.7),
|
||||||
|
"&:hover, &:active":{
|
||||||
|
color:(props:TagChipStyleProp)=>theme.palette.getContrastText(props.color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {blue, pink} = colors;
|
||||||
|
const getTagColorName = (tagname :string):string=>{
|
||||||
|
if(tagname.startsWith("female")){
|
||||||
|
return pink['700'];
|
||||||
|
}
|
||||||
|
else if(tagname.startsWith("male")){
|
||||||
|
return blue[600];
|
||||||
|
}
|
||||||
|
else return "default";
|
||||||
|
}
|
||||||
|
export const ColorChip = (props:Omit<ChipProps,"color"> & {color: string})=>{
|
||||||
|
const {color,...rest} = props;
|
||||||
|
const classes = useTagStyles({color : color !== "default" ? color : "#000"});
|
||||||
|
return <Chip color="default" classes={{
|
||||||
|
...(color !== "default" ? classes : {})
|
||||||
|
}} {...rest}></Chip>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagChip = (props:Omit<ChipProps,"color"> & {tagname: string})=>{
|
||||||
|
const {tagname,...rest} = props;
|
||||||
|
return <ColorChip color={getTagColorName(tagname)} {...rest}></ColorChip>;
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {useHistory, useRouteMatch} from 'react-router-dom';
|
|
||||||
|
|
||||||
export const makeContentInfoUrl = (id:number)=>`/content/${id}`;
|
|
||||||
|
|
||||||
export const ContentInfo = (props?:{children?:React.ReactNode})=>{
|
|
||||||
const match = useRouteMatch("/content/:id");
|
|
||||||
return(<div>{match?.url}</div>)
|
|
||||||
}
|
|
@ -1,157 +0,0 @@
|
|||||||
import { Box, CircularProgress, Typography, Paper, Chip, withStyles, colors, Link } from '@material-ui/core';
|
|
||||||
import {ChipProps} from '@material-ui/core/Chip';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import {QueryListOption,Content} from '../../model/contents';
|
|
||||||
import {ClientContentAccessesor} from '../accessor/contents';
|
|
||||||
import {useTheme, makeStyles, Theme, emphasize, fade} from '@material-ui/core/styles';
|
|
||||||
import {blue , pink} from '@material-ui/core/colors';
|
|
||||||
import {Link as RouterLink} from 'react-router-dom';
|
|
||||||
import {makeContentInfoUrl} from './contentinfo';
|
|
||||||
|
|
||||||
type TagChipStyleProp = {
|
|
||||||
color: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const useTagStyles = makeStyles((theme:Theme)=>({
|
|
||||||
root:(props:TagChipStyleProp)=>({
|
|
||||||
color: theme.palette.getContrastText(props.color),
|
|
||||||
backgroundColor: props.color,
|
|
||||||
}),
|
|
||||||
clickable:(props:TagChipStyleProp)=>({
|
|
||||||
'&:hover, &:focus':{
|
|
||||||
backgroundColor:emphasize(props.color,0.08)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
deletable: {
|
|
||||||
'&:focus': {
|
|
||||||
backgroundColor: (props:TagChipStyleProp)=>emphasize(props.color, 0.2),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
outlined:{
|
|
||||||
color: (props:TagChipStyleProp)=>props.color,
|
|
||||||
border: (props:TagChipStyleProp)=> `1px solid ${props.color}`,
|
|
||||||
'$clickable&:hover, $clickable&:focus, $deletable&:focus': {
|
|
||||||
backgroundColor:(props:TagChipStyleProp)=>fade(props.color,theme.palette.action.hoverOpacity),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icon:{
|
|
||||||
color:"inherit",
|
|
||||||
},
|
|
||||||
deleteIcon:{
|
|
||||||
color:(props:TagChipStyleProp)=>fade(theme.palette.getContrastText(props.color),0.7),
|
|
||||||
"&:hover, &:active":{
|
|
||||||
color:(props:TagChipStyleProp)=>theme.palette.getContrastText(props.color),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const getTagColorName = (tagname :string):string=>{
|
|
||||||
if(tagname.startsWith("female")){
|
|
||||||
return pink['700'];
|
|
||||||
}
|
|
||||||
else if(tagname.startsWith("male")){
|
|
||||||
return blue[600];
|
|
||||||
}
|
|
||||||
else return "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
const TagChip = (props:Omit<ChipProps,"color"> & {color: string})=>{
|
|
||||||
const {color,...rest} = props;
|
|
||||||
const classes = useTagStyles({color : color !== "default" ? color : "#000"});
|
|
||||||
return <Chip color="default" classes={{
|
|
||||||
...(color !== "default" ? classes : {})
|
|
||||||
}} {...rest}></Chip>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme:Theme)=>({
|
|
||||||
root:{
|
|
||||||
display:"grid",
|
|
||||||
gridGap: theme.spacing(4),
|
|
||||||
},
|
|
||||||
table_thumbnail:{
|
|
||||||
width: theme.spacing(25),
|
|
||||||
height: theme.spacing(25),
|
|
||||||
background: '#272733',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
contentPaper:{
|
|
||||||
display:'flex',
|
|
||||||
height:200,
|
|
||||||
},
|
|
||||||
content_thumnail: {
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '100%',
|
|
||||||
},
|
|
||||||
content_info:{
|
|
||||||
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
},
|
|
||||||
content_info_title:{
|
|
||||||
marginLeft:theme.spacing(2),
|
|
||||||
},
|
|
||||||
tag_list:{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
overflowY: 'hidden',
|
|
||||||
'& > *': {
|
|
||||||
margin: theme.spacing(0.5),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export type GalleryProp = {
|
|
||||||
option?:QueryListOption;
|
|
||||||
};
|
|
||||||
type GalleryState = {
|
|
||||||
content:Content[]|undefined;
|
|
||||||
}
|
|
||||||
const content_accessor = new ClientContentAccessesor;
|
|
||||||
|
|
||||||
export const Gallery = (props: GalleryProp)=>{
|
|
||||||
const [state,setState]= useState<GalleryState>({content:undefined});
|
|
||||||
useEffect(()=>{
|
|
||||||
(async ()=>{
|
|
||||||
const c = await content_accessor.findList(props.option);
|
|
||||||
setState({content:c});
|
|
||||||
})()
|
|
||||||
},[props.option]);
|
|
||||||
const num = 1;
|
|
||||||
const classes = useStyles();
|
|
||||||
const theme = useTheme();
|
|
||||||
if(state.content === undefined){
|
|
||||||
return (<div><Box>
|
|
||||||
<CircularProgress title="loading"/>
|
|
||||||
</Box></div>);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
return (<div className={classes.root}>{
|
|
||||||
state.content.map(x=>{
|
|
||||||
const thumbnail_url = `/content/${x.id}/${x.content_type}/thumbnail`;
|
|
||||||
return (<Paper key={x.id} elevation={4} className={classes.contentPaper}>
|
|
||||||
<Link className={classes.table_thumbnail} component={RouterLink} to={makeContentInfoUrl(x.id)}>{
|
|
||||||
x.content_type === "manga" ? <img className={classes.content_thumnail} src={thumbnail_url}></img> :
|
|
||||||
<video className={classes.content_thumnail} src={thumbnail_url}></video>
|
|
||||||
}</Link>
|
|
||||||
<Box className={classes.content_info}>
|
|
||||||
<Link component={RouterLink} to={makeContentInfoUrl(x.id)} variant="h5" color="inherit"
|
|
||||||
className={classes.content_info_title}>
|
|
||||||
{x.title}
|
|
||||||
</Link>
|
|
||||||
<Box className={classes.tag_list}>
|
|
||||||
{x.tags.map(x=>{
|
|
||||||
const tagcolor = getTagColorName(x);
|
|
||||||
return (<TagChip key={x} label={x} clickable={true} color={tagcolor} size="small"></TagChip>);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Paper>);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
}
|
|
115
src/client/page/contentinfo.tsx
Normal file
115
src/client/page/contentinfo.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Redirect, Route, Switch, useHistory, useRouteMatch, match as MatchType, Link as RouterLink } from 'react-router-dom';
|
||||||
|
import ContentAccessor, { Content } from '../accessor/contents';
|
||||||
|
import { LoadingCircle } from '../component/loading';
|
||||||
|
import { Link, Paper, makeStyles, Theme, Box, useTheme, Typography } from '@material-ui/core';
|
||||||
|
import { ThumbnailContainer } from '../presenter/presenter';
|
||||||
|
import { TagChip } from '../component/tagchip';
|
||||||
|
import { MangaReader } from '../presenter/manga';
|
||||||
|
|
||||||
|
export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
|
||||||
|
export const makeMangaReaderUrl = (id: number) => `/doc/${id}/reader`;
|
||||||
|
|
||||||
|
type ContentInfoState = {
|
||||||
|
content: Content | undefined,
|
||||||
|
notfound: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContentAbout = (prop: { match: MatchType }) => {
|
||||||
|
const match = useRouteMatch("/doc/:id");
|
||||||
|
if (match == null) {
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
|
const m = /\/doc\/(\d+)/.exec(match.url);
|
||||||
|
const id = m !== null ? Number.parseInt(m[1]) : NaN;
|
||||||
|
const [info, setInfo] = useState<ContentInfoState>({ content: undefined, notfound:false });
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (!isNaN(id)) {
|
||||||
|
const c = await ContentAccessor.findById(id);
|
||||||
|
setInfo({ content: c, notfound: c === undefined });
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, []);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return (<Typography variant='h2'>Oops. Invalid ID</Typography>)
|
||||||
|
}
|
||||||
|
else if(info.notfound){
|
||||||
|
return (<Typography variant='h2'>Content has been removed.</Typography>)
|
||||||
|
}
|
||||||
|
else if (info.content === undefined) {
|
||||||
|
return (<LoadingCircle />);
|
||||||
|
}
|
||||||
|
else return (<Switch>
|
||||||
|
<Route exact path={`${prop.match.path}/:id`}>
|
||||||
|
<ContentInfo content={info.content}></ContentInfo>
|
||||||
|
</Route>
|
||||||
|
<Route exact path={`${prop.match.path}/:id/reader`}>
|
||||||
|
<MangaReader content={info.content}></MangaReader>
|
||||||
|
</Route>
|
||||||
|
<Route>
|
||||||
|
<div>404 Not Found invalid url : {prop.match.path}</div>
|
||||||
|
</Route>
|
||||||
|
</Switch>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
root: {
|
||||||
|
display: "flex",
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
thumbnail_content: {
|
||||||
|
maxWidth: '300px',
|
||||||
|
maxHeight: '300px'
|
||||||
|
},
|
||||||
|
tag_list: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
overflowY: 'hidden',
|
||||||
|
'& > *': {
|
||||||
|
margin: theme.spacing(0.5),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
InfoContainer: {
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '100px auto',
|
||||||
|
overflowY: 'hidden',
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const ContentInfo = (props: { content: Content, children?: React.ReactNode }) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const theme = useTheme();
|
||||||
|
const content = props?.content;
|
||||||
|
let allTag = content.tags;
|
||||||
|
const artists = allTag.filter(x => x.startsWith("artist:")).map(x => x.substr(7));
|
||||||
|
allTag = allTag.filter(x => !x.startsWith("artist:"));
|
||||||
|
return (<Paper className={classes.root}>
|
||||||
|
<Link component={RouterLink} to={makeMangaReaderUrl(content.id)}>
|
||||||
|
<ThumbnailContainer content={content} className={classes.thumbnail_content}></ThumbnailContainer>
|
||||||
|
</Link>
|
||||||
|
<Box style={{ padding: theme.spacing(1) }}>
|
||||||
|
<Link variant='h5' color='inherit' component={RouterLink} to={makeMangaReaderUrl(content.id)}
|
||||||
|
className={classes.title}>
|
||||||
|
{content.title}
|
||||||
|
</Link>
|
||||||
|
<Box className={classes.InfoContainer}>
|
||||||
|
<Typography variant='subtitle1'>Artist</Typography>
|
||||||
|
<Box>{artists.join(", ")}</Box>
|
||||||
|
<Typography variant='subtitle1'>Tags</Typography>
|
||||||
|
<Box className={classes.tag_list}>
|
||||||
|
{allTag.map(x => {
|
||||||
|
return (<TagChip key={x} label={x} clickable={true} tagname={x} size="small"></TagChip>);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>);
|
||||||
|
}
|
108
src/client/page/gallery.tsx
Normal file
108
src/client/page/gallery.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { Box, Paper, Link, useMediaQuery } from '@material-ui/core';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import ContentAccessor,{makeThumbnailUrl, QueryListOption, Content} from '../accessor/contents';
|
||||||
|
import {useTheme, makeStyles, Theme} from '@material-ui/core/styles';
|
||||||
|
import {Link as RouterLink} from 'react-router-dom';
|
||||||
|
import {makeContentInfoUrl} from './contentinfo';
|
||||||
|
import { LoadingCircle } from '../component/loading';
|
||||||
|
import {ThumbnailContainer} from '../presenter/presenter';
|
||||||
|
import {TagChip} from '../component/tagchip';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme:Theme)=>({
|
||||||
|
root:{
|
||||||
|
display:"grid",
|
||||||
|
gridGap: theme.spacing(4),
|
||||||
|
},
|
||||||
|
anchor_thumbnail:{
|
||||||
|
background: '#272733',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
[theme.breakpoints.up("sm")]:{
|
||||||
|
width: theme.spacing(25),
|
||||||
|
height: theme.spacing(25),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentPaper:{
|
||||||
|
display:'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
[theme.breakpoints.up("sm")]:{
|
||||||
|
height:200,
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content_thumnail: {
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
},
|
||||||
|
content_info:{
|
||||||
|
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
content_info_title:{
|
||||||
|
marginLeft:theme.spacing(2),
|
||||||
|
},
|
||||||
|
tag_list:{
|
||||||
|
display:'none',
|
||||||
|
[theme.breakpoints.up("sm")]:{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
overflowY: 'hidden',
|
||||||
|
'& > *': {
|
||||||
|
margin: theme.spacing(0.5),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type GalleryProp = {
|
||||||
|
option?:QueryListOption;
|
||||||
|
};
|
||||||
|
type GalleryState = {
|
||||||
|
content:Content[]|undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Gallery = (props: GalleryProp)=>{
|
||||||
|
const [state,setState]= useState<GalleryState>({content:undefined});
|
||||||
|
useEffect(()=>{
|
||||||
|
(async ()=>{
|
||||||
|
const c = await ContentAccessor.findList(props.option);
|
||||||
|
setState({content:c});
|
||||||
|
})()
|
||||||
|
},[props.option]);
|
||||||
|
const num = 1;
|
||||||
|
const classes = useStyles();
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = !useMediaQuery(theme.breakpoints.up("sm"));
|
||||||
|
if(state.content === undefined){
|
||||||
|
return (<LoadingCircle/>);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return (<div className={classes.root}>{
|
||||||
|
state.content.map(x=>{
|
||||||
|
const thumbnail_url = makeThumbnailUrl(x);
|
||||||
|
return (<Paper key={x.id} elevation={4} className={classes.contentPaper}>
|
||||||
|
<Link className={classes.anchor_thumbnail}
|
||||||
|
component={RouterLink} to={makeContentInfoUrl(x.id)}>
|
||||||
|
<ThumbnailContainer content={x} className={classes.content_thumnail}></ThumbnailContainer>
|
||||||
|
</Link>
|
||||||
|
<Box className={classes.content_info}>
|
||||||
|
<Link component={RouterLink} to={makeContentInfoUrl(x.id)} variant="h5" color="inherit"
|
||||||
|
className={classes.content_info_title}>
|
||||||
|
{x.title}
|
||||||
|
</Link>
|
||||||
|
<Box className={classes.tag_list}>
|
||||||
|
{x.tags.map(x=>{
|
||||||
|
return (<TagChip key={x} label={x} clickable={true} tagname={x} size="small"></TagChip>);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
Button, CssBaseline, Divider, IconButton, List, ListItem, Drawer,
|
Button, CssBaseline, Divider, IconButton, List, ListItem, Drawer,
|
||||||
AppBar, Toolbar, Typography, InputBase, ListItemIcon, ListItemText, Menu, MenuItem,
|
AppBar, Toolbar, Typography, InputBase, ListItemIcon, ListItemText, Menu, MenuItem,
|
||||||
Hidden, useMediaQuery, Tooltip
|
Hidden, Tooltip
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { makeStyles, Theme, useTheme, fade } from '@material-ui/core/styles';
|
import { makeStyles, Theme, useTheme, fade } from '@material-ui/core/styles';
|
||||||
import { ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon, ArrowBack as ArrowBackIcon, AccountCircle } from '@material-ui/icons';
|
import { ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon, ArrowBack as ArrowBackIcon, AccountCircle } from '@material-ui/icons';
|
||||||
@ -45,13 +45,11 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
toolbar: {
|
toolbar: {
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
padding: theme.spacing(0, 1),
|
|
||||||
...theme.mixins.toolbar,
|
...theme.mixins.toolbar,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
|
display:'flex',
|
||||||
|
flexFlow: 'column',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
padding: theme.spacing(3),
|
padding: theme.spacing(3),
|
||||||
},
|
},
|
||||||
@ -106,7 +104,7 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const Headline = (prop: { children?: React.ReactNode, navListItem?: React.ReactNode}) => {
|
export const Headline = (prop: { children?: React.ReactNode, navListItem?: React.ReactNode, isLogin?:boolean}) => {
|
||||||
const [v, setv] = useState(false);
|
const [v, setv] = useState(false);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
@ -116,7 +114,7 @@ export const Headline = (prop: { children?: React.ReactNode, navListItem?: React
|
|||||||
const handleProfileMenuClose = ()=>setAnchorEl(null);
|
const handleProfileMenuClose = ()=>setAnchorEl(null);
|
||||||
const isProfileMenuOpened = Boolean(anchorEl);
|
const isProfileMenuOpened = Boolean(anchorEl);
|
||||||
const menuId = 'primary-search-account-menu';
|
const menuId = 'primary-search-account-menu';
|
||||||
|
const isLogin = prop.isLogin || false;
|
||||||
const renderProfileMenu = (<Menu
|
const renderProfileMenu = (<Menu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
anchorOrigin={{horizontal:'right',vertical:"top"}}
|
anchorOrigin={{horizontal:'right',vertical:"top"}}
|
||||||
@ -170,6 +168,8 @@ export const Headline = (prop: { children?: React.ReactNode, navListItem?: React
|
|||||||
<InputBase placeholder="search"
|
<InputBase placeholder="search"
|
||||||
classes={{ root: classes.inputRoot, input: classes.inputInput }}></InputBase>
|
classes={{ root: classes.inputRoot, input: classes.inputInput }}></InputBase>
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
isLogin ?
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="end"
|
edge="end"
|
||||||
aria-label="account of current user"
|
aria-label="account of current user"
|
||||||
@ -179,6 +179,8 @@ export const Headline = (prop: { children?: React.ReactNode, navListItem?: React
|
|||||||
color="inherit">
|
color="inherit">
|
||||||
<AccountCircle />
|
<AccountCircle />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
: <Button color="inherit">Login</Button>
|
||||||
|
}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
{renderProfileMenu}
|
{renderProfileMenu}
|
28
src/client/presenter/manga.tsx
Normal file
28
src/client/presenter/manga.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, {useState, useEffect} from 'react';
|
||||||
|
import {Redirect, Route ,Switch,useHistory, useRouteMatch, match as MatchType, Link as RouterLink} from 'react-router-dom';
|
||||||
|
import { LoadingCircle } from '../component/loading';
|
||||||
|
import { Link, Paper, makeStyles, Theme, Box, Typography } from '@material-ui/core';
|
||||||
|
import { Content, makeThumbnailUrl } from '../accessor/contents';
|
||||||
|
|
||||||
|
type MangaType = "manga"|"artist cg"|"donjinshi"|"western"
|
||||||
|
|
||||||
|
export type PresentableTag = {
|
||||||
|
artist:string[],
|
||||||
|
group: string[],
|
||||||
|
series: string[],
|
||||||
|
type: MangaType,
|
||||||
|
character: string[],
|
||||||
|
tags: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MangaReader = (props:{content:Content})=>{
|
||||||
|
const additional = props.content.additional;
|
||||||
|
if(!('page' in additional)){
|
||||||
|
console.error("invalid content : page read fail : "+ JSON.stringify(additional));
|
||||||
|
return <Typography>Error. DB error. page restriction</Typography>
|
||||||
|
}
|
||||||
|
const page:number = additional['page'] as number;
|
||||||
|
return (<div>{[...Array(page).keys()].map(x=>(<img src={`/content/${props.content.id}/manga/${x}`} style={{maxHeight:'100%',maxWidth:'100%'}}></img>))}</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MangaReader;
|
21
src/client/presenter/presenter.tsx
Normal file
21
src/client/presenter/presenter.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Content, makeThumbnailUrl } from '../accessor/contents';
|
||||||
|
import {MangaReader} from './manga';
|
||||||
|
|
||||||
|
export type PresenterCollection = {
|
||||||
|
ReaderPage: (props:{content:Content,className?:string})=> JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPresenter = (content:Content):PresenterCollection=>{
|
||||||
|
return {
|
||||||
|
ReaderPage:MangaReader
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThumbnailContainer = (props:{content:Content, className?:string})=>{
|
||||||
|
const thumbnailurl = makeThumbnailUrl(props.content);
|
||||||
|
if(props.content.content_type === "video"){
|
||||||
|
return (<video src={thumbnailurl} className={props.className}></video>)
|
||||||
|
}
|
||||||
|
else return (<img src={thumbnailurl} className={props.className}></img>)
|
||||||
|
}
|
0
src/client/presenter/video.tsx
Normal file
0
src/client/presenter/video.tsx
Normal file
@ -2,7 +2,7 @@ const path = require("path");
|
|||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
|
||||||
module.exports = ()=>{return {
|
module.exports = ()=>{return {
|
||||||
entry: './src/client/js/app.tsx',
|
entry: './src/client/app.tsx',
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'dist/js'),
|
path: path.resolve(__dirname, 'dist/js'),
|
||||||
filename: 'bundle.js'
|
filename: 'bundle.js'
|
||||||
|
Loading…
Reference in New Issue
Block a user