Former-commit-id: 54ad40f5a938dd5e86a955588f76738cd2679106
This commit is contained in:
troyeguo
2020-10-19 21:33:30 +08:00
parent 5112c8d7c8
commit 430ceef652
31 changed files with 365 additions and 48 deletions

156
main.js
View File

@@ -18,6 +18,7 @@ app.on("ready", () => {
nodeIntegration: true,
nativeWindowOpen: true,
nodeIntegrationInSubFrames: true,
allowRunningInsecureContent: true,
},
show: false,
// transparent: true,
@@ -71,14 +72,165 @@ app.on("ready", () => {
.getFonts()
.then((fonts) => {
event.returnValue = fonts;
const server = require("./server");
})
.catch((err) => {
console.log(err);
});
});
let isFirst = true;
ipcMain.on("start-server", (event, arg) => {
if (isFirst) startExpress();
isFirst = false;
event.returnValue = "pong";
});
});
app.on("window-all-closed", () => {
app.quit();
});
function startExpress() {
const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
const fileUpload = require("express-fileupload");
const path = require("path");
const fs = require("fs");
const Epub = require("epub-gen");
const { readFileSync } = require("fs");
const iconv = require("iconv-lite");
const electron = require("electron");
const configDir = (electron.app || electron.remote.app).getPath("userData");
var dirPath = path.join(configDir, "uploads");
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
console.log("文件夹创建成功");
} else {
console.log("文件夹已存在");
}
const server = express();
server.use(
fileUpload({
createParentPath: true,
})
);
server.use(cors());
server.use(bodyParser.json());
server.use(bodyParser.urlencoded({ extended: true }));
server.post("/ebook_parser", async (req, res) => {
let file = req.files.file;
file.mv(dirPath + file.name, () => {
const data = readFileSync(dirPath + file.name, {
encoding: "binary",
});
const buf = new Buffer(data, "binary");
const lines = iconv.decode(buf, "GBK").split("\n");
const content = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// console.log(line, line.startsWith("序章"), "test");
if (
line.startsWith("CHAPTER ") ||
line.startsWith("Chapter") ||
line.startsWith("第") ||
line.startsWith("序章") ||
line.startsWith("前言") ||
line.startsWith("写在前面的话") ||
line.startsWith("后记") ||
line.startsWith("楔子") ||
line.startsWith("后记") ||
line.startsWith("后序")
) {
if (content.length) {
content[content.length - 1].data = content[
content.length - 1
].data.join("\n");
}
content.push({
data: [],
});
} else if (line.trim() === "" && content.length) {
if (content[content.length - 1].data.length > 1) {
content[content.length - 1].data.push("</p>");
}
content[content.length - 1].data.push("<p style='text-indent:2em'>");
} else if (content.length) {
content[content.length - 1].data.push(line.trim());
}
if (
content[content.length - 1] &&
content[content.length - 1].data &&
i === lines.length - 1
) {
content[content.length - 1].data = content[
content.length - 1
].data.join("\n");
}
}
if (!content.length) {
content.push({
title: "正文",
data: lines
.map((item) => {
return `<p>${item}</p>`;
})
.join("\n"),
});
}
const options = {
title: file.name.split(".")[0],
author: "Koodo Reader",
output: dirPath + `${file.name.split(".")[0]}.epub`,
content,
};
new Epub(options).promise
.then(() => {
res.sendFile(dirPath + `${file.name.split(".")[0]}.epub`);
res.on("finish", function () {
try {
fs.unlink(dirPath + `${file.name.split(".")[0]}.epub`, (err) => {
if (err) throw err;
console.log("successfully deleted");
});
fs.unlink(dirPath + `${file.name}`, (err) => {
if (err) throw err;
console.log("successfully deleted");
});
} catch (e) {
console.log("error removing ");
}
});
})
.catch((err) => console.log("err"));
});
});
async function start() {
try {
const port = 3366;
expressServer = await server.listen(port);
console.log("started");
const address = expressServer.address();
serverInfo = {
port: address.port,
local: "localhost",
url: `http://localhost:${address.port}`,
};
return serverInfo;
} catch (e) {
return { message: e.message };
}
}
async function startServer() {
console.log("starting");
const { port, local, message } = await start();
if (message) {
console.log("err");
console.error(message);
} else {
console.info(`启动成功,本地访问 http://${local}:${port}`);
}
}
startServer();
}

