Former-commit-id: 924785aa776d93c4e0235e57ba1de960ee6d2633
This commit is contained in:
troyeguo
2021-06-13 17:14:13 +08:00
parent 98c519e505
commit 10ebd4f5ab
37 changed files with 1291 additions and 823 deletions

View File

@@ -0,0 +1,344 @@
import React from "react";
import ViewArea from "../epubViewer";
import Background from "../background";
import SettingPanel from "../panels/settingPanel";
import NavigationPanel from "../panels/navigationPanel";
import OperationPanel from "../panels/operationPanel";
import MessageBox from "../messageBox";
import ProgressPanel from "../panels/progressPanel";
import { ReaderProps, ReaderState } from "./interface";
import { MouseEvent } from "../../utils/mouseEvent";
import OtherUtil from "../../utils/otherUtil";
import ReadingTime from "../../utils/readUtils/readingTime";
class Reader extends React.Component<ReaderProps, ReaderState> {
messageTimer!: NodeJS.Timeout;
tickTimer!: NodeJS.Timeout;
rendition: any;
constructor(props: ReaderProps) {
super(props);
this.state = {
isOpenSettingPanel:
OtherUtil.getReaderConfig("isSettingLocked") === "yes" ? true : false,
isOpenOperationPanel: false,
isOpenProgressPanel: false,
isOpenNavPanel:
OtherUtil.getReaderConfig("isNavLocked") === "yes" ? true : false,
isMessage: false,
rendition: null,
scale: OtherUtil.getReaderConfig("scale") || 1,
margin: parseInt(OtherUtil.getReaderConfig("margin")) || 30,
time: ReadingTime.getTime(this.props.currentBook.key),
isTouch: OtherUtil.getReaderConfig("isTouch") === "yes",
readerMode: OtherUtil.getReaderConfig("readerMode") || "double",
};
}
componentWillMount() {
this.props.handleFetchBookmarks();
this.props.handleFetchPercentage(this.props.currentBook);
this.props.handleFetchNotes();
this.props.handleFetchBooks();
this.props.handleFetchChapters(this.props.currentEpub);
}
UNSAFE_componentWillReceiveProps(nextProps: ReaderProps) {
this.setState({
isMessage: nextProps.isMessage,
});
//控制消息提示两秒之后消失
if (nextProps.isMessage) {
this.messageTimer = setTimeout(() => {
this.props.handleMessageBox(false);
this.setState({ isMessage: false });
}, 2000);
}
}
componentDidMount() {
this.handleRenderBook();
window.addEventListener("resize", () => {
this.handleRenderBook();
});
}
handleRenderBook = () => {
let page = document.querySelector("#page-area");
let epub = this.props.currentEpub;
(window as any).rangy.init(); // 初始化
this.rendition = epub.renderTo(page, {
manager:
this.state.readerMode === "continuous" ? "continuous" : "default",
flow: this.state.readerMode === "continuous" ? "scrolled" : "auto",
width: "100%",
height: "100%",
snap: true,
spread:
OtherUtil.getReaderConfig("readerMode") === "single" ? "none" : "",
});
this.setState({ rendition: this.rendition });
this.state.readerMode !== "continuous" && MouseEvent(this.rendition); // 绑定事件
this.tickTimer = setInterval(() => {
let time = this.state.time;
time += 1;
this.setState({ time });
}, 1000);
};
//进入阅读器
handleEnterReader = (position: string) => {
//控制上下左右的菜单的显示
switch (position) {
case "right":
this.setState({
isOpenSettingPanel: this.state.isOpenSettingPanel ? false : true,
});
break;
case "left":
this.setState({
isOpenNavPanel: this.state.isOpenNavPanel ? false : true,
});
break;
case "top":
this.setState({
isOpenOperationPanel: this.state.isOpenOperationPanel ? false : true,
});
break;
case "bottom":
this.setState({
isOpenProgressPanel: this.state.isOpenProgressPanel ? false : true,
});
break;
default:
break;
}
};
//退出阅读器
handleLeaveReader = (position: string) => {
//控制上下左右的菜单的显示
switch (position) {
case "right":
if (OtherUtil.getReaderConfig("isSettingLocked") === "yes") {
break;
} else {
this.setState({ isOpenSettingPanel: false });
break;
}
case "left":
if (OtherUtil.getReaderConfig("isNavLocked") === "yes") {
break;
} else {
this.setState({ isOpenNavPanel: false });
break;
}
case "top":
this.setState({ isOpenOperationPanel: false });
break;
case "bottom":
this.setState({ isOpenProgressPanel: false });
break;
default:
break;
}
};
nextPage = () => {
this.state.rendition.next();
};
prevPage = () => {
this.state.rendition.prev();
};
render() {
const renditionProps = {
rendition: this.state.rendition,
handleLeaveReader: this.handleLeaveReader,
handleEnterReader: this.handleEnterReader,
isShow:
this.state.isOpenNavPanel ||
this.state.isOpenOperationPanel ||
this.state.isOpenProgressPanel ||
this.state.isOpenSettingPanel,
};
return (
<div
className="viewer"
style={{
filter: `brightness(${
OtherUtil.getReaderConfig("brightness") || 1
}) invert(${
OtherUtil.getReaderConfig("isInvert") === "yes" ? 1 : 0
})`,
}}
>
<div
className="previous-chapter-single-container"
onClick={() => {
this.prevPage();
}}
>
<span className="icon-dropdown previous-chapter-single"></span>
</div>
<div
className="next-chapter-single-container"
onClick={() => {
this.nextPage();
}}
>
<span className="icon-dropdown next-chapter-single"></span>
</div>
<div
className="reader-setting-icon-container"
onClick={() => {
this.handleEnterReader("left");
this.handleEnterReader("right");
this.handleEnterReader("bottom");
this.handleEnterReader("top");
}}
>
<span className="icon-grid reader-setting-icon"></span>
</div>
{this.state.isMessage ? <MessageBox /> : null}
<div
className="left-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenNavPanel) {
return;
}
this.handleEnterReader("left");
}}
onClick={() => {
this.handleEnterReader("left");
}}
></div>
<div
className="right-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenSettingPanel) {
return;
}
this.handleEnterReader("right");
}}
onClick={() => {
this.handleEnterReader("right");
}}
></div>
<div
className="top-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenOperationPanel) {
return;
}
this.handleEnterReader("top");
}}
onClick={() => {
this.handleEnterReader("top");
}}
></div>
<div
className="bottom-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenProgressPanel) {
return;
}
this.handleEnterReader("bottom");
}}
onClick={() => {
this.handleEnterReader("bottom");
}}
></div>
{this.state.rendition && this.props.currentEpub.rendition && (
<ViewArea {...renditionProps} />
)}
<div
className="setting-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("right");
}}
style={
this.state.isOpenSettingPanel
? {}
: {
transform: "translateX(309px)",
}
}
>
<SettingPanel />
</div>
<div
className="navigation-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("left");
}}
style={
this.state.isOpenNavPanel
? {}
: {
transform: "translateX(-309px)",
}
}
>
<NavigationPanel {...{ time: this.state.time }} />
</div>
<div
className="progress-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("bottom");
}}
style={
this.state.isOpenProgressPanel
? {}
: {
transform: "translateY(110px)",
}
}
>
<ProgressPanel {...{ time: this.state.time }} />
</div>
<div
className="operation-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("top");
}}
style={
this.state.isOpenOperationPanel
? {}
: {
transform: "translateY(-110px)",
}
}
>
<OperationPanel {...{ time: this.state.time }} />
</div>
<div
className="view-area-page"
id="page-area"
style={
document.body.clientWidth < 570
? { left: 0, right: 0 }
: this.state.readerMode === "continuous"
? {
left: `calc(50vw - ${270 * parseFloat(this.state.scale)}px)`,
right: `calc(50vw - ${270 * parseFloat(this.state.scale)}px)`,
top: "75px",
bottom: "75px",
}
: this.state.readerMode === "single"
? {
left: `calc(50vw - ${270 * parseFloat(this.state.scale)}px)`,
right: `calc(50vw - ${270 * parseFloat(this.state.scale)}px)`,
}
: this.state.readerMode === "double"
? {
left: this.state.margin - 40 + "px",
right: this.state.margin - 40 + "px",
}
: {}
}
></div>
<Background {...{ time: this.state.time }} />
</div>
);
}
}
export default Reader;

View File

