rename content to document
This commit is contained in:
parent
ead51b5dfe
commit
61a7a4b651
6
app.ts
6
app.ts
@ -3,7 +3,7 @@ import { get_setting } from "./src/setting";
|
|||||||
import { create_server, start_server } from "./src/server";
|
import { create_server, start_server } from "./src/server";
|
||||||
import { getAdminAccessTokenValue,getAdminRefreshTokenValue, accessTokenName, refreshTokenName } from "./src/login";
|
import { getAdminAccessTokenValue,getAdminRefreshTokenValue, accessTokenName, refreshTokenName } from "./src/login";
|
||||||
|
|
||||||
const get_loading_html = (content?:string)=> `<!DOCTYPE html>
|
const get_loading_html = (message?:string)=> `<!DOCTYPE html>
|
||||||
<html lang="ko"><head>
|
<html lang="ko"><head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>react-sample</title>
|
<title>react-sample</title>
|
||||||
@ -31,8 +31,8 @@ h1 {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<h1>${content || "Loading..."}</h1>
|
<h1>${message || "Loading..."}</h1>
|
||||||
${content === undefined ? '<div id="loading"></div>' : ""}
|
${message === undefined ? '<div id="loading"></div>' : ""}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
|
@ -6,7 +6,7 @@ export async function up(knex:Knex) {
|
|||||||
b.string("password_hash",64).notNullable();
|
b.string("password_hash",64).notNullable();
|
||||||
b.string("password_salt",64).notNullable();
|
b.string("password_salt",64).notNullable();
|
||||||
});
|
});
|
||||||
await knex.schema.createTable("contents",(b)=>{
|
await knex.schema.createTable("document",(b)=>{
|
||||||
b.increments("id").primary();
|
b.increments("id").primary();
|
||||||
b.string("title").notNullable();
|
b.string("title").notNullable();
|
||||||
b.string("content_type",16).notNullable();
|
b.string("content_type",16).notNullable();
|
||||||
@ -21,12 +21,12 @@ export async function up(knex:Knex) {
|
|||||||
b.string("name").primary();
|
b.string("name").primary();
|
||||||
b.text("description");
|
b.text("description");
|
||||||
});
|
});
|
||||||
await knex.schema.createTable("content_tag_relation",(b)=>{
|
await knex.schema.createTable("doc_tag_relation",(b)=>{
|
||||||
b.integer("content_id").unsigned().notNullable();
|
b.integer("doc_id").unsigned().notNullable();
|
||||||
b.string("tag_name").notNullable();
|
b.string("tag_name").notNullable();
|
||||||
b.foreign("content_id").references("contents.id");
|
b.foreign("doc_id").references("document.id");
|
||||||
b.foreign("tag_name").references("tags.name");
|
b.foreign("tag_name").references("tags.name");
|
||||||
b.primary(["content_id","tag_name"]);
|
b.primary(["doc_id","tag_name"]);
|
||||||
});
|
});
|
||||||
await knex.schema.createTable("permissions",b=>{
|
await knex.schema.createTable("permissions",b=>{
|
||||||
b.string('username').notNullable();
|
b.string('username').notNullable();
|
||||||
@ -45,8 +45,8 @@ export async function up(knex:Knex) {
|
|||||||
export async function down(knex:Knex) {
|
export async function down(knex:Knex) {
|
||||||
//throw new Error('Downward migrations are not supported. Restore from backup.');
|
//throw new Error('Downward migrations are not supported. Restore from backup.');
|
||||||
await knex.schema.dropTable("users");
|
await knex.schema.dropTable("users");
|
||||||
await knex.schema.dropTable("contents");
|
await knex.schema.dropTable("document");
|
||||||
await knex.schema.dropTable("tags");
|
await knex.schema.dropTable("tags");
|
||||||
await knex.schema.dropTable("content_tag_relation");
|
await knex.schema.dropTable("document_tag_relation");
|
||||||
await knex.schema.dropTable("permissions");
|
await knex.schema.dropTable("permissions");
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"asar": false,
|
"asar": false,
|
||||||
"files":[
|
"files": [
|
||||||
"build/**/*",
|
"build/**/*",
|
||||||
"node_modules/**/*",
|
"node_modules/**/*",
|
||||||
"package.json"
|
"package.json"
|
||||||
@ -79,6 +79,7 @@
|
|||||||
"css-loader": "^5.0.1",
|
"css-loader": "^5.0.1",
|
||||||
"electron": "^11.1.1",
|
"electron": "^11.1.1",
|
||||||
"electron-builder": "^22.9.1",
|
"electron-builder": "^22.9.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"mini-css-extract-plugin": "^1.3.3",
|
"mini-css-extract-plugin": "^1.3.3",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"ts-node": "^9.1.1",
|
"ts-node": "^9.1.1",
|
||||||
|
10
plan.md
10
plan.md
@ -5,8 +5,9 @@
|
|||||||
### server routing
|
### server routing
|
||||||
- content
|
- content
|
||||||
- \d+
|
- \d+
|
||||||
- image
|
- manga
|
||||||
- (?P<page>\d+)
|
- (?P<page>\d+)
|
||||||
|
- video
|
||||||
- diff
|
- diff
|
||||||
- itemNum
|
- itemNum
|
||||||
- diff
|
- diff
|
||||||
@ -14,6 +15,8 @@
|
|||||||
- commit
|
- commit
|
||||||
- user
|
- user
|
||||||
- login
|
- login
|
||||||
|
- logout
|
||||||
|
- refresh
|
||||||
|
|
||||||
### client routing
|
### client routing
|
||||||
|
|
||||||
@ -23,6 +26,9 @@
|
|||||||
- login
|
- login
|
||||||
- profile
|
- profile
|
||||||
- search ? querystring...;
|
- search ? querystring...;
|
||||||
|
- setting
|
||||||
|
- difference
|
||||||
|
- profile
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
- server push
|
- server push
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {Content, ContentAccessor, ContentContent, QueryListOption} from "../../model/contents";
|
import {Document, DocumentAccessor, DocumentBody, QueryListOption} from "../../model/doc";
|
||||||
import {toQueryString} from './util';
|
import {toQueryString} from './util';
|
||||||
const baseurl = "/content";
|
const baseurl = "/api/doc";
|
||||||
|
|
||||||
export * from "../../model/contents";
|
export * from "../../model/doc";
|
||||||
|
|
||||||
export class FetchFailError implements Error{
|
export class FetchFailError implements Error{
|
||||||
name: string;
|
name: string;
|
||||||
@ -14,14 +14,14 @@ export class FetchFailError implements Error{
|
|||||||
this.stack = stack;
|
this.stack = stack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class ClientContentAccessor implements ContentAccessor{
|
export class ClientDocumentAccessor implements DocumentAccessor{
|
||||||
async findList(option?: QueryListOption | undefined): Promise<Content[]>{
|
async findList(option?: QueryListOption | undefined): Promise<Document[]>{
|
||||||
let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`);
|
let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`);
|
||||||
if(res.status !== 200) throw new FetchFailError("findList Failed");
|
if(res.status !== 200) throw new FetchFailError("findList Failed");
|
||||||
let ret = await res.json();
|
let ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async findById(id: number, tagload?: boolean | undefined): Promise<Content | undefined>{
|
async findById(id: number, tagload?: boolean | undefined): Promise<Document | undefined>{
|
||||||
let res = await fetch(`${baseurl}/${id}`);
|
let res = await fetch(`${baseurl}/${id}`);
|
||||||
if(res.status !== 200) throw new FetchFailError("findById Failed");;
|
if(res.status !== 200) throw new FetchFailError("findById Failed");;
|
||||||
let ret = await res.json();
|
let ret = await res.json();
|
||||||
@ -30,11 +30,11 @@ export class ClientContentAccessor implements ContentAccessor{
|
|||||||
/**
|
/**
|
||||||
* not implement
|
* not implement
|
||||||
*/
|
*/
|
||||||
async findListByBasePath(basepath: string): Promise<Content[]>{
|
async findListByBasePath(basepath: string): Promise<Document[]>{
|
||||||
throw new Error("not implement");
|
throw new Error("not implement");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
async update(c: Partial<Content> & { id: number; }): Promise<boolean>{
|
async update(c: Partial<Document> & { id: number; }): Promise<boolean>{
|
||||||
const {id,...rest} = c;
|
const {id,...rest} = c;
|
||||||
const res = await fetch(`${baseurl}/${id}`,{
|
const res = await fetch(`${baseurl}/${id}`,{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -43,7 +43,7 @@ export class ClientContentAccessor implements ContentAccessor{
|
|||||||
const ret = await res.json();
|
const ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async add(c: ContentContent): Promise<number>{
|
async add(c: DocumentBody): Promise<number>{
|
||||||
const res = await fetch(`${baseurl}`,{
|
const res = await fetch(`${baseurl}`,{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(c)
|
body: JSON.stringify(c)
|
||||||
@ -58,7 +58,7 @@ export class ClientContentAccessor implements ContentAccessor{
|
|||||||
const ret = await res.json();
|
const ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async addTag(c: Content, tag_name: string): Promise<boolean>{
|
async addTag(c: Document, tag_name: string): Promise<boolean>{
|
||||||
const {id,...rest} = c;
|
const {id,...rest} = c;
|
||||||
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{
|
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -67,7 +67,7 @@ export class ClientContentAccessor implements ContentAccessor{
|
|||||||
const ret = await res.json();
|
const ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async delTag(c: Content, tag_name: string): Promise<boolean>{
|
async delTag(c: Document, tag_name: string): Promise<boolean>{
|
||||||
const {id,...rest} = c;
|
const {id,...rest} = c;
|
||||||
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{
|
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@ -77,9 +77,9 @@ export class ClientContentAccessor implements ContentAccessor{
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const CContentAccessor = new ClientContentAccessor;
|
export const CDocumentAccessor = new ClientDocumentAccessor;
|
||||||
export const makeThumbnailUrl = (x: Content)=>{
|
export const makeThumbnailUrl = (x: Document)=>{
|
||||||
return `/content/${x.id}/${x.content_type}/thumbnail`;
|
return `${baseurl}/${x.id}/${x.content_type}/thumbnail`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CContentAccessor;
|
export default CDocumentAccessor;
|
@ -1,7 +1,7 @@
|
|||||||
import React, { createContext, useEffect, useRef, useState } from 'react';
|
import React, { createContext, useEffect, useRef, useState } from 'react';
|
||||||
import ReactDom from 'react-dom';
|
import ReactDom from 'react-dom';
|
||||||
import {BrowserRouter, Redirect, Route, Switch as RouterSwitch} from 'react-router-dom';
|
import {BrowserRouter, Redirect, Route, Switch as RouterSwitch} from 'react-router-dom';
|
||||||
import { Gallery, ContentAbout, LoginPage, NotFoundPage, ProfilePage,DifferencePage} from './page/mod';
|
import { Gallery, DocumentAbout, LoginPage, NotFoundPage, ProfilePage,DifferencePage} from './page/mod';
|
||||||
import {getInitialValue, UserContext} from './state';
|
import {getInitialValue, UserContext} from './state';
|
||||||
|
|
||||||
import './css/style.css';
|
import './css/style.css';
|
||||||
@ -27,7 +27,7 @@ const App = () => {
|
|||||||
<RouterSwitch>
|
<RouterSwitch>
|
||||||
<Route path="/" exact render={()=><Redirect to='/search?'/>}></Route>
|
<Route path="/" exact render={()=><Redirect to='/search?'/>}></Route>
|
||||||
<Route path="/search" render={()=><Gallery />}></Route>
|
<Route path="/search" render={()=><Gallery />}></Route>
|
||||||
<Route path="/doc" render={(prop)=><ContentAbout {...prop}/>}></Route>
|
<Route path="/doc" render={(prop)=><DocumentAbout {...prop}/>}></Route>
|
||||||
<Route path="/login" render={()=><LoginPage></LoginPage>}/>
|
<Route path="/login" render={()=><LoginPage></LoginPage>}/>
|
||||||
<Route path="/profile" component={ProfilePage}></Route>
|
<Route path="/profile" component={ProfilePage}></Route>
|
||||||
<Route path="/difference" component={DifferencePage}></Route>
|
<Route path="/difference" component={DifferencePage}></Route>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Redirect, Route, Switch, useHistory, useRouteMatch, match as MatchType, Link as RouterLink } from 'react-router-dom';
|
import { Redirect, Route, Switch, useHistory, useRouteMatch, match as MatchType, Link as RouterLink } from 'react-router-dom';
|
||||||
import ContentAccessor, { Content } from '../accessor/contents';
|
import { Document } from '../accessor/document';
|
||||||
import { LoadingCircle } from '../component/loading';
|
import { LoadingCircle } from '../component/loading';
|
||||||
import { Link, Paper, makeStyles, Theme, Box, useTheme, Typography } from '@material-ui/core';
|
import { Link, Paper, makeStyles, Theme, Box, useTheme, Typography } from '@material-ui/core';
|
||||||
import { ThumbnailContainer } from '../page/reader/reader';
|
import { ThumbnailContainer } from '../page/reader/reader';
|
||||||
@ -74,7 +74,7 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
export const ContentInfo = (props: {
|
export const ContentInfo = (props: {
|
||||||
content: Content, children?: React.ReactNode, classes?: {
|
document: Document, children?: React.ReactNode, classes?: {
|
||||||
root?: string,
|
root?: string,
|
||||||
thumbnail_anchor?: string,
|
thumbnail_anchor?: string,
|
||||||
thumbnail_content?: string,
|
thumbnail_content?: string,
|
||||||
@ -88,31 +88,30 @@ export const ContentInfo = (props: {
|
|||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const content = props.content;
|
const document = props.document;
|
||||||
const propclasses = props.classes || {};
|
const propclasses = props.classes || {};
|
||||||
|
|
||||||
const rootName = props.short ? classes.short_root : classes.root;
|
const rootName = props.short ? classes.short_root : classes.root;
|
||||||
const thumbnail_anchor = props.short ? classes.short_thumbnail_anchor : "";
|
const thumbnail_anchor = props.short ? classes.short_thumbnail_anchor : "";
|
||||||
const thumbnail_content = props.short ? classes.short_thumbnail_content :
|
const thumbnail_content = props.short ? classes.short_thumbnail_content :
|
||||||
classes.thumbnail_content;
|
classes.thumbnail_content;
|
||||||
const subinfoContainer = props.short ? classes.short_subinfoContainer :
|
const subinfoContainer = props.short ? classes.short_subinfoContainer :
|
||||||
classes.subinfoContainer;
|
classes.subinfoContainer;
|
||||||
let allTag = content.tags;
|
let allTag = document.tags;
|
||||||
const artists = allTag.filter(x => x.startsWith("artist:")).map(x => x.substr(7));
|
const artists = allTag.filter(x => x.startsWith("artist:")).map(x => x.substr(7));
|
||||||
allTag = allTag.filter(x => !x.startsWith("artist:"));
|
allTag = allTag.filter(x => !x.startsWith("artist:"));
|
||||||
return (<Paper className={propclasses.root || rootName} elevation={4}>
|
return (<Paper className={propclasses.root || rootName} elevation={4}>
|
||||||
<Link className={propclasses.thumbnail_anchor || thumbnail_anchor} component={RouterLink} to={{
|
<Link className={propclasses.thumbnail_anchor || thumbnail_anchor} component={RouterLink} to={{
|
||||||
pathname:makeContentReaderUrl(content.id)
|
pathname:makeContentReaderUrl(document.id)
|
||||||
}}>
|
}}>
|
||||||
<ThumbnailContainer content={content}
|
<ThumbnailContainer content={document}
|
||||||
className={propclasses.thumbnail_content || thumbnail_content}/>
|
className={propclasses.thumbnail_content || thumbnail_content}/>
|
||||||
</Link>
|
</Link>
|
||||||
<Box className={propclasses.infoContainer || classes.infoContainer}>
|
<Box className={propclasses.infoContainer || classes.infoContainer}>
|
||||||
<Link variant='h5' color='inherit' component={RouterLink} to={{
|
<Link variant='h5' color='inherit' component={RouterLink} to={{
|
||||||
pathname: props.gallery === undefined ? makeContentReaderUrl(content.id) : makeContentInfoUrl(content.id),
|
pathname: props.gallery === undefined ? makeContentReaderUrl(document.id) : makeContentInfoUrl(document.id),
|
||||||
}}
|
}}
|
||||||
className={propclasses.title || classes.title}>
|
className={propclasses.title || classes.title}>
|
||||||
{content.title}
|
{document.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Box className={propclasses.subinfoContainer || subinfoContainer}>
|
<Box className={propclasses.subinfoContainer || subinfoContainer}>
|
||||||
<Typography variant='subtitle1'>Artist</Typography>
|
<Typography variant='subtitle1'>Artist</Typography>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Box, Paper, Link, useMediaQuery, Portal, List, ListItem, ListItemIcon, Tooltip, ListItemText } from '@material-ui/core';
|
import { Box, Paper, Link, useMediaQuery, Portal, List, ListItem, ListItemIcon, Tooltip, ListItemText } from '@material-ui/core';
|
||||||
import {useTheme, makeStyles, Theme} from '@material-ui/core/styles';
|
import {useTheme, makeStyles, Theme} from '@material-ui/core/styles';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ContentAccessor,{QueryListOption, Content} from '../accessor/contents';
|
import ContentAccessor,{QueryListOption, Document} from '../accessor/document';
|
||||||
import {Link as RouterLink} from 'react-router-dom';
|
import {Link as RouterLink} from 'react-router-dom';
|
||||||
import { LoadingCircle, ContentInfo, NavList, NavItem } from './mod';
|
import { LoadingCircle, ContentInfo, NavList, NavItem } from './mod';
|
||||||
import {toQueryString} from '../accessor/util';
|
import {toQueryString} from '../accessor/util';
|
||||||
@ -42,29 +42,29 @@ export type GalleryProp = {
|
|||||||
option?:QueryListOption;
|
option?:QueryListOption;
|
||||||
};
|
};
|
||||||
type GalleryState = {
|
type GalleryState = {
|
||||||
content:Content[]|undefined;
|
documents:Document[]|undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GalleryInfo = (props: GalleryProp)=>{
|
export const GalleryInfo = (props: GalleryProp)=>{
|
||||||
const [state,setState]= useState<GalleryState>({content:undefined});
|
const [state,setState]= useState<GalleryState>({documents:undefined});
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const load = (async ()=>{
|
const load = (async ()=>{
|
||||||
const c = await ContentAccessor.findList(props.option);
|
const c = await ContentAccessor.findList(props.option);
|
||||||
//todo : if c is undefined, retry to fetch 3 times. and show error message.
|
//todo : if c is undefined, retry to fetch 3 times. and show error message.
|
||||||
setState({content:c});
|
setState({documents:c});
|
||||||
})
|
})
|
||||||
load();
|
load();
|
||||||
},[props.option]);
|
},[props.option]);
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const queryString = toQueryString(props.option||{});
|
const queryString = toQueryString(props.option||{});
|
||||||
|
|
||||||
if(state.content === undefined){
|
if(state.documents === undefined){
|
||||||
return (<LoadingCircle/>);
|
return (<LoadingCircle/>);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
return (<Box className={classes.root}>{
|
return (<Box className={classes.root}>{
|
||||||
state.content.map(x=>{
|
state.documents.map(x=>{
|
||||||
return (<ContentInfo content={x} key={x.id}
|
return (<ContentInfo document={x} key={x.id}
|
||||||
gallery={`/search?${queryString}`} short/>);
|
gallery={`/search?${queryString}`} short/>);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import { Redirect, Route, Switch, useHistory, useRouteMatch, match as MatchType, Link as RouterLink, useParams, useLocation } from 'react-router-dom';
|
import { Redirect, Route, Switch, useHistory, useRouteMatch, match as MatchType, Link as RouterLink, useParams, useLocation } from 'react-router-dom';
|
||||||
import ContentAccessor, { Content } from '../accessor/contents';
|
import DocumentAccessor, { Document } from '../accessor/document';
|
||||||
import { LoadingCircle } from '../component/loading';
|
import { LoadingCircle } from '../component/loading';
|
||||||
import { Link, Paper, makeStyles, Theme, Box, useTheme, Typography } from '@material-ui/core';
|
import { Link, Paper, makeStyles, Theme, Box, useTheme, Typography } from '@material-ui/core';
|
||||||
import {ArrowBack as ArrowBackIcon } from '@material-ui/icons';
|
import {ArrowBack as ArrowBackIcon } from '@material-ui/icons';
|
||||||
@ -10,8 +10,8 @@ import { BackItem, CommonMenuList, ContentInfo, Headline, NavItem, NavList } fro
|
|||||||
export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
|
export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
|
||||||
export const makeMangaReaderUrl = (id: number) => `/doc/${id}/reader`;
|
export const makeMangaReaderUrl = (id: number) => `/doc/${id}/reader`;
|
||||||
|
|
||||||
type ContentState = {
|
type DocumentState = {
|
||||||
content: Content | undefined,
|
doc: Document | undefined,
|
||||||
notfound: boolean,
|
notfound: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,25 +27,25 @@ const useStyles = makeStyles((theme:Theme)=>({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ContentAbout = (prop: { match: MatchType }) => {
|
export const DocumentAbout = (prop: { match: MatchType }) => {
|
||||||
const match = useRouteMatch<{id:string}>("/doc/:id");
|
const match = useRouteMatch<{id:string}>("/doc/:id");
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw new Error("unreachable");
|
throw new Error("unreachable");
|
||||||
}
|
}
|
||||||
const id = Number.parseInt(match.params['id']);
|
const id = Number.parseInt(match.params['id']);
|
||||||
const [info, setInfo] = useState<ContentState>({ content: undefined, notfound:false });
|
const [info, setInfo] = useState<DocumentState>({ doc: undefined, notfound:false });
|
||||||
const menu_list = () => <CommonMenuList></CommonMenuList>;
|
const menu_list = (link?:string) => <CommonMenuList url={link}></CommonMenuList>;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!isNaN(id)) {
|
if (!isNaN(id)) {
|
||||||
const c = await ContentAccessor.findById(id);
|
const c = await DocumentAccessor.findById(id);
|
||||||
setInfo({ content: c, notfound: c === undefined });
|
setInfo({ doc: c, notfound: c === undefined });
|
||||||
}
|
}
|
||||||
})()
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
console.log(info.doc);
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return (
|
return (
|
||||||
<Headline menu={menu_list()}>
|
<Headline menu={menu_list()}>
|
||||||
@ -60,18 +60,18 @@ export const ContentAbout = (prop: { match: MatchType }) => {
|
|||||||
</Headline>
|
</Headline>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else if (info.content === undefined) {
|
else if (info.doc === undefined) {
|
||||||
return (<Headline menu={menu_list()}>
|
return (<Headline menu={menu_list()}>
|
||||||
<LoadingCircle />
|
<LoadingCircle />
|
||||||
</Headline>
|
</Headline>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
const ReaderPage = getPresenter(info.content);
|
const ReaderPage = getPresenter(info.doc);
|
||||||
return (<Switch>
|
return (<Switch>
|
||||||
<Route exact path={`${prop.match.path}/:id`}>
|
<Route exact path={`${prop.match.path}/:id`}>
|
||||||
<Headline menu={menu_list()}>
|
<Headline menu={menu_list()}>
|
||||||
<ContentInfo content={info.content}></ContentInfo>
|
<ContentInfo document={info.doc}></ContentInfo>
|
||||||
</Headline>
|
</Headline>
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path={`${prop.match.path}/:id/reader`}>
|
<Route exact path={`${prop.match.path}/:id/reader`}>
|
||||||
@ -79,7 +79,7 @@ export const ContentAbout = (prop: { match: MatchType }) => {
|
|||||||
content:classes.noPaddingContent,
|
content:classes.noPaddingContent,
|
||||||
toolbar:classes.noPaddingToolbar
|
toolbar:classes.noPaddingToolbar
|
||||||
}}>
|
}}>
|
||||||
<ReaderPage content={info.content}></ReaderPage>
|
<ReaderPage doc={info.doc}></ReaderPage>
|
||||||
</Headline>
|
</Headline>
|
||||||
</Route>
|
</Route>
|
||||||
<Route>
|
<Route>
|
||||||
|
@ -8,9 +8,8 @@ import { QueryStringToMap } from '../accessor/util';
|
|||||||
export const Gallery = ()=>{
|
export const Gallery = ()=>{
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const query = QueryStringToMap(location.search);
|
const query = QueryStringToMap(location.search);
|
||||||
location;
|
|
||||||
const menu_list = CommonMenuList({url:location.search});
|
const menu_list = CommonMenuList({url:location.search});
|
||||||
|
|
||||||
return (<Headline menu={menu_list}>
|
return (<Headline menu={menu_list}>
|
||||||
<GalleryInfo option={query}></GalleryInfo>
|
<GalleryInfo option={query}></GalleryInfo>
|
||||||
</Headline>)
|
</Headline>)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, {useState, useEffect} from 'react';
|
import React, {useState, useEffect} from 'react';
|
||||||
import { Typography, useTheme } from '@material-ui/core';
|
import { Typography, useTheme } from '@material-ui/core';
|
||||||
import { Content } from '../../accessor/contents';
|
import { Document } from '../../accessor/document';
|
||||||
|
|
||||||
type MangaType = "manga"|"artist cg"|"donjinshi"|"western"
|
type MangaType = "manga"|"artist cg"|"donjinshi"|"western"
|
||||||
|
|
||||||
@ -13,9 +13,9 @@ export type PresentableTag = {
|
|||||||
tags: string[],
|
tags: string[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MangaReader = (props:{content:Content})=>{
|
export const MangaReader = (props:{doc:Document})=>{
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const additional = props.content.additional;
|
const additional = props.doc.additional;
|
||||||
const [curPage,setCurPage] = useState(0);
|
const [curPage,setCurPage] = useState(0);
|
||||||
if(!('page' in additional)){
|
if(!('page' in additional)){
|
||||||
console.error("invalid content : page read fail : "+ JSON.stringify(additional));
|
console.error("invalid content : page read fail : "+ JSON.stringify(additional));
|
||||||
@ -40,7 +40,7 @@ export const MangaReader = (props:{content:Content})=>{
|
|||||||
});
|
});
|
||||||
//theme.mixins.toolbar.minHeight;
|
//theme.mixins.toolbar.minHeight;
|
||||||
return (<div style={{overflow: 'hidden', alignSelf:'center'}}>
|
return (<div style={{overflow: 'hidden', alignSelf:'center'}}>
|
||||||
<img onClick={PageUP} src={`/content/${props.content.id}/manga/${curPage}`}
|
<img onClick={PageUP} src={`/api/doc/${props.doc.id}/manga/${curPage}`}
|
||||||
style={{maxWidth:'100%', maxHeight:'calc(100vh - 64px)'}}></img>
|
style={{maxWidth:'100%', maxHeight:'calc(100vh - 64px)'}}></img>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { Typography } from '@material-ui/core';
|
import { Typography } from '@material-ui/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Content, makeThumbnailUrl } from '../../accessor/contents';
|
import { Document, makeThumbnailUrl } from '../../accessor/document';
|
||||||
import {MangaReader} from './manga';
|
import {MangaReader} from './manga';
|
||||||
import {VideoReader} from './video'
|
import {VideoReader} from './video'
|
||||||
|
|
||||||
export interface PagePresenterProp{
|
export interface PagePresenterProp{
|
||||||
content:Content,
|
doc:Document,
|
||||||
className?:string
|
className?:string
|
||||||
}
|
}
|
||||||
interface PagePresenter{
|
interface PagePresenter{
|
||||||
(prop:PagePresenterProp):JSX.Element
|
(prop:PagePresenterProp):JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPresenter = (content:Content):PagePresenter => {
|
export const getPresenter = (content:Document):PagePresenter => {
|
||||||
switch (content.content_type) {
|
switch (content.content_type) {
|
||||||
case "manga":
|
case "manga":
|
||||||
return MangaReader;
|
return MangaReader;
|
||||||
@ -22,7 +22,7 @@ export const getPresenter = (content:Content):PagePresenter => {
|
|||||||
return ()=><Typography variant='h2'>Not implemented reader</Typography>;
|
return ()=><Typography variant='h2'>Not implemented reader</Typography>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThumbnailContainer = (props:{content:Content, className?:string})=>{
|
export const ThumbnailContainer = (props:{content:Document, className?:string})=>{
|
||||||
const thumbnailurl = makeThumbnailUrl(props.content);
|
const thumbnailurl = makeThumbnailUrl(props.content);
|
||||||
if(props.content.content_type === "video"){
|
if(props.content.content_type === "video"){
|
||||||
return (<video src={thumbnailurl} muted autoPlay loop className={props.className}></video>)
|
return (<video src={thumbnailurl} muted autoPlay loop className={props.className}></video>)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Content } from '../../accessor/contents';
|
import { Document } from '../../accessor/document';
|
||||||
|
|
||||||
export const VideoReader = (props:{content:Content})=>{
|
export const VideoReader = (props:{doc:Document})=>{
|
||||||
const id = props.content.id;
|
const id = props.doc.id;
|
||||||
return <video controls autoPlay src={`/content/${props.content.id}/video`} style={{maxHeight:'100%',maxWidth:'100%'}}></video>;
|
return <video controls autoPlay src={`/api/doc/${props.doc.id}/video`} style={{maxHeight:'100%',maxWidth:'100%'}}></video>;
|
||||||
}
|
}
|
@ -1,8 +1,14 @@
|
|||||||
import {ContentReferrer} from './referrer';
|
import {ContentFile} from './referrer';
|
||||||
import {createDefaultClass,registerContentReferrer} from './referrer';
|
import {createDefaultClass,registerContentReferrer} from './referrer';
|
||||||
|
import {readZip} from '../util/zipwrap';
|
||||||
export class MangaReferrer extends createDefaultClass("manga"){
|
export class MangaReferrer extends createDefaultClass("manga"){
|
||||||
constructor(path:string){
|
constructor(path:string){
|
||||||
super(path);
|
super(path);
|
||||||
|
/*(async ()=>{
|
||||||
|
const zip = await readZip(path);
|
||||||
|
const entry = zip.entries();
|
||||||
|
|
||||||
|
})*/
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
registerContentReferrer(MangaReferrer);
|
registerContentReferrer(MangaReferrer);
|
@ -1,3 +1,3 @@
|
|||||||
import './manga';
|
import './manga';
|
||||||
import './video';
|
import './video';
|
||||||
export {ContentReferrer,createContentReferrer} from './referrer';
|
export {ContentFile as ContentReferrer,createContentReferrer} from './referrer';
|
@ -6,15 +6,15 @@ import {extname} from 'path';
|
|||||||
/**
|
/**
|
||||||
* content file or directory referrer
|
* content file or directory referrer
|
||||||
*/
|
*/
|
||||||
export interface ContentReferrer{
|
export interface ContentFile{
|
||||||
getHash():Promise<string>;
|
getHash():Promise<string>;
|
||||||
readonly path: string;
|
readonly path: string;
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
desc: object|undefined;
|
desc: object|undefined;
|
||||||
}
|
}
|
||||||
type ContentReferrerConstructor = (new (path:string,desc?:object) => ContentReferrer)&{content_type:string};
|
type ContentFileConstructor = (new (path:string,desc?:object) => ContentFile)&{content_type:string};
|
||||||
export const createDefaultClass = (type:string):ContentReferrerConstructor=>{
|
export const createDefaultClass = (type:string):ContentFileConstructor=>{
|
||||||
let cons = class implements ContentReferrer{
|
let cons = class implements ContentFile{
|
||||||
readonly path: string;
|
readonly path: string;
|
||||||
type = type;
|
type = type;
|
||||||
static content_type = type;
|
static content_type = type;
|
||||||
@ -37,8 +37,8 @@ export const createDefaultClass = (type:string):ContentReferrerConstructor=>{
|
|||||||
};
|
};
|
||||||
return cons;
|
return cons;
|
||||||
}
|
}
|
||||||
let ContstructorTable:{[k:string]:ContentReferrerConstructor} = {};
|
let ContstructorTable:{[k:string]:ContentFileConstructor} = {};
|
||||||
export function registerContentReferrer(s: ContentReferrerConstructor){
|
export function registerContentReferrer(s: ContentFileConstructor){
|
||||||
console.log(`registered content type: ${s.content_type}`)
|
console.log(`registered content type: ${s.content_type}`)
|
||||||
ContstructorTable[s.content_type] = s;
|
ContstructorTable[s.content_type] = s;
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ export function createContentReferrer(type:string,path:string,desc?:object){
|
|||||||
}
|
}
|
||||||
return new constructorMethod(path,desc);
|
return new constructorMethod(path,desc);
|
||||||
}
|
}
|
||||||
export function getContentRefererConstructor(type:string): ContentReferrerConstructor|undefined{
|
export function getContentRefererConstructor(type:string): ContentFileConstructor|undefined{
|
||||||
const ret = ContstructorTable[type];
|
const ret = ContstructorTable[type];
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import {ContentReferrer, registerContentReferrer} from './referrer';
|
import {ContentFile, registerContentReferrer} from './referrer';
|
||||||
import {createDefaultClass} from './referrer';
|
import {createDefaultClass} from './referrer';
|
||||||
|
|
||||||
export class VideoReferrer extends createDefaultClass("video"){
|
export class VideoReferrer extends createDefaultClass("video"){
|
||||||
|
@ -1,53 +1,53 @@
|
|||||||
import { Content, ContentContent, ContentAccessor, QueryListOption } from '../model/contents';
|
import { Document, DocumentBody, DocumentAccessor, QueryListOption } from '../model/doc';
|
||||||
import Knex from 'knex';
|
import Knex from 'knex';
|
||||||
import {createKnexTagController} from './tag';
|
import {createKnexTagController} from './tag';
|
||||||
import { TagAccessor } from '../model/tag';
|
import { TagAccessor } from '../model/tag';
|
||||||
|
|
||||||
type DBTagContentRelation = {
|
type DBTagContentRelation = {
|
||||||
content_id:number,
|
doc_id:number,
|
||||||
tag_name:string
|
tag_name:string
|
||||||
}
|
}
|
||||||
|
|
||||||
class KnexContentsAccessor implements ContentAccessor{
|
class KnexContentsAccessor implements DocumentAccessor{
|
||||||
knex : Knex;
|
knex : Knex;
|
||||||
tagController: TagAccessor;
|
tagController: TagAccessor;
|
||||||
constructor(knex : Knex){
|
constructor(knex : Knex){
|
||||||
this.knex = knex;
|
this.knex = knex;
|
||||||
this.tagController = createKnexTagController(knex);
|
this.tagController = createKnexTagController(knex);
|
||||||
}
|
}
|
||||||
async add(c: ContentContent){
|
async add(c: DocumentBody){
|
||||||
const {tags,additional, ...rest} = c;
|
const {tags,additional, ...rest} = c;
|
||||||
const id_lst = await this.knex.insert({
|
const id_lst = await this.knex.insert({
|
||||||
additional:JSON.stringify(additional),
|
additional:JSON.stringify(additional),
|
||||||
...rest
|
...rest
|
||||||
}).into('contents');
|
}).into('document');
|
||||||
const id = id_lst[0];
|
const id = id_lst[0];
|
||||||
for (const it of tags) {
|
for (const it of tags) {
|
||||||
this.tagController.addTag({name:it});
|
this.tagController.addTag({name:it});
|
||||||
}
|
}
|
||||||
if(tags.length > 0){
|
if(tags.length > 0){
|
||||||
await this.knex.insert<DBTagContentRelation>(
|
await this.knex.insert<DBTagContentRelation>(
|
||||||
tags.map(x=>({content_id:id,tag_name:x}))
|
tags.map(x=>({doc_id:id,tag_name:x}))
|
||||||
).into("content_tag_relation");
|
).into("doc_tag_relation");
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
async del(id:number) {
|
async del(id:number) {
|
||||||
if (await this.findById(id) !== undefined){
|
if (await this.findById(id) !== undefined){
|
||||||
await this.knex.delete().from("content_tag_relation").where({content_id:id});
|
await this.knex.delete().from("doc_tag_relation").where({doc_id:id});
|
||||||
await this.knex.delete().from("contents").where({id:id});
|
await this.knex.delete().from("document").where({id:id});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
async findById(id:number,tagload?:boolean): Promise<Content|undefined>{
|
async findById(id:number,tagload?:boolean): Promise<Document|undefined>{
|
||||||
const s = await this.knex.select("*").from("contents").where({id:id});
|
const s = await this.knex.select("*").from("document").where({id:id});
|
||||||
if(s.length === 0) return undefined;
|
if(s.length === 0) return undefined;
|
||||||
const first = s[0];
|
const first = s[0];
|
||||||
let ret_tags:string[] = []
|
let ret_tags:string[] = []
|
||||||
if(tagload === true){
|
if(tagload === true){
|
||||||
const tags : DBTagContentRelation[] = await this.knex.select("*")
|
const tags : DBTagContentRelation[] = await this.knex.select("*")
|
||||||
.from("content_tag_relation").where({content_id:first.id});
|
.from("doc_tag_relation").where({doc_id:first.id});
|
||||||
ret_tags = tags.map(x=>x.tag_name);
|
ret_tags = tags.map(x=>x.tag_name);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -70,13 +70,13 @@ class KnexContentsAccessor implements ContentAccessor{
|
|||||||
const buildquery = ()=>{
|
const buildquery = ()=>{
|
||||||
let query = this.knex.select("*");
|
let query = this.knex.select("*");
|
||||||
if(allow_tag.length > 0){
|
if(allow_tag.length > 0){
|
||||||
query = query.from("content_tag_relation").innerJoin("contents","content_tag_relation.content_id","contents.id");
|
query = query.from("doc_tag_relation").innerJoin("document","doc_tag_relation.doc_id","document.id");
|
||||||
for(const tag of allow_tag){
|
for(const tag of allow_tag){
|
||||||
query = query.where({tag_name:tag});
|
query = query.where({tag_name:tag});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
query = query.from("contents");
|
query = query.from("document");
|
||||||
}
|
}
|
||||||
if(word !== undefined){
|
if(word !== undefined){
|
||||||
//don't worry about sql injection.
|
//don't worry about sql injection.
|
||||||
@ -98,56 +98,58 @@ class KnexContentsAccessor implements ContentAccessor{
|
|||||||
}
|
}
|
||||||
let query = buildquery();
|
let query = buildquery();
|
||||||
//console.log(query.toSQL());
|
//console.log(query.toSQL());
|
||||||
let result:Content[] = await query;
|
let result:Document[] = await query;
|
||||||
for(let i of result){
|
for(let i of result){
|
||||||
i.additional = JSON.parse((i.additional as unknown) as string);
|
i.additional = JSON.parse((i.additional as unknown) as string);
|
||||||
}
|
}
|
||||||
if(eager_loading){
|
if(eager_loading){
|
||||||
let idmap: {[index:number]:Content} = {};
|
let idmap: {[index:number]:Document} = {};
|
||||||
for(const r of result){
|
for(const r of result){
|
||||||
idmap[r.id] = r;
|
idmap[r.id] = r;
|
||||||
r.tags = [];
|
r.tags = [];
|
||||||
}
|
}
|
||||||
let subquery = buildquery();
|
let subquery = buildquery();
|
||||||
let tagresult:{id:number,tag_name:string}[] = await this.knex.select("id","content_tag_relation.tag_name").from(subquery)
|
let tagquery= this.knex.select("id","doc_tag_relation.tag_name").from(subquery)
|
||||||
.innerJoin("content_tag_relation","content_tag_relation.content_id","id");
|
.innerJoin("doc_tag_relation","doc_tag_relation.doc_id","id");
|
||||||
|
//console.log(tagquery.toSQL());
|
||||||
|
let tagresult:{id:number,tag_name:string}[] = await tagquery;
|
||||||
for(const {id,tag_name} of tagresult){
|
for(const {id,tag_name} of tagresult){
|
||||||
idmap[id].tags.push(tag_name);
|
idmap[id].tags.push(tag_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
async findListByBasePath(path:string):Promise<Content[]>{
|
async findListByBasePath(path:string):Promise<Document[]>{
|
||||||
let results = await this.knex.select("*").from("contents").where({basepath:path});
|
let results = await this.knex.select("*").from("document").where({basepath:path});
|
||||||
return results.map(x=>({
|
return results.map(x=>({
|
||||||
...x,
|
...x,
|
||||||
tags:[],
|
tags:[],
|
||||||
additional:JSON.parse(x.additional || "{}"),
|
additional:JSON.parse(x.additional || "{}"),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
async update(c:Partial<Content> & { id:number }){
|
async update(c:Partial<Document> & { id:number }){
|
||||||
const {id,tags,...rest} = c;
|
const {id,tags,...rest} = c;
|
||||||
if (await this.findById(id) !== undefined){
|
if (await this.findById(id) !== undefined){
|
||||||
await this.knex.update(rest).where({id: id}).from("contents");
|
await this.knex.update(rest).where({id: id}).from("document");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
async addTag(c: Content,tag_name:string){
|
async addTag(c: Document,tag_name:string){
|
||||||
if (c.tags.includes(tag_name)) return false;
|
if (c.tags.includes(tag_name)) return false;
|
||||||
this.tagController.addTag({name:tag_name});
|
this.tagController.addTag({name:tag_name});
|
||||||
await this.knex.insert<DBTagContentRelation>({tag_name: tag_name, content_id: c.id})
|
await this.knex.insert<DBTagContentRelation>({tag_name: tag_name, doc_id: c.id})
|
||||||
.into("content_tag_relation");
|
.into("doc_tag_relation");
|
||||||
c.tags.push(tag_name);
|
c.tags.push(tag_name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async delTag(c: Content,tag_name:string){
|
async delTag(c: Document,tag_name:string){
|
||||||
if (c.tags.includes(tag_name)) return false;
|
if (c.tags.includes(tag_name)) return false;
|
||||||
await this.knex.delete().where({tag_name: tag_name,content_id: c.id}).from("content_tag_relation");
|
await this.knex.delete().where({tag_name: tag_name,doc_id: c.id}).from("doc_tag_relation");
|
||||||
c.tags.push(tag_name);
|
c.tags.push(tag_name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const createKnexContentsAccessor = (knex:Knex): ContentAccessor=>{
|
export const createKnexContentsAccessor = (knex:Knex): DocumentAccessor=>{
|
||||||
return new KnexContentsAccessor(knex);
|
return new KnexContentsAccessor(knex);
|
||||||
}
|
}
|
@ -1,3 +1,3 @@
|
|||||||
export * from './contents';
|
export * from './doc';
|
||||||
export * from './tag';
|
export * from './tag';
|
||||||
export * from './user';
|
export * from './user';
|
@ -1,6 +1,6 @@
|
|||||||
import { watch } from 'fs';
|
import { watch } from 'fs';
|
||||||
import { promises } from 'fs';
|
import { promises } from 'fs';
|
||||||
import { ContentReferrer, createContentReferrer, getContentRefererConstructor } from '../content/referrer'
|
import { ContentFile, createContentReferrer, getContentRefererConstructor } from '../content/referrer'
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const readdir = promises.readdir;
|
const readdir = promises.readdir;
|
||||||
@ -12,21 +12,21 @@ export class Watcher{
|
|||||||
/**
|
/**
|
||||||
* @todo : alter type Map<string,ContentReferrer>
|
* @todo : alter type Map<string,ContentReferrer>
|
||||||
*/
|
*/
|
||||||
private _added: ContentReferrer[];
|
private _added: ContentFile[];
|
||||||
private _deleted: ContentReferrer[];
|
private _deleted: ContentFile[];
|
||||||
constructor(path:string,type:string){
|
constructor(path:string,type:string){
|
||||||
this._path = path;
|
this._path = path;
|
||||||
this._added =[];
|
this._added =[];
|
||||||
this._deleted =[];
|
this._deleted =[];
|
||||||
this._type = type;
|
this._type = type;
|
||||||
}
|
}
|
||||||
public get added() : ContentReferrer[] {
|
public get added() : ContentFile[] {
|
||||||
return this._added;
|
return this._added;
|
||||||
}
|
}
|
||||||
/*public set added(diff : FileDiff[]) {
|
/*public set added(diff : FileDiff[]) {
|
||||||
this._added = diff;
|
this._added = diff;
|
||||||
}*/
|
}*/
|
||||||
public get deleted(): ContentReferrer[]{
|
public get deleted(): ContentFile[]{
|
||||||
return this._deleted;
|
return this._deleted;
|
||||||
}
|
}
|
||||||
/*public set deleted(diff : FileDiff[]){
|
/*public set deleted(diff : FileDiff[]){
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import {TagAccessor} from './tag';
|
import {TagAccessor} from './tag';
|
||||||
import {check_type} from './../util/type_check'
|
import {check_type} from '../util/type_check'
|
||||||
|
|
||||||
type JSONPrimitive = null|boolean|number|string;
|
type JSONPrimitive = null|boolean|number|string;
|
||||||
interface JSONMap extends Record<string, JSONType>{}
|
interface JSONMap extends Record<string, JSONType>{}
|
||||||
interface JSONArray extends Array<JSONType>{};
|
interface JSONArray extends Array<JSONType>{};
|
||||||
type JSONType = JSONMap|JSONPrimitive|JSONArray;
|
type JSONType = JSONMap|JSONPrimitive|JSONArray;
|
||||||
|
|
||||||
export interface ContentContent{
|
export interface DocumentBody{
|
||||||
title : string,
|
title : string,
|
||||||
content_type : string,
|
content_type : string,
|
||||||
basepath : string,
|
basepath : string,
|
||||||
@ -16,7 +16,7 @@ export interface ContentContent{
|
|||||||
tags : string[],//eager loading
|
tags : string[],//eager loading
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetaContentContent = {
|
export const MetaContentBody = {
|
||||||
title : "string",
|
title : "string",
|
||||||
content_type : "string",
|
content_type : "string",
|
||||||
basepath : "string",
|
basepath : "string",
|
||||||
@ -26,18 +26,18 @@ export const MetaContentContent = {
|
|||||||
tags : "string[]",
|
tags : "string[]",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isContentContent = (c : any):c is ContentContent =>{
|
export const isDocBody = (c : any):c is DocumentBody =>{
|
||||||
return check_type<ContentContent>(c,MetaContentContent);
|
return check_type<DocumentBody>(c,MetaContentBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Content extends ContentContent{
|
export interface Document extends DocumentBody{
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isContent = (c: any):c is Content =>{
|
export const isDoc = (c: any):c is Document =>{
|
||||||
if('id' in c && typeof c['id'] === "number"){
|
if('id' in c && typeof c['id'] === "number"){
|
||||||
const {id, ...rest} = c;
|
const {id, ...rest} = c;
|
||||||
return isContentContent(rest);
|
return isDocBody(rest);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -59,11 +59,11 @@ export type QueryListOption = {
|
|||||||
*/
|
*/
|
||||||
use_offset?:boolean,
|
use_offset?:boolean,
|
||||||
/**
|
/**
|
||||||
* cursor of contents
|
* cursor of documents
|
||||||
*/
|
*/
|
||||||
cursor?:number,
|
cursor?:number,
|
||||||
/**
|
/**
|
||||||
* offset of contents
|
* offset of documents
|
||||||
*/
|
*/
|
||||||
offset?:number,
|
offset?:number,
|
||||||
/**
|
/**
|
||||||
@ -77,41 +77,41 @@ export type QueryListOption = {
|
|||||||
content_type?:string
|
content_type?:string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentAccessor{
|
export interface DocumentAccessor{
|
||||||
/**
|
/**
|
||||||
* find list by option
|
* find list by option
|
||||||
* @returns content list
|
* @returns documents list
|
||||||
*/
|
*/
|
||||||
findList: (option?:QueryListOption)=>Promise<Content[]>,
|
findList: (option?:QueryListOption)=>Promise<Document[]>,
|
||||||
/**
|
/**
|
||||||
* @returns content if exist, otherwise undefined
|
* @returns document if exist, otherwise undefined
|
||||||
*/
|
*/
|
||||||
findById: (id:number,tagload?:boolean)=> Promise<Content| undefined>,
|
findById: (id:number,tagload?:boolean)=> Promise<Document| undefined>,
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
findListByBasePath:(basepath: string)=>Promise<Content[]>;
|
findListByBasePath:(basepath: string)=>Promise<Document[]>;
|
||||||
/**
|
/**
|
||||||
* update content except tag.
|
* update document except tag.
|
||||||
*/
|
*/
|
||||||
update:(c:Partial<Content> & { id:number })=>Promise<boolean>;
|
update:(c:Partial<Document> & { id:number })=>Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* add content
|
* add document
|
||||||
*/
|
*/
|
||||||
add:(c:ContentContent)=>Promise<number>;
|
add:(c:DocumentBody)=>Promise<number>;
|
||||||
/**
|
/**
|
||||||
* delete content
|
* delete document
|
||||||
* @returns if it exists, return true.
|
* @returns if it exists, return true.
|
||||||
*/
|
*/
|
||||||
del:(id:number)=>Promise<boolean>;
|
del:(id:number)=>Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* @param c Valid Content
|
* @param c Valid Document
|
||||||
* @param tagname tag name to add
|
* @param tagname tag name to add
|
||||||
* @returns if success, return true
|
* @returns if success, return true
|
||||||
*/
|
*/
|
||||||
addTag:(c:Content,tag_name:string)=>Promise<boolean>;
|
addTag:(c:Document,tag_name:string)=>Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* @returns if success, return true
|
* @returns if success, return true
|
||||||
*/
|
*/
|
||||||
delTag:(c:Content,tag_name:string)=>Promise<boolean>;
|
delTag:(c:Document,tag_name:string)=>Promise<boolean>;
|
||||||
};
|
};
|
@ -1,3 +1,3 @@
|
|||||||
export * from './contents';
|
export * from './doc';
|
||||||
export * from './tag';
|
export * from './tag';
|
||||||
export * from './user';
|
export * from './user';
|
@ -14,7 +14,7 @@ const all_middleware = (cont: string|undefined, restarg: string|undefined)=>asyn
|
|||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(ctx.state.content.type != cont){
|
if(ctx.state.location.type != cont){
|
||||||
console.error("not matched")
|
console.error("not matched")
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
return;
|
return;
|
||||||
@ -24,8 +24,7 @@ const all_middleware = (cont: string|undefined, restarg: string|undefined)=>asyn
|
|||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rest = "/"+(restarg as string|undefined || "");
|
const rest = "/"+(restarg || "");
|
||||||
|
|
||||||
const result = router.match(rest,"GET");
|
const result = router.match(rest,"GET");
|
||||||
if(!result.route){
|
if(!result.route){
|
||||||
return await next();
|
return await next();
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
import { Context, Next } from 'koa';
|
import { Context, Next } from 'koa';
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import {Content, ContentAccessor, isContentContent} from './../model/contents';
|
import {Document, DocumentAccessor, isDocBody} from '../model/doc';
|
||||||
import {QueryListOption} from './../model/contents';
|
import {QueryListOption} from '../model/doc';
|
||||||
import {ParseQueryNumber, ParseQueryArray, ParseQueryBoolean} from './util'
|
import {ParseQueryNumber, ParseQueryArray, ParseQueryBoolean} from './util'
|
||||||
import {sendError} from './error_handler';
|
import {sendError} from './error_handler';
|
||||||
import { createContentReferrer } from '../content/mod';
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import {AllContentRouter} from './all';
|
import {AllContentRouter} from './all';
|
||||||
import {createPermissionCheckMiddleware as PerCheck, Permission as Per, AdminOnlyMiddleware as AdminOnly} from '../permission/permission';
|
import {createPermissionCheckMiddleware as PerCheck, Permission as Per, AdminOnlyMiddleware as AdminOnly} from '../permission/permission';
|
||||||
|
import {ContentLocation} from './context'
|
||||||
|
|
||||||
const ContentIDHandler = (controller: ContentAccessor) => async (ctx: Context,next: Next)=>{
|
const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context,next: Next)=>{
|
||||||
const num = Number.parseInt(ctx.params['num']);
|
const num = Number.parseInt(ctx.params['num']);
|
||||||
let content = await controller.findById(num,true);
|
let document = await controller.findById(num,true);
|
||||||
if (content == undefined){
|
if (document == undefined){
|
||||||
return sendError(404,"content does not exist.");
|
return sendError(404,"document does not exist.");
|
||||||
}
|
}
|
||||||
ctx.body = content;
|
ctx.body = document;
|
||||||
ctx.type = 'json';
|
ctx.type = 'json';
|
||||||
console.log(content.additional);
|
console.log(document.additional);
|
||||||
};
|
};
|
||||||
const ContentTagIDHandler = (controller: ContentAccessor) => async (ctx: Context,next: Next)=>{
|
const ContentTagIDHandler = (controller: DocumentAccessor) => async (ctx: Context,next: Next)=>{
|
||||||
const num = Number.parseInt(ctx.params['num']);
|
const num = Number.parseInt(ctx.params['num']);
|
||||||
let content = await controller.findById(num,true);
|
let document = await controller.findById(num,true);
|
||||||
if (content == undefined){
|
if (document == undefined){
|
||||||
return sendError(404,"content does not exist.");
|
return sendError(404,"document does not exist.");
|
||||||
}
|
}
|
||||||
ctx.body = content.tags || [];
|
ctx.body = document.tags || [];
|
||||||
ctx.type = 'json';
|
ctx.type = 'json';
|
||||||
};
|
};
|
||||||
const ContentQueryHandler = (controller : ContentAccessor) => async (ctx: Context,next: Next)=>{
|
const ContentQueryHandler = (controller : DocumentAccessor) => async (ctx: Context,next: Next)=>{
|
||||||
const limit = ParseQueryNumber(ctx.query['limit']);
|
const limit = ParseQueryNumber(ctx.query['limit']);
|
||||||
const cursor = ParseQueryNumber(ctx.query['cursor']);
|
const cursor = ParseQueryNumber(ctx.query['cursor']);
|
||||||
const word: string|undefined = ctx.query['word'];
|
const word: string|undefined = ctx.query['word'];
|
||||||
@ -52,35 +52,35 @@ const ContentQueryHandler = (controller : ContentAccessor) => async (ctx: Contex
|
|||||||
use_offset: use_offset,
|
use_offset: use_offset,
|
||||||
content_type:content_type,
|
content_type:content_type,
|
||||||
};
|
};
|
||||||
let content = await controller.findList(option);
|
let document = await controller.findList(option);
|
||||||
ctx.body = content;
|
ctx.body = document;
|
||||||
ctx.type = 'json';
|
ctx.type = 'json';
|
||||||
}
|
}
|
||||||
const UpdateContentHandler = (controller : ContentAccessor) => async (ctx: Context, next: Next) => {
|
const UpdateContentHandler = (controller : DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
const num = Number.parseInt(ctx.params['num']);
|
const num = Number.parseInt(ctx.params['num']);
|
||||||
if(ctx.request.type !== 'json'){
|
if(ctx.request.type !== 'json'){
|
||||||
return sendError(400,"update fail. invalid content type: it is not json.");
|
return sendError(400,"update fail. invalid document type: it is not json.");
|
||||||
}
|
}
|
||||||
if(typeof ctx.request.body !== "object"){
|
if(typeof ctx.request.body !== "object"){
|
||||||
return sendError(400,"update fail. invalid argument: not");
|
return sendError(400,"update fail. invalid argument: not");
|
||||||
}
|
}
|
||||||
const content_desc: Partial<Content> & {id: number} = {
|
const content_desc: Partial<Document> & {id: number} = {
|
||||||
id:num,...ctx.request.body
|
id:num,...ctx.request.body
|
||||||
};
|
};
|
||||||
const success = await controller.update(content_desc);
|
const success = await controller.update(content_desc);
|
||||||
ctx.body = JSON.stringify(success);
|
ctx.body = JSON.stringify(success);
|
||||||
ctx.type = 'json';
|
ctx.type = 'json';
|
||||||
}
|
}
|
||||||
const CreateContentHandler = (controller : ContentAccessor) => async (ctx: Context, next: Next) => {
|
const CreateContentHandler = (controller : DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
const content_desc = ctx.request.body;
|
const content_desc = ctx.request.body;
|
||||||
if(!isContentContent(content_desc)){
|
if(!isDocBody(content_desc)){
|
||||||
return sendError(400,"it is not a valid format");
|
return sendError(400,"it is not a valid format");
|
||||||
}
|
}
|
||||||
const id = await controller.add(content_desc);
|
const id = await controller.add(content_desc);
|
||||||
ctx.body = JSON.stringify(id);
|
ctx.body = JSON.stringify(id);
|
||||||
ctx.type = 'json';
|
ctx.type = 'json';
|
||||||
};
|
};
|
||||||
const AddTagHandler = (controller: ContentAccessor)=>async (ctx: Context, next: Next)=>{
|
const AddTagHandler = (controller: DocumentAccessor)=>async (ctx: Context, next: Next)=>{
|
||||||
let tag_name = ctx.params['tag'];
|
let tag_name = ctx.params['tag'];
|
||||||
const num = Number.parseInt(ctx.params['num']);
|
const num = Number.parseInt(ctx.params['num']);
|
||||||
if(typeof tag_name === undefined){
|
if(typeof tag_name === undefined){
|
||||||
@ -95,7 +95,7 @@ const AddTagHandler = (controller: ContentAccessor)=>async (ctx: Context, next:
|
|||||||
ctx.body = JSON.stringify(r);
|
ctx.body = JSON.stringify(r);
|
||||||
ctx.type = 'json';
|
ctx.type = 'json';
|
||||||
};
|
};
|
||||||
const DelTagHandler = (controller: ContentAccessor)=>async (ctx: Context, next: Next)=>{
|
const DelTagHandler = (controller: DocumentAccessor)=>async (ctx: Context, next: Next)=>{
|
||||||
let tag_name = ctx.params['tag'];
|
let tag_name = ctx.params['tag'];
|
||||||
const num = Number.parseInt(ctx.params['num']);
|
const num = Number.parseInt(ctx.params['num']);
|
||||||
if(typeof tag_name === undefined){
|
if(typeof tag_name === undefined){
|
||||||
@ -110,24 +110,28 @@ const DelTagHandler = (controller: ContentAccessor)=>async (ctx: Context, next:
|
|||||||
ctx.body = JSON.stringify(r);
|
ctx.body = JSON.stringify(r);
|
||||||
ctx.type = 'json';
|
ctx.type = 'json';
|
||||||
}
|
}
|
||||||
const DeleteContentHandler = (controller : ContentAccessor) => async (ctx: Context, next: Next) => {
|
const DeleteContentHandler = (controller : DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
const num = Number.parseInt(ctx.params['num']);
|
const num = Number.parseInt(ctx.params['num']);
|
||||||
const r = await controller.del(num);
|
const r = await controller.del(num);
|
||||||
ctx.body = JSON.stringify(r);
|
ctx.body = JSON.stringify(r);
|
||||||
ctx.type = 'json';
|
ctx.type = 'json';
|
||||||
};
|
};
|
||||||
const ContentHandler = (controller : ContentAccessor) => async (ctx:Context, next:Next) => {
|
const ContentHandler = (controller : DocumentAccessor) => async (ctx:Context, next:Next) => {
|
||||||
const num = Number.parseInt(ctx.params['num']);
|
const num = Number.parseInt(ctx.params['num']);
|
||||||
let content = await controller.findById(num,true);
|
let document = await controller.findById(num,true);
|
||||||
if (content == undefined){
|
if (document == undefined){
|
||||||
return sendError(404,"content does not exist.");
|
return sendError(404,"document does not exist.");
|
||||||
}
|
}
|
||||||
const path = join(content.basepath,content.filename);
|
const path = join(document.basepath,document.filename);
|
||||||
ctx.state['content'] = createContentReferrer(content.content_type,path,content.additional);
|
ctx.state['location'] = {
|
||||||
|
path:path,
|
||||||
|
type:document.content_type,
|
||||||
|
additional:document.additional,
|
||||||
|
} as ContentLocation;
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContentRouter = (controller: ContentAccessor)=>{
|
export const getContentRouter = (controller: DocumentAccessor)=>{
|
||||||
const ret = new Router();
|
const ret = new Router();
|
||||||
ret.get("/search",PerCheck(Per.QueryContent),ContentQueryHandler(controller));
|
ret.get("/search",PerCheck(Per.QueryContent),ContentQueryHandler(controller));
|
||||||
ret.get("/:num(\\d+)",PerCheck(Per.QueryContent),ContentIDHandler(controller));
|
ret.get("/:num(\\d+)",PerCheck(Per.QueryContent),ContentIDHandler(controller));
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import {ContentReferrer} from '../content/mod';
|
import {ContentReferrer} from '../content/mod';
|
||||||
|
|
||||||
|
export type ContentLocation = {
|
||||||
|
path:string,
|
||||||
|
type:string,
|
||||||
|
additional:object|undefined,
|
||||||
|
}
|
||||||
export interface ContentContext{
|
export interface ContentContext{
|
||||||
content:ContentReferrer
|
location:ContentLocation
|
||||||
}
|
}
|
@ -37,14 +37,14 @@ export class MangaRouter extends Router<ContentContext>{
|
|||||||
constructor(){
|
constructor(){
|
||||||
super();
|
super();
|
||||||
this.get("/",async (ctx,next)=>{
|
this.get("/",async (ctx,next)=>{
|
||||||
await renderZipImage(ctx,ctx.state.content.path,0);
|
await renderZipImage(ctx,ctx.state.location.path,0);
|
||||||
});
|
});
|
||||||
this.get("/:page(\\d+)",async (ctx,next)=>{
|
this.get("/:page(\\d+)",async (ctx,next)=>{
|
||||||
const page = Number.parseInt(ctx.params['page']);
|
const page = Number.parseInt(ctx.params['page']);
|
||||||
await renderZipImage(ctx,ctx.state.content.path,page);
|
await renderZipImage(ctx,ctx.state.location.path,page);
|
||||||
});
|
});
|
||||||
this.get("/thumbnail", async (ctx,next)=>{
|
this.get("/thumbnail", async (ctx,next)=>{
|
||||||
await renderZipImage(ctx,ctx.state.content.path,0);
|
await renderZipImage(ctx,ctx.state.location.path,0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,10 +57,10 @@ export class VideoRouter extends Router<ContentContext>{
|
|||||||
constructor(){
|
constructor(){
|
||||||
super();
|
super();
|
||||||
this.get("/", async (ctx,next)=>{
|
this.get("/", async (ctx,next)=>{
|
||||||
await renderVideo(ctx,ctx.state.content.path);
|
await renderVideo(ctx,ctx.state.location.path);
|
||||||
});
|
});
|
||||||
this.get("/thumbnail", async (ctx,next)=>{
|
this.get("/thumbnail", async (ctx,next)=>{
|
||||||
await renderVideo(ctx,ctx.state.content.path);
|
await renderVideo(ctx,ctx.state.location.path);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import {Watcher} from './diff/diff'
|
|||||||
|
|
||||||
import { createReadStream, readFileSync } from 'fs';
|
import { createReadStream, readFileSync } from 'fs';
|
||||||
import getContentRouter from './route/contents';
|
import getContentRouter from './route/contents';
|
||||||
import { createKnexContentsAccessor } from './db/contents';
|
import { createKnexContentsAccessor } from './db/doc';
|
||||||
import bodyparser from 'koa-bodyparser';
|
import bodyparser from 'koa-bodyparser';
|
||||||
import {error_handler} from './route/error_handler';
|
import {error_handler} from './route/error_handler';
|
||||||
|
|
||||||
@ -64,8 +64,8 @@ export async function create_server(){
|
|||||||
static_file_server('dist/js/bundle.js.map','text');
|
static_file_server('dist/js/bundle.js.map','text');
|
||||||
|
|
||||||
const content_router = getContentRouter(createKnexContentsAccessor(db));
|
const content_router = getContentRouter(createKnexContentsAccessor(db));
|
||||||
router.use('/content',content_router.routes());
|
router.use('/api/doc',content_router.routes());
|
||||||
router.use('/content',content_router.allowedMethods());
|
router.use('/api/doc',content_router.allowedMethods());
|
||||||
|
|
||||||
router.post('/user/login',createLoginMiddleware(db));
|
router.post('/user/login',createLoginMiddleware(db));
|
||||||
router.post('/user/logout',LogoutMiddleware);
|
router.post('/user/logout',LogoutMiddleware);
|
||||||
|
6
src/types/db.d.ts
vendored
6
src/types/db.d.ts
vendored
@ -11,7 +11,7 @@ declare module "knex" {
|
|||||||
password_hash: string;
|
password_hash: string;
|
||||||
password_salt: string;
|
password_salt: string;
|
||||||
};
|
};
|
||||||
contents: {
|
document: {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
@ -20,8 +20,8 @@ declare module "knex" {
|
|||||||
content_hash?: string;
|
content_hash?: string;
|
||||||
additional?: string;
|
additional?: string;
|
||||||
};
|
};
|
||||||
content_tag_relation: {
|
doc_tag_relation: {
|
||||||
content_id: number;
|
doc_id: number;
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
};
|
};
|
||||||
permissions: {
|
permissions: {
|
||||||
|
@ -24,7 +24,7 @@ module.exports = ()=>{return {
|
|||||||
},
|
},
|
||||||
plugins : [
|
plugins : [
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
"filename":'../css/style.css'})
|
"filename":'../css/style.css'}),
|
||||||
],
|
],
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
resolve: {
|
resolve: {
|
||||||
|
Loading…
Reference in New Issue
Block a user