View File

@@ -105,7 +105,11 @@ class BookCardItem extends React.Component<BookProps, BookState> {
>
<div className="book-item-cover-img">
<img
src={`${window.location.href.split("#")[0]}assets/cover.svg`}
src={
process.env.NODE_ENV === "production"
? "./assets/cover.svg"
: "../../assets/cover.svg"
}
alt=""
style={{ width: "80%" }}
/>

View File

@@ -76,7 +76,11 @@ class BookListItem extends React.Component<BookItemProps, BookItemState> {
}}
>
<img
src={`${window.location.href.split("#")[0]}assets/cover.svg`}
src={
process.env.NODE_ENV === "production"
? "./assets/cover.svg"
: "../../assets/cover.svg"
}
alt=""
style={{ width: "80%" }}
/>

View File

@@ -13,11 +13,18 @@ import { config } from "../../constants/readerConfig";
import MobiFile from "../../utils/mobiUtil";
import iconv from "iconv-lite";
import isElectron from "is-electron";
import { withRouter } from "react-router-dom";
declare var window: any;
var pdfjsLib = window["pdfjs-dist/build/pdf"];
class ImportLocal extends React.Component<ImportLocalProps, ImportLocalState> {
componentDidMount() {
if (isElectron()) {
const { ipcRenderer } = window.require("electron");
ipcRenderer.sendSync("start-server", "ping");
}
}
handleAddBook = (book: BookModel) => {
return new Promise((resolve, reject) => {
let bookArr = this.props.books;
@@ -33,6 +40,7 @@ class ImportLocal extends React.Component<ImportLocalProps, ImportLocalState> {
this.props.handleMessage("Add Successfully");
this.props.handleMessageBox(true);
resolve();
this.props.history.push("/manager/home");
})
.catch(() => {
reject();
@@ -349,4 +357,4 @@ class ImportLocal extends React.Component<ImportLocalProps, ImportLocalState> {
}
}
export default ImportLocal;
export default withRouter(ImportLocal);

View File

@@ -1,6 +1,6 @@
import BookModel from "../../model/Book";
export interface ImportLocalProps {
import { RouteComponentProps } from "react-router";
export interface ImportLocalProps extends RouteComponentProps<any> {
books: BookModel[];
handleMessageBox: (isShow: boolean) => void;
handleMessage: (message: string) => void;

View File

@@ -271,7 +271,11 @@ class SettingDialog extends React.Component<
</div>
<img
src={`${window.location.href.split("#")[0]}assets/empty.svg`}
src={
process.env.NODE_ENV === "production"
? "./assets/empty.svg"
: "../../assets/empty.svg"
}
alt=""
className="setting-dialog-illustration"
/>

View File

@@ -66,7 +66,11 @@ class UpdateDialog extends React.Component<UpdateInfoProps, UpdateInfoState> {
koodo.960960.xyz
</p>
<img
src={`${window.location.href.split("#")[0]}assets/empty.svg`}
src={
process.env.NODE_ENV === "production"
? "./assets/empty.svg"
: "../../assets/empty.svg"
}
alt=""
className="update-dialog-illustration"
/>

View File

@@ -79,7 +79,7 @@ export const sideMenu = [
export const config = {
callback_url:
process.env.NODE_ENV === "production"
? "https://reader.960960.xyz"
? "https://koodo.960960.xyz"
: "http://localhost:3000",
token_url:
process.env.NODE_ENV === "production"

View File

@@ -14,7 +14,7 @@ import OtherUtil from "../../utils/otherUtil";
import localforage from "localforage";
import DeletePopup from "../../components/deletePopup";
import EmptyPage from "../emptyPage";
import { Redirect } from "react-router-dom";
import { Redirect, withRouter } from "react-router-dom";
declare var window: any;
@@ -28,6 +28,9 @@ class BookList extends React.Component<BookListProps, BookListState> {
};
}
componentDidMount() {
if (!this.props.books || !this.props.books[0]) {
return <Redirect to="manager/empty" />;
}
this.handleOldVersion();
}
handleOldVersion = async () => {
@@ -146,11 +149,10 @@ class BookList extends React.Component<BookListProps, BookListState> {
SortUtil.sortBooks(this.props.books, this.props.sortCode) || []
)
: this.handleRecent(this.props.books, RecordRecent.getAllRecent());
console.log(books.length, "books.length");
if (books.length === 0) {
console.log("empty");
if (this.props.mode === "shelf" && books.length === 0) {
return <EmptyPage />;
}
return books.map((item: BookModel, index: number) => {
return this.props.isList === "list" ? (
<BookItem
@@ -210,7 +212,11 @@ class BookList extends React.Component<BookListProps, BookListState> {
this.setState({ isOpenDelete });
};
render() {
if (this.state.favoriteBooks === 0 && this.props.mode === "favorite") {
if (
(this.state.favoriteBooks === 0 && this.props.mode === "favorite") ||
!this.props.books ||
!this.props.books[0]
) {
return <Redirect to="/manager/empty" />;
}
const deletePopupProps = {
@@ -285,4 +291,4 @@ class BookList extends React.Component<BookListProps, BookListState> {
}
}
export default BookList;
export default withRouter(BookList);

View File

@@ -1,6 +1,6 @@
import BookModel from "../../model/Book";
export interface BookListProps {
import { RouteComponentProps } from "react-router";
export interface BookListProps extends RouteComponentProps<any> {
books: BookModel[];
mode: string;
shelfIndex: number;

View File

@@ -8,6 +8,7 @@ import RecordLocation from "../../utils/recordLocation";
import AddFavorite from "../../utils/addFavorite";
import { Trans } from "react-i18next";
import { DeleteDialogProps } from "./interface";
import { withRouter } from "react-router-dom";
class DeleteDialog extends React.Component<DeleteDialogProps> {
handleCancel = () => {
@@ -74,6 +75,9 @@ class DeleteDialog extends React.Component<DeleteDialogProps> {
//删除书签,笔记,书摘,高亮
this.handleDeleteOther();
this.props.handleActionDialog(false);
if (this.props.books.length === 1) {
this.props.history.push("/manager/empty");
}
}
this.props.handleMessage("Delete Successfully");
@@ -130,4 +134,4 @@ class DeleteDialog extends React.Component<DeleteDialogProps> {
}
}
export default DeleteDialog;
export default withRouter(DeleteDialog);

View File

@@ -1,8 +1,9 @@
import BookModel from "../../model/Book";
import NoteModel from "../../model/Note";
import BookmarkModel from "../../model/Bookmark";
import { RouteComponentProps } from "react-router";
export interface DeleteDialogProps {
export interface DeleteDialogProps extends RouteComponentProps<any> {
books: BookModel[];
isOpenDeleteDialog: boolean;
currentBook: BookModel;

View File

@@ -13,6 +13,9 @@ class DigestList extends React.Component<DigestListProps, DigestListStates> {
tag: [],
};
}
componentWillMount() {
this.props.handleFetchNotes();
}
handleFilter = (items: any, arr: number[]) => {
let itemArr: any[] = [];
arr.forEach((item) => {

View File

@@ -3,6 +3,7 @@ import { connect } from "react-redux";
import { stateType } from "../../store";
import { withNamespaces } from "react-i18next";
import DigestList from "./component";
import { handleFetchNotes } from "../../store/actions/reader";
const mapStateToProps = (state: stateType) => {
return {
@@ -11,7 +12,7 @@ const mapStateToProps = (state: stateType) => {
searchResults: state.manager.searchResults,
};
};
const actionCreator = {};
const actionCreator = { handleFetchNotes };
export default connect(
mapStateToProps,
actionCreator

View File

@@ -4,6 +4,7 @@ export interface DigestListProps {
digests: NoteModel[];
isSearch: boolean;
searchResults: number[];
handleFetchNotes: () => void;
}
export interface DigestListStates {
tag: string[];

View File

@@ -7,7 +7,6 @@ import { EmptyPageProps, EmptyPageState } from "./interface";
class EmptyPage extends React.Component<EmptyPageProps, EmptyPageState> {
render() {
console.log("empty");
const renderEmptyList = () => {
return emptyList.map((item) => {
return (
@@ -40,7 +39,7 @@ class EmptyPage extends React.Component<EmptyPageProps, EmptyPageState> {
<img
src={
process.env.NODE_ENV === "production"
? `${window.location.href.split("#")[0]}assets/empty.svg`
? "./assets/empty.svg"
: "../../assets/empty.svg"
}
alt=""

View File

@@ -51,7 +51,6 @@ class Reader extends React.Component<ReaderProps, ReaderState> {
}
}
componentDidMount() {
console.log(window.location.href, "rendered");
let page = document.querySelector("#page-area");
let epub = this.props.currentEpub;
(window as any).rangy.init(); // 初始化

View File

@@ -58,11 +58,7 @@ class Header extends React.Component<HeaderProps, HeaderState> {
></span>
</div>
<a
href={`${window.location.href.split("#")[0]}assets/demo.epub`}
target="_blank"
rel="noopener noreferrer"
>
<a href="./assets/demo.epub" target="_blank" rel="noopener noreferrer">
<div
className="download-demo-book"
style={this.state.isBookImported ? { display: "none" } : {}}

View File

@@ -13,6 +13,9 @@ class NoteList extends React.Component<NoteListProps, NoteListState> {
tag: [],
};
}
componentWillMount() {
this.props.handleFetchNotes();
}
handleTag = (tag: string[]) => {
this.setState({ tag });
};

View File

@@ -3,6 +3,8 @@ import { connect } from "react-redux";
import { stateType } from "../../store";
import { withNamespaces } from "react-i18next";
import NoteList from "./component";
import { handleFetchNotes } from "../../store/actions/reader";
const mapStateToProps = (state: stateType) => {
return {
@@ -11,7 +13,7 @@ const mapStateToProps = (state: stateType) => {
searchResults: state.manager.searchResults,
};
};
const actionCreator = {};
const actionCreator = { handleFetchNotes };
export default connect(
mapStateToProps,
actionCreator

View File

@@ -4,6 +4,7 @@ export interface NoteListProps {
notes: NoteModel[];
isSearch: boolean;
searchResults: number[];
handleFetchNotes: () => void;
}
export interface NoteListState {
tag: string[];

View File

@@ -22,6 +22,7 @@ class Sidebar extends React.Component<SidebarProps, SidebarState> {
this.setState({ isCollapse: true });
this.props.history.push(`/manager/${mode}`);
this.props.handleMode(mode);
this.props.handleSearch(false);
};
render() {
const renderSideMenu = () => {
@@ -67,7 +68,7 @@ class Sidebar extends React.Component<SidebarProps, SidebarState> {
<img
src={
process.env.NODE_ENV === "production"
? `${window.location.href.split("#")[0]}assets/logo.png`
? "./assets/logo.png"
: "../../assets/logo.png"
}
alt=""

View File

@@ -3,11 +3,12 @@ import { connect } from "react-redux";
import { stateType } from "../../store";
import { withNamespaces } from "react-i18next";
import Sidebar from "./component";
import { handleSearch } from "../../store/actions/manager";
const mapStateToProps = (state: stateType) => {
return { mode: state.sidebar.mode };
};
const actionCreator = { handleMode };
const actionCreator = { handleMode, handleSearch };
export default connect(
mapStateToProps,

View File

@@ -3,6 +3,7 @@ import { RouteComponentProps } from "react-router";
export interface SidebarProps extends RouteComponentProps<any> {
mode: string;
handleMode: (mode: string) => void;
handleSearch: (isSearch: boolean) => void;
}
export interface SidebarState {

View File

@@ -154,7 +154,11 @@ class Manager extends React.Component<ManagerProps, ManagerState> {
</div>
<div>
<img
src={`${window.location.href.split("#")[0]}assets/empty.svg`}
src={
process.env.NODE_ENV === "production"
? "./assets/empty.svg"
: "../../assets/empty.svg"
}
alt=""
className="waring-pic"
/>
@@ -183,7 +187,7 @@ class Manager extends React.Component<ManagerProps, ManagerState> {
) : null}
{this.state.isUpdated ? <UpdateDialog {...updateDialogProps} /> : null}
{this.props.isSettingOpen ? <SettingDialog /> : null}
{!books ? (
{!books && this.state.totalBooks ? (
<Redirect to="/manager/loading" />
) : (
<Switch>
@@ -200,5 +204,4 @@ class Manager extends React.Component<ManagerProps, ManagerState> {
);
}
}
export default Manager;
export default Manager;

View File

@@ -0,0 +1,85 @@
import React from "react";
import "./manager.css";
import { RedirectProps, RedirectState } from "./interface";
import { Trans } from "react-i18next";
import { getParamsFromUrl } from "../../utils/syncUtils/common";
import copy from "copy-text-to-clipboard";
import { withRouter } from "react-router-dom";
class Redirect extends React.Component<RedirectProps, RedirectState> {
timer!: NodeJS.Timeout;
constructor(props: RedirectProps) {
super(props);
this.state = {
isAuthed: false,
isError: false,
isCopied: false,
token: "",
};
}
componentDidMount() {
//判断是否是获取token后的回调页面
let url = document.location.href;
if (url.indexOf("error") > -1) {
this.setState({ isError: true });
return false;
}
if (url.indexOf("code") > -1) {
let params: any = getParamsFromUrl();
console.log(params, "params");
this.setState({ token: params.code });
this.setState({ isAuthed: true });
return false;
}
if (url.indexOf("access_token") > -1) {
let params: any = getParamsFromUrl();
console.log(params, "params");
this.setState({ token: params.access_token });
this.setState({ isAuthed: true });
return false;
}
}
render() {
if (this.state.isError || this.state.isAuthed) {
return (
<div className="backup-page-finish-container">
<div className="backup-page-finish">
{this.state.isAuthed ? (
<span className="icon-message backup-page-finish-icon"></span>
) : (
<span className="icon-close auth-page-close-icon"></span>
)}
<div className="backup-page-finish-text">
<Trans>
{this.state.isAuthed
? "Authorize Successfully"
: "Authorize Failed"}
</Trans>
</div>
{this.state.isAuthed ? (
<div
className="token-dialog-token-text"
onClick={() => {
copy(this.state.token);
this.setState({ isCopied: true });
}}
>
{this.state.isCopied ? (
<Trans>Copied</Trans>
) : (
<Trans>Copy Token</Trans>
)}
</div>
) : null}
</div>
</div>
);
}
return <div className="manager"></div>;
}
}
export default withRouter(Redirect);

View File

@@ -0,0 +1,9 @@
import { connect } from "react-redux";
import "./manager.css";
import { stateType } from "../../store";
import Redirect from "./component";
const mapStateToProps = (state: stateType) => {
return {};
};
const actionCreator = {};
export default connect(mapStateToProps, actionCreator)(Redirect);

View File

@@ -0,0 +1,9 @@
import { RouteComponentProps } from "react-router";
export interface RedirectProps extends RouteComponentProps<any> {}
export interface RedirectState {
isAuthed: boolean;
isError: boolean;
isCopied: boolean;
token: string;
}

View File

@@ -0,0 +1,22 @@
.manager {
width: 100%;
height: 100%;
}
.token-dialog-token-text {
width: 94px;
height: 29px;
background: rgba(255, 255, 255, 1);
border: 2px solid rgba(45, 111, 200, 1);
opacity: 1;
font-size: 14px;
font-weight: 500;
line-height: 29px;
color: rgba(45, 111, 200, 1);
text-align: center;
cursor: pointer;
}
.auth-page-close-icon {
font-size: 60px !important;
float: none !important;
}

View File

@@ -1,24 +1,18 @@
import { hot } from "react-hot-loader/root";
import React from "react";
import {
Route,
Switch,
Redirect,
HashRouter,
BrowserRouter,
} from "react-router-dom";
import { Route, Switch, HashRouter } from "react-router-dom";
import Manager from "../pages/manager";
import EpubReader from "../pages/epubReader";
import _Redirect from "../pages/redirect";
const Router = () => {
return (
<HashRouter>
<Switch>
<Route component={Manager} path="/manager" />
<Route component={Manager} exact path="/" />
<Route component={EpubReader} path="/epub" />
{/* <Route component={_Redirect} path="/redirect" />
<Redirect to="/redirect" /> */}
<Route component={Manager} exact path="/" />
<Route component={_Redirect} path="/" />
</Switch>
</HashRouter>
);

View File

@@ -3,8 +3,8 @@ export function getParamsFromUrl() {
var e,
r = /([^&;=]+)=?([^&;]*)/g,
q =
window.location.search.substring(1).split("#")[0] ||
window.location.hash.substring(1);
window.location.hash.substring(1) ||
window.location.search.substring(1).split("#")[0];
while ((e = r.exec(q))) {
hashParams[e[1]] = decodeURIComponent(e[2]);