@@ -67,7 +67,7 @@
height: 200px;
position: absolute;
left: 0px;
top: calc(50% - 100px);
top: calc(50vh - 100px);
background-color: pink;
z-index: 10;
opacity: 0;
@@ -77,7 +77,7 @@
height: 200px;
position: absolute;
right: 0px;
top: calc(50% - 100px);
top: calc(50vh - 100px);
background-color: pink;
z-index: 10;
opacity: 0;
@@ -86,7 +86,7 @@
width: 200px;
height: 50px;
position: absolute;
right: calc(50% - 100px);
right: calc(50vw - 100px);
top: 0px;
background-color: pink;
z-index: 10;
@@ -96,8 +96,8 @@
width: 200px;
height: 50px;
position: absolute;
right: calc(50% - 100px);
bottom: 0px;
right: calc(50vw - 100px);
top: calc(100vh - 50px);
background-color: pink;
z-index: 10;
opacity: 0;
@@ -107,7 +107,7 @@
height: 60px;
position: absolute;
top: 0px;
left: calc(50% - 206px);
left: calc(50vw - 206px);
z-index: 15;
transition: transform 0.5s ease;
@@ -116,8 +116,8 @@
width: 412px;
height: 60px;
position: absolute;
bottom: 0px;
left: calc(50% - 206px);
top: calc(100vh - 60px);
left: calc(50vw - 206px);
z-index: 15;
transition: transform 0.5s ease;
}
@@ -125,7 +125,7 @@
width: 299px;
height: 100vh;
position: absolute;
bottom: 0px;
top: 0px;
right: 0px;
transition: transform 0.5s ease;
z-index: 15;
@@ -134,7 +134,7 @@
width: 299px;
height: 100vh;
position: absolute;
bottom: 0px;
top: 0px;
left: 0px;
transition: transform 0.5s ease;
z-index: 15;

View File

@@ -0,0 +1,31 @@
import {
handleFetchNotes,
handleFetchBookmarks,
handleFetchChapters,
} from "../../store/actions/reader";
import { handleFetchPercentage } from "../../store/actions/progressPanel";
import {
handleMessageBox,
handleFetchBooks,
} from "../../store/actions/manager";
import "./epubViewer.css";
import { connect } from "react-redux";
import { stateType } from "../../store";
import Reader from "./component";
const mapStateToProps = (state: stateType) => {
return {
currentEpub: state.book.currentEpub,
currentBook: state.book.currentBook,
isMessage: state.manager.isMessage,
};
};
const actionCreator = {
handleFetchNotes,
handleFetchBookmarks,
handleFetchChapters,
handleMessageBox,
handleFetchPercentage,
handleFetchBooks,
};
export default connect(mapStateToProps, actionCreator)(Reader);

View File

@@ -0,0 +1,26 @@
import BookModel from "../../model/Book";
export interface ReaderProps {
currentEpub: any;
currentBook: BookModel;
isMessage: boolean;
handleFetchNotes: () => void;
handleFetchBooks: () => void;
handleFetchBookmarks: () => void;
handleMessageBox: (isShow: boolean) => void;
handleFetchPercentage: (currentBook: BookModel) => void;
handleFetchChapters: (currentEpub: any) => void;
}
export interface ReaderState {
isOpenSettingPanel: boolean;
isOpenOperationPanel: boolean;
isOpenProgressPanel: boolean;
isOpenNavPanel: boolean;
isMessage: boolean;
isTouch: boolean;
readerMode: string;
rendition: any;
time: number;
scale: string;
margin: number;
}

View File

@@ -1,345 +1,126 @@
//阅读器图书内容区域
import React from "react";
import ViewArea from "../viewArea";
import Background from "../background";
import SettingPanel from "../panels/settingPanel";
import NavigationPanel from "../panels/navigationPanel";
import OperationPanel from "../panels/operationPanel";
import MessageBox from "../messageBox";
import ProgressPanel from "../panels/progressPanel";
import { ReaderProps, ReaderState } from "./interface";
import { MouseEvent } from "../../utils/mouseEvent";
import "./viewArea.css";
import PopupMenu from "../../components/popups/popupMenu";
import { ViewAreaProps, ViewAreaStates } from "./interface";
import RecordLocation from "../../utils/readUtils/recordLocation";
import OtherUtil from "../../utils/otherUtil";
import ReadingTime from "../../utils/readUtils/readingTime";
import BookmarkModel from "../../model/Bookmark";
import StyleUtil from "../../utils/readUtils/styleUtil";
import ImageViewer from "../../components/imageViewer";
import Lottie from "react-lottie";
import animationSiri from "../../assets/lotties/siri.json";
class Reader extends React.Component<ReaderProps, ReaderState> {
messageTimer!: NodeJS.Timeout;
tickTimer!: NodeJS.Timeout;
rendition: any;
const siriOptions = {
loop: true,
autoplay: true,
animationData: animationSiri,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};
declare var window: any;
constructor(props: ReaderProps) {
class ViewArea extends React.Component<ViewAreaProps, ViewAreaStates> {
isFirst: boolean;
constructor(props: ViewAreaProps) {
super(props);
this.state = {
isOpenSettingPanel:
OtherUtil.getReaderConfig("isSettingLocked") === "yes" ? true : false,
isOpenOperationPanel: false,
isOpenProgressPanel: false,
isOpenNavPanel:
OtherUtil.getReaderConfig("isNavLocked") === "yes" ? true : false,
isMessage: false,
rendition: null,
scale: OtherUtil.getReaderConfig("scale") || 1,
margin: parseInt(OtherUtil.getReaderConfig("margin")) || 30,
time: ReadingTime.getTime(this.props.currentBook.key),
isTouch: OtherUtil.getReaderConfig("isTouch") === "yes",
readerMode: OtherUtil.getReaderConfig("readerMode") || "double",
cfiRange: null,
contents: null,
rect: null,
loading: true,
};
this.isFirst = true;
}
componentWillMount() {
this.props.handleFetchBookmarks();
this.props.handleFetchPercentage(this.props.currentBook);
this.props.handleFetchNotes();
this.props.handleFetchBooks();
this.props.handleFetchChapters(this.props.currentEpub);
}
UNSAFE_componentWillReceiveProps(nextProps: ReaderProps) {
this.setState({
isMessage: nextProps.isMessage,
});
//控制消息提示两秒之后消失
if (nextProps.isMessage) {
this.messageTimer = setTimeout(() => {
this.props.handleMessageBox(false);
this.setState({ isMessage: false });
}, 2000);
}
}
componentDidMount() {
this.handleRenderBook();
window.addEventListener("resize", () => {
this.handleRenderBook();
});
}
handleRenderBook = () => {
let page = document.querySelector("#page-area");
let epub = this.props.currentEpub;
(window as any).rangy.init(); // 初始化
this.rendition = epub.renderTo(page, {
manager:
this.state.readerMode === "continuous" ? "continuous" : "default",
flow: this.state.readerMode === "continuous" ? "scrolled" : "auto",
width: "100%",
height: "100%",
snap: true,
spread:
OtherUtil.getReaderConfig("readerMode") === "single" ? "none" : "",
window.rangy.init(); // 初始化
this.props.rendition.on("locationChanged", () => {
this.props.handleReadingEpub(epub);
this.props.handleOpenMenu(false);
const currentLocation = this.props.rendition.currentLocation();
if (!currentLocation.start) {
return;
}
const cfi = currentLocation.start.cfi;
this.props.handleShowBookmark(
this.props.bookmarks &&
this.props.bookmarks.filter(
(item: BookmarkModel) => item.cfi === cfi
)[0]
? true
: false
);
if (!this.isFirst && this.props.locations) {
let percentage = this.props.locations.percentageFromCfi(cfi);
RecordLocation.recordCfi(this.props.currentBook.key, cfi, percentage);
this.props.handlePercentage(percentage);
} else if (!this.isFirst) {
//如果过暂时没有解析出locations就直接记录cfi
RecordLocation.recordCfi(
this.props.currentBook.key,
cfi,
RecordLocation.getCfi(this.props.currentBook.key).percentage
);
}
this.isFirst = false;
});
this.setState({ rendition: this.rendition });
this.state.readerMode !== "continuous" && MouseEvent(this.rendition); // 绑定事件
this.tickTimer = setInterval(() => {
let time = this.state.time;
time += 1;
this.setState({ time });
}, 1000);
};
this.props.rendition.on("rendered", () => {
this.setState({ loading: false });
let iframe = document.getElementsByTagName("iframe")[0];
if (!iframe) return;
let doc = iframe.contentDocument;
if (!doc) {
return;
}
StyleUtil.addDefaultCss();
this.props.rendition.themes.default(StyleUtil.getCustomCss(false));
});
this.props.rendition.on("selected", (cfiRange: any, contents: any) => {
var range = contents.range(cfiRange);
var rect = range.getBoundingClientRect();
this.setState({ cfiRange, contents, rect });
});
this.props.rendition.themes.default(StyleUtil.getCustomCss(false));
this.props.rendition.display(
RecordLocation.getCfi(this.props.currentBook.key) === null
? null
: RecordLocation.getCfi(this.props.currentBook.key).cfi
);
}
//进入阅读器
handleEnterReader = (position: string) => {
//控制上下左右的菜单的显示
switch (position) {
case "right":
this.setState({
isOpenSettingPanel: this.state.isOpenSettingPanel ? false : true,
});
break;
case "left":
this.setState({
isOpenNavPanel: this.state.isOpenNavPanel ? false : true,
});
break;
case "top":
this.setState({
isOpenOperationPanel: this.state.isOpenOperationPanel ? false : true,
});
break;
case "bottom":
this.setState({
isOpenProgressPanel: this.state.isOpenProgressPanel ? false : true,
});
break;
default:
break;
}
};
//退出阅读器
handleLeaveReader = (position: string) => {
//控制上下左右的菜单的显示
switch (position) {
case "right":
if (OtherUtil.getReaderConfig("isSettingLocked") === "yes") {
break;
} else {
this.setState({ isOpenSettingPanel: false });
break;
}
case "left":
if (OtherUtil.getReaderConfig("isNavLocked") === "yes") {
break;
} else {
this.setState({ isOpenNavPanel: false });
break;
}
case "top":
this.setState({ isOpenOperationPanel: false });
break;
case "bottom":
this.setState({ isOpenProgressPanel: false });
break;
default:
break;
}
};
nextPage = () => {
this.state.rendition.next();
};
prevPage = () => {
this.state.rendition.prev();
};
render() {
const renditionProps = {
rendition: this.state.rendition,
handleLeaveReader: this.handleLeaveReader,
handleEnterReader: this.handleEnterReader,
isShow:
this.state.isOpenNavPanel ||
this.state.isOpenOperationPanel ||
this.state.isOpenProgressPanel ||
this.state.isOpenSettingPanel,
const popupMenuProps = {
rendition: this.props.rendition,
cfiRange: this.state.cfiRange,
contents: this.state.contents,
rect: this.state.rect,
};
return (
<div
className="viewer"
style={{
filter: `brightness(${
OtherUtil.getReaderConfig("brightness") || 1
}) invert(${
OtherUtil.getReaderConfig("isInvert") === "yes" ? 1 : 0
})`,
}}
>
<div
className="previous-chapter-single-container"
onClick={() => {
this.prevPage();
<div className="view-area">
<ImageViewer
{...{
isShow: this.props.isShow,
rendition: this.props.rendition,
handleEnterReader: this.props.handleEnterReader,
handleLeaveReader: this.props.handleLeaveReader,
}}
>
<span className="icon-dropdown previous-chapter-single"></span>
</div>
<div
className="next-chapter-single-container"
onClick={() => {
this.nextPage();
}}
>
<span className="icon-dropdown next-chapter-single"></span>
</div>
<div
className="reader-setting-icon-container"
onClick={() => {
this.handleEnterReader("left");
this.handleEnterReader("right");
this.handleEnterReader("bottom");
this.handleEnterReader("top");
}}
>
<span className="icon-grid reader-setting-icon"></span>
</div>
{this.state.isMessage ? <MessageBox /> : null}
<div
className="left-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenNavPanel) {
return;
}
this.handleEnterReader("left");
}}
onClick={() => {
this.handleEnterReader("left");
}}
></div>
<div
className="right-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenSettingPanel) {
return;
}
this.handleEnterReader("right");
}}
onClick={() => {
this.handleEnterReader("right");
}}
></div>
<div
className="top-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenOperationPanel) {
return;
}
this.handleEnterReader("top");
}}
onClick={() => {
this.handleEnterReader("top");
}}
></div>
<div
className="bottom-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenProgressPanel) {
return;
}
this.handleEnterReader("bottom");
}}
onClick={() => {
this.handleEnterReader("bottom");
}}
></div>
{this.state.rendition && this.props.currentEpub.rendition && (
<ViewArea {...renditionProps} />
)}
<div
className="setting-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("right");
}}
style={
this.state.isOpenSettingPanel
? {}
: {
transform: "translateX(309px)",
}
}
>
<SettingPanel />
</div>
<div
className="navigation-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("left");
}}
style={
this.state.isOpenNavPanel
? {}
: {
transform: "translateX(-309px)",
}
}
>
<NavigationPanel {...{ time: this.state.time }} />
</div>
<div
className="progress-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("bottom");
}}
style={
this.state.isOpenProgressPanel
? {}
: {
transform: "translateY(110px)",
}
}
>
<ProgressPanel {...{ time: this.state.time }} />
</div>
<div
className="operation-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("top");
}}
style={
this.state.isOpenOperationPanel
? {}
: {
transform: "translateY(-110px)",
}
}
>
<OperationPanel {...{ time: this.state.time }} />
</div>
<div
className="view-area-page"
id="page-area"
style={
document.body.clientWidth < 570
? { left: 0, right: 0 }
: this.state.readerMode === "continuous"
? {
left: `calc(50vw - ${270 * parseFloat(this.state.scale)}px)`,
right: `calc(50vw - ${270 * parseFloat(this.state.scale)}px)`,
top: "75px",
bottom: "75px",
}
: this.state.readerMode === "single"
? {
left: `calc(50vw - ${270 * parseFloat(this.state.scale)}px)`,
right: `calc(50vw - ${270 * parseFloat(this.state.scale)}px)`,
}
: this.state.readerMode === "double"
? {
left: this.state.margin - 40 + "px",
right: this.state.margin - 40 + "px",
}
: {}
}
></div>
<Background {...{ time: this.state.time }} />
/>
<PopupMenu {...popupMenuProps} />
{this.state.loading ? (
<div className="spinner">
<Lottie options={siriOptions} height={100} width={300} />
</div>
) : null}
<>
{this.props.isShowBookmark ? <div className="bookmark"></div> : null}
</>
</div>
);
}
}
export default Reader;
export default ViewArea;

View File

@@ -1,31 +1,28 @@
import {
handleFetchNotes,
handleFetchBookmarks,
handleFetchChapters,
} from "../../store/actions/reader";
import { handleFetchPercentage } from "../../store/actions/progressPanel";
import {
handleMessageBox,
handleFetchBooks,
} from "../../store/actions/manager";
import "./epubViewer.css";
import { connect } from "react-redux";
import { stateType } from "../../store";
import Reader from "./component";
import ViewArea from "./component";
import { handlePercentage } from "../../store/actions/progressPanel";
import {
handleOpenMenu,
handleShowBookmark,
} from "../../store/actions/viewArea";
import { handleReadingEpub } from "../../store/actions/book";
const mapStateToProps = (state: stateType) => {
return {
chapters: state.reader.chapters,
currentEpub: state.book.currentEpub,
currentBook: state.book.currentBook,
isMessage: state.manager.isMessage,
locations: state.progressPanel.locations,
bookmarks: state.reader.bookmarks,
isShowBookmark: state.viewArea.isShowBookmark,
};
};
const actionCreator = {
handleFetchNotes,
handleFetchBookmarks,
handleFetchChapters,
handleMessageBox,
handleFetchPercentage,
handleFetchBooks,
handlePercentage,
handleOpenMenu,
handleShowBookmark,
handleReadingEpub,
};
export default connect(mapStateToProps, actionCreator)(Reader);
export default connect(mapStateToProps, actionCreator)(ViewArea);

View File

@@ -1,26 +1,26 @@
import BookModel from "../../model/Book";
export interface ReaderProps {
currentEpub: any;
currentBook: BookModel;
isMessage: boolean;
handleFetchNotes: () => void;
handleFetchBooks: () => void;
handleFetchBookmarks: () => void;
handleMessageBox: (isShow: boolean) => void;
handleFetchPercentage: (currentBook: BookModel) => void;
handleFetchChapters: (currentEpub: any) => void;
}
import BookmarkModel from "../../model/Bookmark";
export interface ReaderState {
isOpenSettingPanel: boolean;
isOpenOperationPanel: boolean;
isOpenProgressPanel: boolean;
isOpenNavPanel: boolean;
isMessage: boolean;
isTouch: boolean;
readerMode: string;
export interface ViewAreaProps {
rendition: any;
time: number;
scale: string;
margin: number;
currentBook: BookModel;
currentEpub: any;
bookmarks: BookmarkModel[];
locations: any;
isShowBookmark: boolean;
chapters: any[];
isShow: boolean;
handleLeaveReader: (position: string) => void;
handleEnterReader: (position: string) => void;
handlePercentage: (percentage: number) => void;
handleOpenMenu: (isOpenMenu: boolean) => void;
handleShowBookmark: (isShowBookmark: boolean) => void;
handleReadingEpub: (epub: object) => void;
}
export interface ViewAreaStates {
loading: boolean;
cfiRange: any;
contents: any;
// rendition: any;
rect: any;
}

View File

@@ -0,0 +1,283 @@
import React from "react";
import SettingPanel from "../panels/settingPanel";
import NavigationPanel from "../panels/navigationPanel";
import OperationPanel from "../panels/operationPanel";
import MessageBox from "../messageBox";
import ProgressPanel from "../panels/progressPanel";
import { ReaderProps, ReaderState } from "./interface";
import OtherUtil from "../../utils/otherUtil";
import ReadingTime from "../../utils/readUtils/readingTime";
import Viewer from "../htmlViewer";
class Reader extends React.Component<ReaderProps, ReaderState> {
messageTimer!: NodeJS.Timeout;
tickTimer!: NodeJS.Timeout;
rendition: any;
constructor(props: ReaderProps) {
super(props);
this.state = {
isOpenSettingPanel:
OtherUtil.getReaderConfig("isSettingLocked") === "yes" ? true : false,
isOpenOperationPanel: false,
isOpenProgressPanel: false,
isOpenNavPanel:
OtherUtil.getReaderConfig("isNavLocked") === "yes" ? true : false,
isMessage: false,
rendition: null,
scale: OtherUtil.getReaderConfig("scale") || 1,
margin: parseInt(OtherUtil.getReaderConfig("margin")) || 30,
time: ReadingTime.getTime(this.props.currentBook.key),
isTouch: OtherUtil.getReaderConfig("isTouch") === "yes",
readerMode: OtherUtil.getReaderConfig("readerMode") || "double",
};
}
componentWillMount() {
// this.props.handleFetchBookmarks();
// this.props.handleFetchPercentage(this.props.currentBook);
// this.props.handleFetchNotes();
// this.props.handleFetchBooks();
// this.props.handleFetchChapters(this.props.currentEpub);
}
UNSAFE_componentWillReceiveProps(nextProps: ReaderProps) {
this.setState({
isMessage: nextProps.isMessage,
});
//控制消息提示两秒之后消失
if (nextProps.isMessage) {
this.messageTimer = setTimeout(() => {
this.props.handleMessageBox(false);
this.setState({ isMessage: false });
}, 2000);
}
}
//进入阅读器
handleEnterReader = (position: string) => {
//控制上下左右的菜单的显示
switch (position) {
case "right":
this.setState({
isOpenSettingPanel: this.state.isOpenSettingPanel ? false : true,
});
break;
case "left":
this.setState({
isOpenNavPanel: this.state.isOpenNavPanel ? false : true,
});
break;
case "top":
this.setState({
isOpenOperationPanel: this.state.isOpenOperationPanel ? false : true,
});
break;
case "bottom":
this.setState({
isOpenProgressPanel: this.state.isOpenProgressPanel ? false : true,
});
break;
default:
break;
}
};
//退出阅读器
handleLeaveReader = (position: string) => {
//控制上下左右的菜单的显示
switch (position) {
case "right":
if (OtherUtil.getReaderConfig("isSettingLocked") === "yes") {
break;
} else {
this.setState({ isOpenSettingPanel: false });
break;
}
case "left":
if (OtherUtil.getReaderConfig("isNavLocked") === "yes") {
break;
} else {
this.setState({ isOpenNavPanel: false });
break;
}
case "top":
this.setState({ isOpenOperationPanel: false });
break;
case "bottom":
this.setState({ isOpenProgressPanel: false });
break;
default:
break;
}
};
nextPage = () => {
this.state.rendition.next();
};
prevPage = () => {
this.state.rendition.prev();
};
render() {
const renditionProps = {
rendition: this.state.rendition,
handleLeaveReader: this.handleLeaveReader,
handleEnterReader: this.handleEnterReader,
isShow:
this.state.isOpenNavPanel ||
this.state.isOpenOperationPanel ||
this.state.isOpenProgressPanel ||
this.state.isOpenSettingPanel,
};
return (
<div
className="viewer"
style={{
filter: `brightness(${
OtherUtil.getReaderConfig("brightness") || 1
}) invert(${
OtherUtil.getReaderConfig("isInvert") === "yes" ? 1 : 0
})`,
}}
>
<div
className="previous-chapter-single-container"
onClick={() => {
this.prevPage();
}}
>
<span className="icon-dropdown previous-chapter-single"></span>
</div>
<div
className="next-chapter-single-container"
onClick={() => {
this.nextPage();
}}
>
<span className="icon-dropdown next-chapter-single"></span>
</div>
<div
className="reader-setting-icon-container"
onClick={() => {
this.handleEnterReader("left");
this.handleEnterReader("right");
this.handleEnterReader("bottom");
this.handleEnterReader("top");
}}
>
<span className="icon-grid reader-setting-icon"></span>
</div>
{this.state.isMessage ? <MessageBox /> : null}
<div
className="left-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenNavPanel) {
return;
}
this.handleEnterReader("left");
}}
onClick={() => {
this.handleEnterReader("left");
}}
></div>
<div
className="right-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenSettingPanel) {
return;
}
this.handleEnterReader("right");
}}
onClick={() => {
this.handleEnterReader("right");
}}
></div>
<div
className="top-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenOperationPanel) {
return;
}
this.handleEnterReader("top");
}}
onClick={() => {
this.handleEnterReader("top");
}}
></div>
<div
className="bottom-panel"
onMouseEnter={() => {
if (this.state.isTouch || this.state.isOpenProgressPanel) {
return;
}
this.handleEnterReader("bottom");
}}
onClick={() => {
this.handleEnterReader("bottom");
}}
></div>
<Viewer {...renditionProps} />
<div
className="setting-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("right");
}}
style={
this.state.isOpenSettingPanel
? {}
: {
transform: "translateX(309px)",
}
}
>
<SettingPanel />
</div>
<div
className="navigation-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("left");
}}
style={
this.state.isOpenNavPanel
? {}
: {
transform: "translateX(-309px)",
}
}
>
<NavigationPanel {...{ time: this.state.time }} />
</div>
<div
className="progress-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("bottom");
}}
style={
this.state.isOpenProgressPanel
? {}
: {
transform: "translateY(110px)",
}
}
>
<ProgressPanel {...{ time: this.state.time }} />
</div>
<div
className="operation-panel-container"
onMouseLeave={(event) => {
this.handleLeaveReader("top");
}}
style={
this.state.isOpenOperationPanel
? {}
: {
transform: "translateY(-110px)",
}
}
>
<OperationPanel {...{ time: this.state.time }} />
</div>
</div>
);
}
}
export default Reader;

View File

@@ -0,0 +1,30 @@
import {
handleFetchNotes,
handleFetchBookmarks,
handleFetchChapters,
} from "../../store/actions/reader";
import { handleFetchPercentage } from "../../store/actions/progressPanel";
import {
handleMessageBox,
handleFetchBooks,
} from "../../store/actions/manager";
import { connect } from "react-redux";
import { stateType } from "../../store";
import Reader from "./component";
const mapStateToProps = (state: stateType) => {
return {
currentEpub: state.book.currentEpub,
currentBook: state.book.currentBook,
isMessage: state.manager.isMessage,
};
};
const actionCreator = {
handleFetchNotes,
handleFetchBookmarks,
handleFetchChapters,
handleMessageBox,
handleFetchPercentage,
handleFetchBooks,
};
export default connect(mapStateToProps, actionCreator)(Reader);

View File

@@ -0,0 +1,26 @@
import BookModel from "../../model/Book";
export interface ReaderProps {
currentEpub: any;
currentBook: BookModel;
isMessage: boolean;
handleFetchNotes: () => void;
handleFetchBooks: () => void;
handleFetchBookmarks: () => void;
handleMessageBox: (isShow: boolean) => void;
handleFetchPercentage: (currentBook: BookModel) => void;
handleFetchChapters: (currentEpub: any) => void;
}
export interface ReaderState {
isOpenSettingPanel: boolean;
isOpenOperationPanel: boolean;
isOpenProgressPanel: boolean;
isOpenNavPanel: boolean;
isMessage: boolean;
isTouch: boolean;
readerMode: string;
rendition: any;
time: number;
scale: string;
margin: number;
}

View File

@@ -8,13 +8,15 @@ import _ from "underscore";
import BookUtil from "../../utils/bookUtil";
import MobiParser from "../../utils/mobiParser";
import marked from "marked";
import "./viewer.css";
import OtherUtil from "../../utils/otherUtil";
import iconv from "iconv-lite";
import chardet from "chardet";
import rtfToHTML from "@iarna/rtf-to-html";
import { xmlBookTagFilter, xmlBookToObj } from "../../utils/xmlUtil";
import HtmlParser from "../../utils/htmlParser";
import OtherUtil from "../../utils/otherUtil";
import RecordLocation from "../../utils/readUtils/recordLocation";
import { mimetype } from "../../constants/mimetype";
import styleUtil from "../../utils/readUtils/styleUtil";
declare var window: any;
@@ -45,29 +47,40 @@ class Viewer extends React.Component<ViewerProps, ViewerState> {
this.handleRtf(result as ArrayBuffer);
} else if (book.format === "DOCX") {
this.handleDocx(result as ArrayBuffer);
} else if (book.format === "FB2") {
this.handleFb2(result as ArrayBuffer);
} else if (
book.format === "HTML" ||
book.format === "XHTML" ||
book.format === "HTM" ||
book.format === "XML"
) {
this.handleHtml(result as ArrayBuffer, book.format);
}
this.props.handleReadingState(true);
RecentBooks.setRecent(key);
});
});
// document.documentElement.style.height = "auto";
// document.documentElement.style.overflow = "auto";
document.documentElement.style.height = "auto";
document.documentElement.style.overflow = "auto";
window.addEventListener("wheel", (event) => {
window.frames[0].document.addEventListener("wheel", (event) => {
RecordLocation.recordScrollHeight(
key,
document.body.clientWidth,
document.body.clientHeight,
document.scrollingElement!.scrollTop
window.frames[0].document.scrollingElement!.scrollTop
);
});
window.frames[0].document.addEventListener("click", (event) => {
this.props.handleLeaveReader("left");
this.props.handleLeaveReader("right");
this.props.handleLeaveReader("top");
this.props.handleLeaveReader("bottom");
});
window.onbeforeunload = () => {
this.handleExit();
};
}
// 点击退出按钮的处理程序
handleExit() {
this.props.handleReadingState(false);
@@ -76,8 +89,9 @@ class Viewer extends React.Component<ViewerProps, ViewerState> {
OtherUtil.setReaderConfig("windowX", window.screenX + "");
OtherUtil.setReaderConfig("windowY", window.screenY + "");
}
handleJump = () => {
document.scrollingElement!.scrollTo(
handleRest = () => {
styleUtil.addHtmlCss();
window.frames[0].document.scrollingElement!.scrollTo(
0,
RecordLocation.getScrollHeight(this.state.key).scroll
);
@@ -85,29 +99,26 @@ class Viewer extends React.Component<ViewerProps, ViewerState> {
handleMobi = async (result: ArrayBuffer) => {
let mobiFile = new MobiParser(result);
let content: any = await mobiFile.render();
let viewer: HTMLElement | null = document.querySelector(".ebook-viewer");
if (!viewer?.innerHTML) return;
viewer.innerHTML = content.outerHTML;
this.handleJump();
window.frames[0].document.body.innerHTML = content.outerHTML;
this.handleRest();
};
handleTxt = (result: ArrayBuffer) => {
let viewer: HTMLElement | null = document.querySelector(".ebook-viewer");
let text = iconv.decode(
Buffer.from(result),
chardet.detect(Buffer.from(result)) as string
);
if (!viewer?.innerText) return;
viewer.innerText = text;
this.handleJump();
window.frames[0].document.body.innerText = text;
this.handleRest();
};
handleMD = (result: ArrayBuffer) => {
var blob = new Blob([result], { type: "text/plain" });
var reader = new FileReader();
reader.onload = (evt) => {
let viewer: HTMLElement | null = document.querySelector(".ebook-viewer");
if (!viewer?.innerHTML) return;
viewer.innerHTML = marked(evt.target?.result as any);
this.handleJump();
window.frames[0].document.body.innerHTML = marked(
evt.target?.result as any
);
this.handleRest();
};
reader.readAsText(blob, "UTF-8");
};
@@ -117,18 +128,26 @@ class Viewer extends React.Component<ViewerProps, ViewerState> {
chardet.detect(Buffer.from(result)) as string
);
rtfToHTML.fromString(text, (err: any, html: any) => {
let viewer: HTMLElement | null = document.querySelector(".ebook-viewer");
if (!viewer?.innerHTML) return;
viewer.innerHTML = html;
this.handleJump();
window.frames[0].document.body.innerHTML = html;
this.handleRest();
});
};
handleDocx = (result: ArrayBuffer) => {
window.mammoth.convertToHtml({ arrayBuffer: result }).then((res: any) => {
let viewer: HTMLElement | null = document.querySelector(".ebook-viewer");
let viewer: any = document.querySelector(".ebook-viewer");
if (!viewer?.innerHTML) return;
viewer.innerHTML = res.value;
this.handleJump();
let htmlParser = new HtmlParser(
new DOMParser().parseFromString(res.value, "text/html")
);
this.props.handleHtmlBook({
doc: htmlParser.getAnchoredDoc(),
chapters: htmlParser.getContentList(),
subitems: [],
});
window.frames[0].document.body.innerHTML = htmlParser.getAnchoredDoc().documentElement.outerHTML;
this.handleRest();
});
};
handleFb2 = (result: ArrayBuffer) => {
@@ -138,13 +157,33 @@ class Viewer extends React.Component<ViewerProps, ViewerState> {
);
let bookObj = xmlBookToObj(Buffer.from(result));
bookObj += xmlBookTagFilter(fb2Str);
let viewer: HTMLElement | null = document.querySelector(".ebook-viewer");
if (!viewer?.innerHTML) return;
viewer.innerHTML = bookObj;
this.handleJump();
window.frames[0].document.body.innerHTML = bookObj;
this.handleRest();
};
handleHtml = (result: ArrayBuffer, format: string) => {
var blob = new Blob([result], {
type: mimetype[format.toLocaleLowerCase()],
});
var reader = new FileReader();
reader.onload = (evt) => {
const html = evt.target?.result as any;
window.frames[0].document.body.innerHTML = html;
this.handleRest();
};
reader.readAsText(blob, "UTF-8");
};
render() {
return <div className="ebook-viewer">Loading</div>;
return (
<iframe
className="ebook-viewer"
title="html-viewer"
width="100%"
height="100%"
>
Loading
</iframe>
);
}
}
export default withRouter(Viewer as any);

View File

@@ -7,6 +7,7 @@ import {
handleReadingEpub,
} from "../../store/actions/book";
import { handleMessageBox, handleMessage } from "../../store/actions/manager";
import { handleHtmlBook } from "../../store/actions/reader";
import Viewer from "./component";
import { stateType } from "../../store";
@@ -24,5 +25,6 @@ const actionCreator = {
handleActionDialog,
handleMessageBox,
handleMessage,
handleHtmlBook,
};
export default connect(mapStateToProps, actionCreator)(Viewer as any);

View File

@@ -1,4 +1,5 @@
import BookModel from "../../model/Book";
import HtmlBookModel from "../../model/HtmlBook";
export interface ViewerProps {
book: BookModel;
@@ -8,6 +9,8 @@ export interface ViewerProps {
handleReadingBook: (book: BookModel) => void;
handleMessage: (message: string) => void;
handleMessageBox: (isShow: boolean) => void;
handleHtmlBook: (htmlBook: HtmlBookModel) => void;
handleLeaveReader: (position: string) => void;
}
export interface ViewerState {
key: string;

View File

@@ -17,18 +17,48 @@ class ContentList extends React.Component<ContentListProps, ContentListState> {
componentWillMount() {
//获取目录
this.props.currentEpub.loaded.navigation
.then((chapters: any) => {
this.setState({ chapters: chapters.toc });
})
.catch(() => {
console.log("Error occurs");
});
if (this.props.currentEpub.loaded) {
this.props.currentEpub.loaded.navigation
.then((chapters: any) => {
this.setState({ chapters: chapters.toc });
})
.catch(() => {
console.log("Error occurs");
});
}
}
handleJump(event: any) {
event.preventDefault();
let href = event.target.getAttribute("href");
this.props.currentEpub.rendition.display(href);
if (this.props.currentEpub.rendition) {
this.props.currentEpub.rendition.display(href);
} else {
// let a = document.createElement("a");
// a.href = href;
// a.innerHTML = "testastas";
// window.frames[0].document.body.appendChild(a);
let id = href.substr(1);
console.log(
id,
window.frames[0].document,
window.frames[0].document.getElementById(id)
);
var top = window.frames[0].document.getElementById(id)?.offsetTop; //Getting Y of target element
if (!top) return;
window.frames[0].scrollTo(0, top);
// window.frames[0].location.href = href;
// const clickEvent = new MouseEvent("click", {
// view: window,
// bubbles: true,
// cancelable: true,
// });
// a.dispatchEvent(clickEvent);
}
}
UNSAFE_componentWillReceiveProps(nextProps: ContentListProps) {
if (nextProps.htmlBook !== this.props.htmlBook) {
this.setState({ chapters: nextProps.htmlBook.chapters });
}
}
render() {
const renderContentList = (items: any, level: number) => {

View File

@@ -6,6 +6,7 @@ const mapStateToProps = (state: stateType) => {
return {
currentEpub: state.book.currentEpub,
chapters: state.reader.chapters,
htmlBook: state.reader.htmlBook,
};
};
const actionCreator = {};

View File

@@ -1,6 +1,8 @@
import HtmlBookModel from "../../../model/HtmlBook";
export interface ContentListProps {
currentEpub: any;
chapters: any;
htmlBook: HtmlBookModel;
}
export interface ContentListState {
chapters: any;

View File

@@ -1,234 +0,0 @@
//阅读器图书内容区域
import React from "react";
import "./viewArea.css";
import PopupMenu from "../../components/popups/popupMenu";
import { ViewAreaProps, ViewAreaStates } from "./interface";
import RecordLocation from "../../utils/readUtils/recordLocation";
import OtherUtil from "../../utils/otherUtil";
import BookmarkModel from "../../model/Bookmark";
import StyleUtil from "../../utils/readUtils/styleUtil";
import ImageViewer from "../../components/imageViewer";
import Lottie from "react-lottie";
import animationSiri from "../../assets/lotties/siri.json";
const siriOptions = {
loop: true,
autoplay: true,
animationData: animationSiri,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};
declare var window: any;
class ViewArea extends React.Component<ViewAreaProps, ViewAreaStates> {
isFirst: boolean;
constructor(props: ViewAreaProps) {
super(props);
this.state = {
cfiRange: null,
contents: null,
rect: null,
loading: true,
};
this.isFirst = true;
}
componentDidMount() {
let epub = this.props.currentEpub;
window.rangy.init(); // 初始化
this.props.rendition.on("locationChanged", () => {
this.props.handleReadingEpub(epub);
this.props.handleOpenMenu(false);
const currentLocation = this.props.rendition.currentLocation();
if (!currentLocation.start) {
return;
}
const cfi = currentLocation.start.cfi;
this.props.handleShowBookmark(
this.props.bookmarks &&
this.props.bookmarks.filter(
(item: BookmarkModel) => item.cfi === cfi
)[0]
? true
: false
);
if (!this.isFirst && this.props.locations) {
let percentage = this.props.locations.percentageFromCfi(cfi);
RecordLocation.recordCfi(this.props.currentBook.key, cfi, percentage);
this.props.handlePercentage(percentage);
} else if (!this.isFirst) {
//如果过暂时没有解析出locations就直接记录cfi
RecordLocation.recordCfi(
this.props.currentBook.key,
cfi,
RecordLocation.getCfi(this.props.currentBook.key).percentage
);
}
this.isFirst = false;
});
this.props.rendition.on("rendered", () => {
this.setState({ loading: false });
let iframe = document.getElementsByTagName("iframe")[0];
if (!iframe) return;
let doc = iframe.contentDocument;
if (!doc) {
return;
}
StyleUtil.addDefaultCss();
this.props.rendition.themes.default({
"a, article, cite, code, div, li, p, pre, span, table": {
"font-size": `${
OtherUtil.getReaderConfig("fontSize") || 17
}px !important`,
"line-height": `${
OtherUtil.getReaderConfig("lineHeight") || "1.25"
} !important`,
"font-family": `${
OtherUtil.getReaderConfig("fontFamily") || "Helvetica"
} !important`,
color: `${
OtherUtil.getReaderConfig("textColor")
? OtherUtil.getReaderConfig("textColor")
: OtherUtil.getReaderConfig("backgroundColor") ===
"rgba(44,47,49,1)" ||
OtherUtil.getReaderConfig("isDisplayDark") === "yes"
? "white"
: ""
} !important`,
"letter-spacing": `${
OtherUtil.getReaderConfig("letterSpacing")
? `${OtherUtil.getReaderConfig("letterSpacing")}px`
: ""
} !important`,
"text-align": `${
OtherUtil.getReaderConfig("textAlign")
? `${OtherUtil.getReaderConfig("textAlign")}`
: ""
} !important`,
"font-weight": `${
OtherUtil.getReaderConfig("isBold") === "yes"
? "bold !important"
: ""
}`,
"font-style": `${
OtherUtil.getReaderConfig("isItalic") === "yes"
? "italic !important"
: ""
}`,
"text-shadow": `${
OtherUtil.getReaderConfig("isShadow") === "yes"
? "2px 2px 2px #cccccc !important"
: ""
}`,
"text-decoration": `${
OtherUtil.getReaderConfig("isUnderline") === "yes"
? "underline !important"
: ""
}`,
"p, div, table": {
"margin-bottom": `${
OtherUtil.getReaderConfig("paraSpacing") || 0
}px !important`,
},
},
});
});
this.props.rendition.on("selected", (cfiRange: any, contents: any) => {
var range = contents.range(cfiRange);
var rect = range.getBoundingClientRect();
this.setState({ cfiRange, contents, rect });
});
this.props.rendition.themes.default({
"a, article, cite, code, div, li, p, pre, span, table": {
"font-size": `${
OtherUtil.getReaderConfig("fontSize") || 17
}px !important`,
"line-height": `${
OtherUtil.getReaderConfig("lineHeight") || "1.25"
} !important`,
"letter-spacing": `${
OtherUtil.getReaderConfig("letterSpacing") || "0"
}px !important`,
"font-family": `${
OtherUtil.getReaderConfig("fontFamily") || "Built-in font"
} !important`,
color: `${
OtherUtil.getReaderConfig("textColor")
? OtherUtil.getReaderConfig("textColor")
: OtherUtil.getReaderConfig("backgroundColor") ===
"rgba(44,47,49,1)" ||
OtherUtil.getReaderConfig("isDisplayDark") === "yes"
? "white"
: ""
} !important`,
"text-align": `${
OtherUtil.getReaderConfig("textAlign")
? `${OtherUtil.getReaderConfig("textAlign")}`
: ""
} !important`,
"font-weight": `${
OtherUtil.getReaderConfig("isBold") === "yes" ? "bold !important" : ""
}`,
"font-style": `${
OtherUtil.getReaderConfig("isItalic") === "yes"
? "italic !important"
: ""
}`,
"text-shadow": `${
OtherUtil.getReaderConfig("isShadow") === "yes"
? "2px 2px 2px #cccccc !important"
: ""
}`,
"text-decoration": `${
OtherUtil.getReaderConfig("isUnderline") === "yes"
? "underline !important"
: ""
}`,
},
"p, div, table": {
"margin-bottom": `${
OtherUtil.getReaderConfig("paraSpacing") || 0
}px !important`,
},
});
this.props.rendition.display(
RecordLocation.getCfi(this.props.currentBook.key) === null
? null
: RecordLocation.getCfi(this.props.currentBook.key).cfi
);
}
render() {
const popupMenuProps = {
rendition: this.props.rendition,
cfiRange: this.state.cfiRange,
contents: this.state.contents,
rect: this.state.rect,
};
return (
<div className="view-area">
<ImageViewer
{...{
isShow: this.props.isShow,
rendition: this.props.rendition,
handleEnterReader: this.props.handleEnterReader,
handleLeaveReader: this.props.handleLeaveReader,
}}
/>
<PopupMenu {...popupMenuProps} />
{this.state.loading ? (
<div className="spinner">
<Lottie options={siriOptions} height={100} width={300} />
</div>
) : null}
<>
{this.props.isShowBookmark ? <div className="bookmark"></div> : null}
</>
</div>
);
}
}
export default ViewArea;

View File

@@ -1,28 +0,0 @@
import { connect } from "react-redux";
import { stateType } from "../../store";
import ViewArea from "./component";
import { handlePercentage } from "../../store/actions/progressPanel";
import {
handleOpenMenu,
handleShowBookmark,
} from "../../store/actions/viewArea";
import { handleReadingEpub } from "../../store/actions/book";
const mapStateToProps = (state: stateType) => {
return {
chapters: state.reader.chapters,
currentEpub: state.book.currentEpub,
currentBook: state.book.currentBook,
locations: state.progressPanel.locations,
bookmarks: state.reader.bookmarks,
isShowBookmark: state.viewArea.isShowBookmark,
};
};
const actionCreator = {
handlePercentage,
handleOpenMenu,
handleShowBookmark,
handleReadingEpub,
};
export default connect(mapStateToProps, actionCreator)(ViewArea);

View File

@@ -1,26 +0,0 @@
import BookModel from "../../model/Book";
import BookmarkModel from "../../model/Bookmark";
export interface ViewAreaProps {
rendition: any;
currentBook: BookModel;
currentEpub: any;
bookmarks: BookmarkModel[];
locations: any;
isShowBookmark: boolean;
chapters: any[];
isShow: boolean;
handleLeaveReader: (position: string) => void;
handleEnterReader: (position: string) => void;
handlePercentage: (percentage: number) => void;
handleOpenMenu: (isOpenMenu: boolean) => void;
handleShowBookmark: (isShowBookmark: boolean) => void;
handleReadingEpub: (epub: object) => void;
}
export interface ViewAreaStates {
loading: boolean;
cfiRange: any;
contents: any;
// rendition: any;
rect: any;
}

17
src/model/HtmlBook.ts Normal file
View File

@@ -0,0 +1,17 @@
class HtmlBook {
doc:HTMLElement;
chapters:{label:string,id:string,href:string}[];
subitems:any;
constructor(
doc:HTMLElement,
chapters:{label:string,id:string,href:string}[],
subitems:any,
) {
this.doc=doc,
this.chapters=chapters,
this.subitems=subitems
}
}
export default HtmlBook;

View File

@@ -43,14 +43,14 @@ class Viewer extends React.Component<ViewerProps, ViewerState> {
RecentBooks.setRecent(key);
});
});
document.documentElement.style.height = "auto";
document.documentElement.style.overflow = "auto";
window.addEventListener("wheel", (event) => {
// document.documentElement.style.height = "auto";
// document.documentElement.style.overflow = "auto";
window.frames[0].document.addEventListener("wheel", (event) => {
RecordLocation.recordScrollHeight(
key,
document.body.clientWidth,
document.body.clientHeight,
document.scrollingElement!.scrollTop
window.frames[0].document.scrollingElement!.scrollTop
);
});
window.onbeforeunload = () => {
@@ -108,12 +108,16 @@ class Viewer extends React.Component<ViewerProps, ViewerState> {
imageDom.src =
"data:" + mimetype[extension.toLowerCase()] + ";base64," + url;
imageDom.setAttribute("style", "width: 100%");
let viewer: HTMLElement | null = document.querySelector(".ebook-viewer");
if (!viewer?.innerHTML) return;
viewer.appendChild(imageDom);
let loading = document.querySelector("p");
window.frames[0].document.body.appendChild(imageDom);
let loading = window.frames[0].document.querySelector("p");
if (!loading) return;
viewer.removeChild(loading);
window.frames[0].document.body.removeChild(loading);
};
handleJump = () => {
window.frames[0].document.scrollingElement!.scrollTo(
0,
RecordLocation.getScrollHeight(this.state.key).scroll
);
};
handleCbz = (result: ArrayBuffer) => {
let zip = new JSZip();
@@ -123,11 +127,7 @@ class Viewer extends React.Component<ViewerProps, ViewerState> {
const extension = filename.split(".").reverse()[0];
this.addImage(content, extension);
}
document.scrollingElement!.scrollTo(
0,
RecordLocation.getScrollHeight(this.state.key).scroll
);
this.handleJump();
});
};
handleCbr = (result: ArrayBuffer) => {
@@ -169,9 +169,14 @@ class Viewer extends React.Component<ViewerProps, ViewerState> {
};
render() {
return (
<div className="ebook-viewer">
<iframe
className="ebook-viewer"
title="html-viewer"
width="100%"
height="100%"
>
<p>Loading</p>
</div>
</iframe>
);
}
}

View File

@@ -2,7 +2,7 @@ import React from "react";
import RecentBooks from "../../utils/readUtils/recordRecent";
import { EpubReaderProps, EpubReaderState } from "./interface";
import localforage from "localforage";
import Reader from "../../containers/epubViewer";
import Reader from "../../containers/epubReader";
import { withRouter } from "react-router-dom";
import _ from "underscore";
import BookUtil from "../../utils/bookUtil";

View File

@@ -0,0 +1,48 @@
//卡片模式下的图书显示
import React from "react";
import { ViewerProps, ViewerState } from "./interface";
import { withRouter } from "react-router-dom";
import OtherUtil from "../../utils/otherUtil";
import RecordLocation from "../../utils/readUtils/recordLocation";
import Viewer from "../../containers/htmlViewer";
class HtmlReader extends React.Component<ViewerProps, ViewerState> {
constructor(props: ViewerProps) {
super(props);
this.state = {};
}
componentDidMount() {
let url = document.location.href.split("/");
let key = url[url.length - 1].split("?")[0];
document.documentElement.style.height = "auto";
document.documentElement.style.overflow = "auto";
window.addEventListener("wheel", (event) => {
RecordLocation.recordScrollHeight(
key,
document.body.clientWidth,
document.body.clientHeight,
document.scrollingElement!.scrollTop
);
});
window.onbeforeunload = () => {
this.handleExit();
};
}
// 点击退出按钮的处理程序
handleExit() {
this.props.handleReadingState(false);
OtherUtil.setReaderConfig("windowWidth", document.body.clientWidth + "");
OtherUtil.setReaderConfig("windowHeight", document.body.clientHeight + "");
OtherUtil.setReaderConfig("windowX", window.screenX + "");
OtherUtil.setReaderConfig("windowY", window.screenY + "");
}
render() {
return <Viewer />;
}
}
export default withRouter(HtmlReader as any);

View File

@@ -1,79 +0,0 @@
//卡片模式下的图书显示
import React from "react";
import RecentBooks from "../../utils/readUtils/recordRecent";
import { ViewerProps, ViewerState } from "./interface";
import localforage from "localforage";
import { withRouter } from "react-router-dom";
import _ from "underscore";
import BookUtil from "../../utils/bookUtil";
import "./viewer.css";
import OtherUtil from "../../utils/otherUtil";
import { mimetype } from "../../constants/mimetype";
declare var window: any;
class Viewer extends React.Component<ViewerProps, ViewerState> {
epub: any;
constructor(props: ViewerProps) {
super(props);
this.state = {};
}
componentDidMount() {
let url = document.location.href.split("/");
let key = url[url.length - 1].split("?")[0];
localforage.getItem("books").then((result: any) => {
let book = result[_.findIndex(result, { key })];
BookUtil.fetchBook(key, true).then((result) => {
this.props.handleReadingBook(book);
this.handleHtml(result as ArrayBuffer, book.format);
this.props.handleReadingState(true);
RecentBooks.setRecent(key);
});
});
// document
// .querySelectorAll('style,link[rel="stylesheet"]')
// .forEach((item) => item.remove());
window.onbeforeunload = () => {
this.handleExit();
};
}
// 点击退出按钮的处理程序
handleExit() {
this.props.handleReadingState(false);
OtherUtil.setReaderConfig("windowWidth", document.body.clientWidth + "");
OtherUtil.setReaderConfig("windowHeight", document.body.clientHeight + "");
OtherUtil.setReaderConfig("windowX", window.screenX + "");
OtherUtil.setReaderConfig("windowY", window.screenY + "");
}
handleHtml = (result: ArrayBuffer, format: string) => {
var blob = new Blob([result], {
type: mimetype[format.toLocaleLowerCase()],
});
var reader = new FileReader();
reader.onload = function (evt) {
let iframe: any = document.querySelector(".html-viewer");
if (!iframe) return;
const html = evt.target?.result as any;
iframe.sandbox = "";
iframe.srcdoc = html;
};
reader.readAsText(blob, "UTF-8");
};
render() {
return (
<iframe
className="html-viewer"
title="html-viewer"
width="100%"
height="100%"
/>
);
}
}
export default withRouter(Viewer as any);

View File

@@ -1,3 +0,0 @@
/* #root {
height: ;
} */

View File

@@ -1,11 +1,10 @@
import React, { useEffect } from "react";
import { Route, Switch, HashRouter } from "react-router-dom";
import Manager from "../pages/manager";
import EpubReader from "../pages/epubReader";
import Viewer from "../pages/viewer";
import EpubReader from "../pages/epubPage";
import HtmlReader from "../containers/htmlReader";
import DjvuReader from "../pages/djvuReader";
import ComicReader from "../pages/comicReader";
import HtmlViewer from "../pages/htmlViewer";
import _Redirect from "../pages/redirect";
import i18n from "../i18n";
import OtherUtil from "../utils/otherUtil";
@@ -34,22 +33,23 @@ const Router = () => {
<HashRouter>
<Switch>
<Route component={Manager} path="/manager" />
<Route component={EpubReader} path="/epub" />{" "}
<Route component={EpubReader} path="/epub" />
<Route component={DjvuReader} path="/djvu" />
<Route component={Viewer} path="/mobi" />
<Route component={Viewer} path="/azw3" />
<Route component={Viewer} path="/txt" />
<Route component={Viewer} path="/docx" />
<Route component={Viewer} path="/md" />
<Route component={HtmlViewer} path="/html" />
<Route component={HtmlViewer} path="/htm" />
<Route component={HtmlViewer} path="/xml" />
<Route component={HtmlViewer} path="/xhtml" />
<Route component={Viewer} path="/rtf" />
<Route component={Viewer} path="/fb2" />
<Route component={HtmlReader} path="/mobi" />
<Route component={HtmlReader} path="/azw3" />
<Route component={HtmlReader} path="/txt" />
<Route component={HtmlReader} path="/docx" />
<Route component={HtmlReader} path="/md" />
<Route component={HtmlReader} path="/rtf" />
<Route component={HtmlReader} path="/fb2" />
<Route component={ComicReader} path="/cbr" />
<Route component={ComicReader} path="/cbz" />
<Route component={ComicReader} path="/cbt" />
<Route component={ComicReader} path="/cbt" />{" "}
<Route component={HtmlReader} path="/html" />
<Route component={HtmlReader} path="/htm" />
<Route component={HtmlReader} path="/xml" />
<Route component={HtmlReader} path="/xhtml" />
<Route component={HtmlReader} path="/href" />
<Route component={_Redirect} path="/" />
</Switch>
</HashRouter>

View File

@@ -1,6 +1,7 @@
import localforage from "localforage";
import NoteModel from "../../model/Note";
import BookmarkModel from "../../model/Bookmark";
import HtmlBookModel from "../../model/HtmlBook";
import AddTrash from "../../utils/readUtils/addTrash";
export function handleNotes(notes: NoteModel[]) {
return { type: "HANDLE_NOTES", payload: notes };
@@ -17,7 +18,9 @@ export function handleBookmarks(bookmarks: BookmarkModel[]) {
export function handleDigests(digests: NoteModel[]) {
return { type: "HANDLE_DIGESTS", payload: digests };
}
export function handleHtmlBook(htmlBook: HtmlBookModel) {
return { type: "HANDLE_HTML_BOOK", payload: htmlBook };
}
export function handleCurrentChapter(currentChapter: string) {
return { type: "HANDLE_CURRENT_CHAPTER", payload: currentChapter };
}

View File

@@ -10,6 +10,7 @@ import { backupPage } from "./reducers/backupPage";
import BookModel from "../model/Book";
import NoteModel from "../model/Note";
import BookmarkModel from "../model/Bookmark";
import HtmlBookModel from "../model/HtmlBook";
const rootReducer = combineReducers({
book,
manager,
@@ -76,6 +77,7 @@ export type stateType = {
flattenChapters: any;
noteKey: string;
originalText: string;
htmlBook: HtmlBookModel;
};
sidebar: {
mode: string;

View File

@@ -9,6 +9,7 @@ const initState = {
color: OtherUtil.getReaderConfig("isDisplayDark") === "yes" ? 3 : 0,
noteKey: "",
originalText: "",
htmlBook: null,
readerMode: OtherUtil.getReaderConfig("readerMode") || "double",
};
export function reader(
@@ -31,6 +32,11 @@ export function reader(
...state,
originalText: action.payload,
};
case "HANDLE_HTML_BOOK":
return {
...state,
htmlBook: action.payload,
};
case "HANDLE_COLOR":
return {
...state,

36
src/utils/htmlParser.tsx Normal file
View File

@@ -0,0 +1,36 @@
class HtmlParser {
bookDoc: any;
contentList: HTMLElement[];
contentTitleList: any[];
constructor(bookDoc: any) {
this.bookDoc = bookDoc;
this.contentList = [];
this.contentTitleList = [];
this.getContent(bookDoc);
}
getContent(bookDoc: HTMLElement) {
this.contentList = Array.from(
bookDoc.querySelectorAll("h1,h2,h3,h4,h5,font")
);
for (let i = 0; i < this.contentList.length; i++) {
let random = Math.floor(Math.random() * 900) + 100;
this.contentTitleList.push({
label: this.contentList[i].innerText,
id: this.contentList[i].innerText.replaceAll(" ", "_") + random,
href: "#" + this.contentList[i].innerText.replaceAll(" ", "_") + random,
subitems: [],
});
}
for (let i = 0; i < this.contentList.length; i++) {
this.contentList[i].id = this.contentTitleList[i].id;
}
}
getAnchoredDoc() {
return this.bookDoc;
}
getContentList() {
return this.contentTitleList;
}
}
export default HtmlParser;

View File

@@ -7,7 +7,7 @@ class styleUtil {
if (!iframe) return;
let doc = iframe.contentDocument;
if (!doc) return;
let css = this.getDefaultCss();
let css = this.getDefaultCss() + this.getCustomCss();
let style = doc.getElementById("default-style");
let background = document.querySelector(".background");
if (!background) return;
@@ -27,6 +27,32 @@ class styleUtil {
}
style.textContent = css;
}
static addHtmlCss() {
let iframe = document.getElementsByTagName("iframe")[0];
if (!iframe) return;
let doc = iframe.contentDocument;
if (!doc) return;
let css = this.getDefaultCss();
let style = doc.getElementById("default-style");
let background = document.querySelector(".viewer");
if (!background) return;
background!.setAttribute(
"style",
`background-color:${OtherUtil.getReaderConfig("backgroundColor")}`
);
window.frames[0].document.body.setAttribute(
"style",
this.getCustomCss() as string
);
if (!style) {
style = doc.createElement("style");
style.id = "default-style";
style.textContent = css;
doc.head.appendChild(style);
return;
}
style.textContent = css;
}
// 获取为文档默认应用的css样式
static getDefaultCss() {
let colors = ["#FBF1D1", "#EFEEB0", "#CAEFC9", "#76BEE9"];
@@ -34,6 +60,106 @@ class styleUtil {
return `::selection{background:#f3a6a68c}::-moz-selection{background:#f3a6a68c}[class*=color-]:hover{cursor:pointer;background-image:linear-gradient(0,rgba(0,0,0,.075),rgba(0,0,0,.075))}.color-0{background-color:${colors[0]}}.color-1{background-color:${colors[1]}}.color-2{background-color:${colors[2]}}.color-3{background-color:${colors[3]}}.line-0{border-bottom:2px solid ${lines[0]}}.line-1{border-bottom:2px solid ${lines[1]}}.line-2{border-bottom:2px solid ${lines[2]}}.line-3{border-bottom:2px solid ${lines[3]}}}`;
}
static getCustomCss(isJSON: boolean = true) {
if (isJSON) {
return `font-size: ${
OtherUtil.getReaderConfig("fontSize") || 17
}px !important;line-height: ${
OtherUtil.getReaderConfig("lineHeight") || "1.25"
} !important;font-family: ${
OtherUtil.getReaderConfig("fontFamily") || "Helvetica"
} !important;color: ${
OtherUtil.getReaderConfig("textColor")
? OtherUtil.getReaderConfig("textColor")
: OtherUtil.getReaderConfig("backgroundColor") ===
"rgba(44,47,49,1)" ||
OtherUtil.getReaderConfig("isDisplayDark") === "yes"
? "white"
: ""
} !important;letter-spacing: ${
OtherUtil.getReaderConfig("letterSpacing")
? OtherUtil.getReaderConfig("letterSpacing")
: ""
}px !important;text-align: ${
OtherUtil.getReaderConfig("textAlign")
? OtherUtil.getReaderConfig("textAlign")
: ""
} !important;
font-weight: ${
OtherUtil.getReaderConfig("isBold") === "yes" ? "bold !important" : ""
};font-style: ${
OtherUtil.getReaderConfig("isItalic") === "yes"
? "italic !important"
: ""
};text-shadow: ${
OtherUtil.getReaderConfig("isShadow") === "yes"
? "2px 2px 2px #cccccc !important"
: ""
};text-decoration: ${
OtherUtil.getReaderConfig("isUnderline") === "yes"
? "underline !important"
: ""
};margin-bottom: ${
OtherUtil.getReaderConfig("paraSpacing") || 0
}px !important;`;
} else {
return {
"a, article, cite, code, div, li, p, pre, span, table": {
"font-size": `${
OtherUtil.getReaderConfig("fontSize") || 17
}px !important`,
"line-height": `${
OtherUtil.getReaderConfig("lineHeight") || "1.25"
} !important`,
"font-family": `${
OtherUtil.getReaderConfig("fontFamily") || "Helvetica"
} !important`,
color: `${
OtherUtil.getReaderConfig("textColor")
? OtherUtil.getReaderConfig("textColor")
: OtherUtil.getReaderConfig("backgroundColor") ===
"rgba(44,47,49,1)" ||
OtherUtil.getReaderConfig("isDisplayDark") === "yes"
? "white"
: ""
} !important`,
"letter-spacing": `${
OtherUtil.getReaderConfig("letterSpacing")
? `${OtherUtil.getReaderConfig("letterSpacing")}px`
: ""
} !important`,
"text-align": `${
OtherUtil.getReaderConfig("textAlign")
? `${OtherUtil.getReaderConfig("textAlign")}`
: ""
} !important`,
"font-weight": `${
OtherUtil.getReaderConfig("isBold") === "yes"
? "bold !important"
: ""
}`,
"font-style": `${
OtherUtil.getReaderConfig("isItalic") === "yes"
? "italic !important"
: ""
}`,
"text-shadow": `${
OtherUtil.getReaderConfig("isShadow") === "yes"
? "2px 2px 2px #cccccc !important"
: ""
}`,
"text-decoration": `${
OtherUtil.getReaderConfig("isUnderline") === "yes"
? "underline !important"
: ""
}`,
"margin-bottom": `${
OtherUtil.getReaderConfig("paraSpacing") || 0
}px !important`,
},
};
}
}
static addStyle = (url: string) => {
const style = document.createElement("link");
style.href = url;