mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-16 00:44:51 +02:00
Ebook and comic reader #14
This commit is contained in:
parent
3234e7ef06
commit
5a025b3a03
21 changed files with 2139 additions and 14 deletions
|
@ -13,8 +13,8 @@ android {
|
||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 22
|
versionCode 23
|
||||||
versionName "0.9.6-beta"
|
versionName "0.9.7-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
@import "./fonts.css";
|
@import "./fonts.css";
|
||||||
|
|
||||||
|
.box-shadow-sm {
|
||||||
|
box-shadow: 0px 3px 6px #11111170;
|
||||||
|
}
|
||||||
|
|
||||||
.box-shadow-md {
|
.box-shadow-md {
|
||||||
box-shadow: 2px 8px 6px #111111aa;
|
box-shadow: 2px 8px 6px #111111aa;
|
||||||
}
|
}
|
||||||
|
|
506
assets/ebooks/basic.js
Normal file
506
assets/ebooks/basic.js
Normal file
|
@ -0,0 +1,506 @@
|
||||||
|
/*
|
||||||
|
Calibres stylesheet
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default `
|
||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calibre styles
|
||||||
|
*/
|
||||||
|
.arabic {
|
||||||
|
display: block;
|
||||||
|
list-style-type: decimal;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
text-align: justify
|
||||||
|
}
|
||||||
|
.attribution {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0.3em 0
|
||||||
|
}
|
||||||
|
.big {
|
||||||
|
font-size: 1.375em;
|
||||||
|
line-height: 1.2
|
||||||
|
}
|
||||||
|
.big1 {
|
||||||
|
font-size: 1em
|
||||||
|
}
|
||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 1em 1em 2em
|
||||||
|
}
|
||||||
|
.block1 {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 1em 4em
|
||||||
|
}
|
||||||
|
.block2 {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 1em 1em 1em 2em
|
||||||
|
}
|
||||||
|
.bullet {
|
||||||
|
display: block;
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
text-align: disc
|
||||||
|
}
|
||||||
|
.calibre {
|
||||||
|
background-color: #000007;
|
||||||
|
display: block;
|
||||||
|
font-family: Charis, "Times New Roman", Verdana, Arial;
|
||||||
|
font-size: 1.125em;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 5pt
|
||||||
|
}
|
||||||
|
.calibre1 {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
.calibre2 {
|
||||||
|
height: auto;
|
||||||
|
width: auto
|
||||||
|
}
|
||||||
|
.calibre3:not(strong) {
|
||||||
|
display: block;
|
||||||
|
font-family: Charis, "Times New Roman", Verdana, Arial;
|
||||||
|
font-size: 1.125em;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
margin: 0 5pt
|
||||||
|
}
|
||||||
|
.calibre4 {
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
.calibre5 {
|
||||||
|
font-style: italic
|
||||||
|
}
|
||||||
|
.calibre6 {
|
||||||
|
background-color: #FFF;
|
||||||
|
display: block;
|
||||||
|
font-family: Charis, "Times New Roman", Verdana, Arial;
|
||||||
|
font-size: 1.125em;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 5pt
|
||||||
|
}
|
||||||
|
.calibre7 {
|
||||||
|
display: list-item
|
||||||
|
}
|
||||||
|
.calibre8 {
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.2;
|
||||||
|
vertical-align: super
|
||||||
|
}
|
||||||
|
.calibre9 {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 2px;
|
||||||
|
display: table;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
text-indent: 0
|
||||||
|
}
|
||||||
|
.calibre10 {
|
||||||
|
display: table-row;
|
||||||
|
vertical-align: middle
|
||||||
|
}
|
||||||
|
.calibre11 {
|
||||||
|
display: table-cell;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: inherit;
|
||||||
|
padding: 1px
|
||||||
|
}
|
||||||
|
.calibre12 {
|
||||||
|
display: table-cell;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: inherit;
|
||||||
|
padding: 1px
|
||||||
|
}
|
||||||
|
.calibre13 {
|
||||||
|
height: 1em;
|
||||||
|
width: auto
|
||||||
|
}
|
||||||
|
.calibre14 {
|
||||||
|
font-size: 0.88889em;
|
||||||
|
line-height: 1.2;
|
||||||
|
vertical-align: super
|
||||||
|
}
|
||||||
|
.calibre15 {
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.2;
|
||||||
|
vertical-align: sub
|
||||||
|
}
|
||||||
|
.calibre16 {
|
||||||
|
display: block;
|
||||||
|
list-style-type: decimal;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-top: 1em
|
||||||
|
}
|
||||||
|
.calibre17 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0.83em 0
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1em 0
|
||||||
|
}
|
||||||
|
.center1 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
margin: -2em 0 3em
|
||||||
|
}
|
||||||
|
.center2 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
margin: 2em 0 1em
|
||||||
|
}
|
||||||
|
.center3 {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin: -1em 0 1em
|
||||||
|
}
|
||||||
|
.center4 {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-indent: 3%;
|
||||||
|
margin: 1em 0
|
||||||
|
}
|
||||||
|
.chapter {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
margin: 2em 0 1em
|
||||||
|
}
|
||||||
|
.chapter1 {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.88889em;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
margin-top: 2em
|
||||||
|
}
|
||||||
|
.chapter2 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
margin: 2em 0 3em
|
||||||
|
}
|
||||||
|
.copyright {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.88889em;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-top: 4em;
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
.dedication {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.88889em;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-top: 4em
|
||||||
|
}
|
||||||
|
.dropcaps {
|
||||||
|
float: left;
|
||||||
|
font-size: 3.4375rem;
|
||||||
|
line-height: 50px;
|
||||||
|
margin-right: 0.09em;
|
||||||
|
margin-top: -0.05em;
|
||||||
|
padding-top: 1px
|
||||||
|
}
|
||||||
|
.dropcaps1 {
|
||||||
|
float: left;
|
||||||
|
font-size: 3.4375rem;
|
||||||
|
line-height: 50px;
|
||||||
|
margin-right: 0.09em;
|
||||||
|
margin-top: 0.15em;
|
||||||
|
padding-top: 1px
|
||||||
|
}
|
||||||
|
.extract {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 2em 0 0.3em
|
||||||
|
}
|
||||||
|
.extract1 {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
text-indent: 3%;
|
||||||
|
margin: 2em 0 0.3em
|
||||||
|
}
|
||||||
|
.extract2 {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 1em 0 0.3em
|
||||||
|
}
|
||||||
|
.footnote {
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right-style: solid;
|
||||||
|
border-right-width: 0;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-width: 1px;
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-top: 2 em
|
||||||
|
}
|
||||||
|
.footnote1 {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 0.3em 0 0.3em 2
|
||||||
|
}
|
||||||
|
.footnote2 {
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right-style: solid;
|
||||||
|
border-right-width: 0;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-width: 1px;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.88889em;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-top: 2 em
|
||||||
|
}
|
||||||
|
.hanging {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.88889em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
text-indent: -1em;
|
||||||
|
margin: 0.5em 0 0.3em 1em
|
||||||
|
}
|
||||||
|
.hanging1 {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.88889em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
text-indent: -1em;
|
||||||
|
margin: 0.5em 0 0.3em 1.5em
|
||||||
|
}
|
||||||
|
.hanging2 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-indent: -1em;
|
||||||
|
margin: 0.5em 0 0.3em 1em
|
||||||
|
}
|
||||||
|
.hanging3 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
text-indent: 1em;
|
||||||
|
margin: 0.1em 0 0.3em 1em
|
||||||
|
}
|
||||||
|
.hanging4 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
text-indent: 0.1em;
|
||||||
|
margin: 0.1em 0 0.3em 1em
|
||||||
|
}
|
||||||
|
a.hlink {
|
||||||
|
text-decoration: none
|
||||||
|
}
|
||||||
|
.indent {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
text-indent: 1em;
|
||||||
|
margin: 0.3em 0
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
border-top: currentColor solid 1px;
|
||||||
|
border-bottom: currentColor solid 1px
|
||||||
|
}
|
||||||
|
.loweralpha {
|
||||||
|
display: block;
|
||||||
|
list-style-type: lower-alpha;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
text-align: justify
|
||||||
|
}
|
||||||
|
.none {
|
||||||
|
display: block;
|
||||||
|
list-style-type: none;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
text-align: justify
|
||||||
|
}
|
||||||
|
.none1 {
|
||||||
|
display: block;
|
||||||
|
list-style-type: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
text-align: justify
|
||||||
|
}
|
||||||
|
.nonindent {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 0.3em 0
|
||||||
|
}
|
||||||
|
.nonindent1 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-indent: -1em;
|
||||||
|
margin: 0.5em 0 0.3em 0.1em
|
||||||
|
}
|
||||||
|
.nonindent2 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-indent: -1em;
|
||||||
|
margin: 0.5em 0 0.3em -0.5em
|
||||||
|
}
|
||||||
|
.nonindent3 {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
text-indent: 3%;
|
||||||
|
margin: 0.3em 0
|
||||||
|
}
|
||||||
|
.part {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
margin: 4em 0 1em
|
||||||
|
}
|
||||||
|
.preface {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.88889em;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-left: 2em;
|
||||||
|
margin-right: 2em;
|
||||||
|
text-align: justify
|
||||||
|
}
|
||||||
|
.pubhlink {
|
||||||
|
color: green;
|
||||||
|
text-decoration: none
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
margin: 0.3em 0
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
margin: 2em 0 0.5em
|
||||||
|
}
|
||||||
|
.section1 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
margin: 2em 0 0.3em
|
||||||
|
}
|
||||||
|
.section2 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
margin: 2em 0 0.3em 1em
|
||||||
|
}
|
||||||
|
.small {
|
||||||
|
font-size: 0.66667em
|
||||||
|
}
|
||||||
|
.small1 {
|
||||||
|
font-size: 0.75em
|
||||||
|
}
|
||||||
|
.subchapter {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 1em 0
|
||||||
|
}
|
||||||
|
.textbox {
|
||||||
|
background-color: #E4E4E4;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.5em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
margin-top: 2em;
|
||||||
|
text-align: justify;
|
||||||
|
border-top: currentColor double 2px;
|
||||||
|
border-bottom: currentColor double 2px
|
||||||
|
}
|
||||||
|
.textbox1 {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 0.3em 0.5em 0.3em 0.8em
|
||||||
|
}
|
||||||
|
.textbox2 {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
text-indent: 1em;
|
||||||
|
margin: 0.3em 0.5em
|
||||||
|
}
|
||||||
|
.textbox3 {
|
||||||
|
display: block;
|
||||||
|
text-align: justify;
|
||||||
|
text-indent: 3%;
|
||||||
|
margin: 0.3em 0.5em 0.3em 0.8em
|
||||||
|
}
|
||||||
|
.titlepage {
|
||||||
|
display: block;
|
||||||
|
margin-left: -0.4em;
|
||||||
|
margin-top: 1.2em
|
||||||
|
}
|
||||||
|
.toc {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
.toc1 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0.67em 0 3em
|
||||||
|
}
|
||||||
|
.underline {
|
||||||
|
text-decoration: underline
|
||||||
|
}
|
||||||
|
`
|
248
assets/ebooks/htmlParser.js
Normal file
248
assets/ebooks/htmlParser.js
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
/*
|
||||||
|
This is borrowed from koodo-reader https://github.com/troyeguo/koodo-reader/tree/master/src
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const isTitle = (
|
||||||
|
line,
|
||||||
|
isContainDI = false,
|
||||||
|
isContainChapter = false,
|
||||||
|
isContainCHAPTER = false
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
line.length < 30 &&
|
||||||
|
line.indexOf("[") === -1 &&
|
||||||
|
line.indexOf("(") === -1 &&
|
||||||
|
(line.startsWith("CHAPTER") ||
|
||||||
|
line.startsWith("Chapter") ||
|
||||||
|
line.startsWith("序章") ||
|
||||||
|
line.startsWith("前言") ||
|
||||||
|
line.startsWith("声明") ||
|
||||||
|
line.startsWith("聲明") ||
|
||||||
|
line.startsWith("写在前面的话") ||
|
||||||
|
line.startsWith("后记") ||
|
||||||
|
line.startsWith("楔子") ||
|
||||||
|
line.startsWith("后序") ||
|
||||||
|
line.startsWith("寫在前面的話") ||
|
||||||
|
line.startsWith("後記") ||
|
||||||
|
line.startsWith("後序") ||
|
||||||
|
/(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$/.test(
|
||||||
|
line
|
||||||
|
) ||
|
||||||
|
(line.startsWith("第") && startWithDI(line)) ||
|
||||||
|
(line.startsWith("卷") && startWithJUAN(line)) ||
|
||||||
|
startWithRomanNum(line) ||
|
||||||
|
(!isContainDI &&
|
||||||
|
!isContainChapter &&
|
||||||
|
!isContainCHAPTER &&
|
||||||
|
line.indexOf("第") > -1 &&
|
||||||
|
(line[line.indexOf("第") - 1] === " " ||
|
||||||
|
line[line.indexOf("第") - 1] === " " ||
|
||||||
|
line[line.indexOf("第") - 1] === "、" ||
|
||||||
|
line[line.indexOf("第") - 1] === ":" ||
|
||||||
|
line[line.indexOf("第") - 1] === ":") &&
|
||||||
|
startWithDI(line.substr(line.indexOf("第")))) ||
|
||||||
|
(!isContainDI &&
|
||||||
|
!isContainChapter &&
|
||||||
|
!isContainCHAPTER &&
|
||||||
|
line.indexOf(" ") &&
|
||||||
|
startWithNumAndSpace(line)) ||
|
||||||
|
(!isContainDI &&
|
||||||
|
!isContainChapter &&
|
||||||
|
!isContainCHAPTER &&
|
||||||
|
line.indexOf(" ") &&
|
||||||
|
startWithNumAndSpace(line)) ||
|
||||||
|
(!isContainDI &&
|
||||||
|
!isContainChapter &&
|
||||||
|
!isContainCHAPTER &&
|
||||||
|
line.indexOf("、") &&
|
||||||
|
startWithNumAndPause(line)) ||
|
||||||
|
(!isContainDI &&
|
||||||
|
!isContainChapter &&
|
||||||
|
!isContainCHAPTER &&
|
||||||
|
line.indexOf(":") &&
|
||||||
|
startWithNumAndColon(line)) ||
|
||||||
|
(!isContainDI &&
|
||||||
|
!isContainChapter &&
|
||||||
|
!isContainCHAPTER &&
|
||||||
|
line.indexOf(":") &&
|
||||||
|
startWithNumAndColon(line)))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const startWithDI = (line) => {
|
||||||
|
let keywords = [
|
||||||
|
"章",
|
||||||
|
"节",
|
||||||
|
"回",
|
||||||
|
"節",
|
||||||
|
"卷",
|
||||||
|
"部",
|
||||||
|
"輯",
|
||||||
|
"辑",
|
||||||
|
"話",
|
||||||
|
"集",
|
||||||
|
"话",
|
||||||
|
"篇",
|
||||||
|
];
|
||||||
|
let flag = false;
|
||||||
|
for (let i = 0; i < keywords.length; i++) {
|
||||||
|
if (
|
||||||
|
(line.indexOf(keywords[i]) > -1 &&
|
||||||
|
(line[line.indexOf(keywords[i]) + 1] === " " ||
|
||||||
|
line[line.indexOf(keywords[i]) + 1] === " " ||
|
||||||
|
line[line.indexOf(keywords[i]) + 1] === "、" ||
|
||||||
|
line[line.indexOf(keywords[i]) + 1] === ":" ||
|
||||||
|
line[line.indexOf(keywords[i]) + 1] === ":")) ||
|
||||||
|
!line[line.indexOf(keywords[i]) + 1]
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c]+$/.test(
|
||||||
|
line.substring(1, line.indexOf(keywords[i])).trim()
|
||||||
|
) ||
|
||||||
|
/^\d+$/.test(line.substring(1, line.indexOf(keywords[i])).trim())
|
||||||
|
) {
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
if (flag) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flag;
|
||||||
|
};
|
||||||
|
const startWithJUAN = (line) => {
|
||||||
|
if (
|
||||||
|
/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c]+$/.test(
|
||||||
|
line.substring(1, line.indexOf(" "))
|
||||||
|
) ||
|
||||||
|
/^\d+$/.test(line.substring(1, line.indexOf(" ")))
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
if (
|
||||||
|
/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c]+$/.test(
|
||||||
|
line.substring(1, line.indexOf(" "))
|
||||||
|
) ||
|
||||||
|
/^\d+$/.test(line.substring(1, line.indexOf(" ")))
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
if (
|
||||||
|
/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c]+$/.test(
|
||||||
|
line.substring(1)
|
||||||
|
) ||
|
||||||
|
/^\d+$/.test(line.substring(1))
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const startWithRomanNum = (line) => {
|
||||||
|
if (
|
||||||
|
/(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$/.test(
|
||||||
|
line.substring(0, line.indexOf(" "))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
if (
|
||||||
|
/(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$/.test(
|
||||||
|
line.substring(0, line.indexOf("."))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
if (
|
||||||
|
/(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$/.test(
|
||||||
|
line.trim()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const startWithNumAndSpace = (line) => {
|
||||||
|
if (
|
||||||
|
/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c]+$/.test(
|
||||||
|
line.substring(0, line.indexOf(" "))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
if (
|
||||||
|
/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c]+$/.test(
|
||||||
|
line.substring(0, line.indexOf(" "))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (/^\d+$/.test(line.substring(0, line.indexOf(" ")))) return true;
|
||||||
|
if (/^\d+$/.test(line.substring(0, line.indexOf(" ")))) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const startWithNumAndColon = (line) => {
|
||||||
|
if (
|
||||||
|
/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c]+$/.test(
|
||||||
|
line.substring(0, line.indexOf(":"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
if (
|
||||||
|
/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c]+$/.test(
|
||||||
|
line.substring(0, line.indexOf(":"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (/^\d+$/.test(line.substring(0, line.indexOf(":")))) return true;
|
||||||
|
if (/^\d+$/.test(line.substring(0, line.indexOf(":")))) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const startWithNumAndPause = (line) => {
|
||||||
|
if (
|
||||||
|
/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c]+$/.test(
|
||||||
|
line.substring(0, line.indexOf("、"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (/^\d+$/.test(line.substring(0, line.indexOf("、")))) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class HtmlParser {
|
||||||
|
bookDoc;
|
||||||
|
contentList;
|
||||||
|
contentTitleList;
|
||||||
|
constructor(bookDoc) {
|
||||||
|
this.bookDoc = bookDoc;
|
||||||
|
this.contentList = [];
|
||||||
|
this.contentTitleList = [];
|
||||||
|
this.getContent(bookDoc);
|
||||||
|
}
|
||||||
|
getContent(bookDoc) {
|
||||||
|
this.contentList = Array.from(
|
||||||
|
bookDoc.querySelectorAll("h1,h2,h3,h4,h5,b,font")
|
||||||
|
).filter((item, index) => {
|
||||||
|
return isTitle(item.innerText.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < this.contentList.length; i++) {
|
||||||
|
let random = Math.floor(Math.random() * 900000) + 100000;
|
||||||
|
this.contentTitleList.push({
|
||||||
|
label: this.contentList[i].innerText,
|
||||||
|
id: "title" + random,
|
||||||
|
href: "#title" + 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.filter((item, index) => {
|
||||||
|
if (index > 0) {
|
||||||
|
return item.label !== this.contentTitleList[index - 1].label;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HtmlParser;
|
450
assets/ebooks/mobi.js
Normal file
450
assets/ebooks/mobi.js
Normal file
|
@ -0,0 +1,450 @@
|
||||||
|
/*
|
||||||
|
This is borrowed from koodo-reader https://github.com/troyeguo/koodo-reader/tree/master/src
|
||||||
|
*/
|
||||||
|
|
||||||
|
function ab2str(buf) {
|
||||||
|
if (buf instanceof ArrayBuffer) {
|
||||||
|
buf = new Uint8Array(buf);
|
||||||
|
}
|
||||||
|
return new TextDecoder("utf-8").decode(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
var domParser = new DOMParser();
|
||||||
|
|
||||||
|
class Buffer {
|
||||||
|
capacity;
|
||||||
|
fragment_list;
|
||||||
|
imageArray;
|
||||||
|
cur_fragment;
|
||||||
|
constructor(capacity) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.fragment_list = [];
|
||||||
|
this.imageArray = [];
|
||||||
|
this.cur_fragment = new Fragment(capacity);
|
||||||
|
this.fragment_list.push(this.cur_fragment);
|
||||||
|
}
|
||||||
|
write(byte) {
|
||||||
|
var result = this.cur_fragment.write(byte);
|
||||||
|
if (!result) {
|
||||||
|
this.cur_fragment = new Fragment(this.capacity);
|
||||||
|
this.fragment_list.push(this.cur_fragment);
|
||||||
|
this.cur_fragment.write(byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get(idx) {
|
||||||
|
var fi = 0;
|
||||||
|
while (fi < this.fragment_list.length) {
|
||||||
|
var frag = this.fragment_list[fi];
|
||||||
|
if (idx < frag.size) {
|
||||||
|
return frag.get(idx);
|
||||||
|
}
|
||||||
|
idx -= frag.size;
|
||||||
|
fi += 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
size() {
|
||||||
|
var s = 0;
|
||||||
|
for (var i = 0; i < this.fragment_list.length; i++) {
|
||||||
|
s += this.fragment_list[i].size;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
shrink() {
|
||||||
|
var total_buffer = new Uint8Array(this.size());
|
||||||
|
var offset = 0;
|
||||||
|
for (var i = 0; i < this.fragment_list.length; i++) {
|
||||||
|
var frag = this.fragment_list[i];
|
||||||
|
if (frag.full()) {
|
||||||
|
total_buffer.set(frag.buffer, offset);
|
||||||
|
} else {
|
||||||
|
total_buffer.set(frag.buffer.slice(0, frag.size), offset);
|
||||||
|
}
|
||||||
|
offset += frag.size;
|
||||||
|
}
|
||||||
|
return total_buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var copagesne_uint8array = function (buffers) {
|
||||||
|
var total_size = 0;
|
||||||
|
for (let i = 0; i < buffers.length; i++) {
|
||||||
|
var buffer = buffers[i];
|
||||||
|
total_size += buffer.length;
|
||||||
|
}
|
||||||
|
var total_buffer = new Uint8Array(total_size);
|
||||||
|
var offset = 0;
|
||||||
|
for (let i = 0; i < buffers.length; i++) {
|
||||||
|
buffer = buffers[i];
|
||||||
|
total_buffer.set(buffer, offset);
|
||||||
|
offset += buffer.length;
|
||||||
|
}
|
||||||
|
return total_buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Fragment {
|
||||||
|
buffer;
|
||||||
|
capacity;
|
||||||
|
size;
|
||||||
|
constructor(capacity) {
|
||||||
|
this.buffer = new Uint8Array(capacity);
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
write(byte) {
|
||||||
|
if (this.size >= this.capacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.buffer[this.size] = byte;
|
||||||
|
this.size += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
full() {
|
||||||
|
return this.size === this.capacity;
|
||||||
|
}
|
||||||
|
get(idx) {
|
||||||
|
return this.buffer[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var uncompression_lz77 = function (data) {
|
||||||
|
var length = data.length;
|
||||||
|
var offset = 0; // Current offset into data
|
||||||
|
var buffer = new Buffer(data.length);
|
||||||
|
|
||||||
|
while (offset < length) {
|
||||||
|
var char = data[offset];
|
||||||
|
offset += 1;
|
||||||
|
|
||||||
|
if (char === 0) {
|
||||||
|
buffer.write(char);
|
||||||
|
} else if (char <= 8) {
|
||||||
|
for (var i = offset; i < offset + char; i++) {
|
||||||
|
buffer.write(data[i]);
|
||||||
|
}
|
||||||
|
offset += char;
|
||||||
|
} else if (char <= 0x7f) {
|
||||||
|
buffer.write(char);
|
||||||
|
} else if (char <= 0xbf) {
|
||||||
|
var next = data[offset];
|
||||||
|
offset += 1;
|
||||||
|
var distance = (((char << 8) | next) >> 3) & 0x7ff;
|
||||||
|
var lz_length = (next & 0x7) + 3;
|
||||||
|
|
||||||
|
var buffer_size = buffer.size();
|
||||||
|
for (let i = 0; i < lz_length; i++) {
|
||||||
|
buffer.write(buffer.get(buffer_size - distance));
|
||||||
|
buffer_size += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer.write(32);
|
||||||
|
buffer.write(char ^ 0x80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MobiFile {
|
||||||
|
view;
|
||||||
|
buffer;
|
||||||
|
offset;
|
||||||
|
header;
|
||||||
|
palm_header;
|
||||||
|
mobi_header;
|
||||||
|
reclist;
|
||||||
|
constructor(data) {
|
||||||
|
this.view = new DataView(data);
|
||||||
|
this.buffer = this.view.buffer;
|
||||||
|
this.offset = 0;
|
||||||
|
this.header = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse() { }
|
||||||
|
|
||||||
|
getUint8() {
|
||||||
|
var v = this.view.getUint8(this.offset);
|
||||||
|
this.offset += 1;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUint16() {
|
||||||
|
var v = this.view.getUint16(this.offset);
|
||||||
|
this.offset += 2;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUint32() {
|
||||||
|
var v = this.view.getUint32(this.offset);
|
||||||
|
this.offset += 4;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStr(size) {
|
||||||
|
var v = ab2str(this.buffer.slice(this.offset, this.offset + size));
|
||||||
|
this.offset += size;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
skip(size) {
|
||||||
|
this.offset += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
setoffset(_of) {
|
||||||
|
this.offset = _of;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_record_extrasize(data, flags) {
|
||||||
|
var pos = data.length - 1;
|
||||||
|
var extra = 0;
|
||||||
|
for (var i = 15; i > 0; i--) {
|
||||||
|
if (flags & (1 << i)) {
|
||||||
|
var res = this.buffer_get_varlen(data, pos);
|
||||||
|
var size = res[0];
|
||||||
|
var l = res[1];
|
||||||
|
pos = res[2];
|
||||||
|
pos -= size - l;
|
||||||
|
extra += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags & 1) {
|
||||||
|
var a = data[pos];
|
||||||
|
extra += (a & 0x3) + 1;
|
||||||
|
}
|
||||||
|
return extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
// data should be uint8array
|
||||||
|
buffer_get_varlen(data, pos) {
|
||||||
|
var l = 0;
|
||||||
|
var size = 0;
|
||||||
|
var byte_count = 0;
|
||||||
|
var mask = 0x7f;
|
||||||
|
var stop_flag = 0x80;
|
||||||
|
var shift = 0;
|
||||||
|
for (var i = 0; ; i++) {
|
||||||
|
var byte = data[pos];
|
||||||
|
size |= (byte & mask) << shift;
|
||||||
|
shift += 7;
|
||||||
|
l += 1;
|
||||||
|
byte_count += 1;
|
||||||
|
pos -= 1;
|
||||||
|
|
||||||
|
var to_stop = byte & stop_flag;
|
||||||
|
if (byte_count >= 4 || to_stop > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [size, l, pos];
|
||||||
|
}
|
||||||
|
// 读出文本内容
|
||||||
|
read_text() {
|
||||||
|
var text_end = this.palm_header.record_count;
|
||||||
|
var buffers = [];
|
||||||
|
for (var i = 1; i <= text_end; i++) {
|
||||||
|
buffers.push(this.read_text_record(i));
|
||||||
|
}
|
||||||
|
var all = copagesne_uint8array(buffers);
|
||||||
|
return ab2str(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
read_text_record(i) {
|
||||||
|
var flags = this.mobi_header.extra_flags;
|
||||||
|
var begin = this.reclist[i].offset;
|
||||||
|
var end = this.reclist[i + 1].offset;
|
||||||
|
|
||||||
|
var data = new Uint8Array(this.buffer.slice(begin, end));
|
||||||
|
var ex = this.get_record_extrasize(data, flags);
|
||||||
|
|
||||||
|
data = new Uint8Array(this.buffer.slice(begin, end - ex));
|
||||||
|
if (this.palm_header.compression === 2) {
|
||||||
|
var buffer = uncompression_lz77(data);
|
||||||
|
return buffer.shrink();
|
||||||
|
} else {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 从buffer中读出image
|
||||||
|
read_image(idx) {
|
||||||
|
var first_image_idx = this.mobi_header.first_image_idx;
|
||||||
|
var begin = this.reclist[first_image_idx + idx].offset;
|
||||||
|
var end = this.reclist[first_image_idx + idx + 1].offset;
|
||||||
|
var data = new Uint8Array(this.buffer.slice(begin, end));
|
||||||
|
return new Blob([data.buffer]);
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.header = this.load_pdbheader();
|
||||||
|
this.reclist = this.load_reclist();
|
||||||
|
this.load_record0();
|
||||||
|
}
|
||||||
|
|
||||||
|
load_pdbheader() {
|
||||||
|
var header = {};
|
||||||
|
header.name = this.getStr(32);
|
||||||
|
header.attr = this.getUint16();
|
||||||
|
header.version = this.getUint16();
|
||||||
|
header.ctime = this.getUint32();
|
||||||
|
header.mtime = this.getUint32();
|
||||||
|
header.btime = this.getUint32();
|
||||||
|
header.mod_num = this.getUint32();
|
||||||
|
header.appinfo_offset = this.getUint32();
|
||||||
|
header.sortinfo_offset = this.getUint32();
|
||||||
|
header.type = this.getStr(4);
|
||||||
|
header.creator = this.getStr(4);
|
||||||
|
header.uid = this.getUint32();
|
||||||
|
header.next_rec = this.getUint32();
|
||||||
|
header.record_num = this.getUint16();
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_reclist() {
|
||||||
|
var reclist = [];
|
||||||
|
for (var i = 0; i < this.header.record_num; i++) {
|
||||||
|
var record = {};
|
||||||
|
record.offset = this.getUint32();
|
||||||
|
// TODO(zz) change
|
||||||
|
record.attr = this.getUint32();
|
||||||
|
reclist.push(record);
|
||||||
|
}
|
||||||
|
return reclist;
|
||||||
|
}
|
||||||
|
load_record0() {
|
||||||
|
this.palm_header = this.load_record0_header();
|
||||||
|
this.mobi_header = this.load_mobi_header();
|
||||||
|
}
|
||||||
|
|
||||||
|
load_record0_header() {
|
||||||
|
var p_header = {};
|
||||||
|
var first_record = this.reclist[0];
|
||||||
|
this.setoffset(first_record.offset);
|
||||||
|
|
||||||
|
p_header.compression = this.getUint16();
|
||||||
|
this.skip(2);
|
||||||
|
p_header.text_length = this.getUint32();
|
||||||
|
p_header.record_count = this.getUint16();
|
||||||
|
p_header.record_size = this.getUint16();
|
||||||
|
p_header.encryption_type = this.getUint16();
|
||||||
|
this.skip(2);
|
||||||
|
|
||||||
|
return p_header;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_mobi_header() {
|
||||||
|
var mobi_header = {};
|
||||||
|
|
||||||
|
var start_offset = this.offset;
|
||||||
|
|
||||||
|
mobi_header.identifier = this.getUint32();
|
||||||
|
mobi_header.header_length = this.getUint32();
|
||||||
|
mobi_header.mobi_type = this.getUint32();
|
||||||
|
mobi_header.text_encoding = this.getUint32();
|
||||||
|
mobi_header.uid = this.getUint32();
|
||||||
|
mobi_header.generator_version = this.getUint32();
|
||||||
|
|
||||||
|
this.skip(40);
|
||||||
|
|
||||||
|
mobi_header.first_nonbook_index = this.getUint32();
|
||||||
|
mobi_header.full_name_offset = this.getUint32();
|
||||||
|
mobi_header.full_name_length = this.getUint32();
|
||||||
|
|
||||||
|
mobi_header.language = this.getUint32();
|
||||||
|
mobi_header.input_language = this.getUint32();
|
||||||
|
mobi_header.output_language = this.getUint32();
|
||||||
|
mobi_header.min_version = this.getUint32();
|
||||||
|
mobi_header.first_image_idx = this.getUint32();
|
||||||
|
|
||||||
|
mobi_header.huff_rec_index = this.getUint32();
|
||||||
|
mobi_header.huff_rec_count = this.getUint32();
|
||||||
|
mobi_header.datp_rec_index = this.getUint32();
|
||||||
|
mobi_header.datp_rec_count = this.getUint32();
|
||||||
|
|
||||||
|
mobi_header.exth_flags = this.getUint32();
|
||||||
|
|
||||||
|
this.skip(36);
|
||||||
|
|
||||||
|
mobi_header.drm_offset = this.getUint32();
|
||||||
|
mobi_header.drm_count = this.getUint32();
|
||||||
|
mobi_header.drm_size = this.getUint32();
|
||||||
|
mobi_header.drm_flags = this.getUint32();
|
||||||
|
|
||||||
|
this.skip(8);
|
||||||
|
|
||||||
|
// TODO (zz) fdst_index
|
||||||
|
this.skip(4);
|
||||||
|
|
||||||
|
this.skip(46);
|
||||||
|
|
||||||
|
mobi_header.extra_flags = this.getUint16();
|
||||||
|
|
||||||
|
this.setoffset(start_offset + mobi_header.header_length);
|
||||||
|
|
||||||
|
return mobi_header;
|
||||||
|
}
|
||||||
|
load_exth_header() {
|
||||||
|
// TODO
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
extractContent(s) {
|
||||||
|
var span = document.createElement("span");
|
||||||
|
span.innerHTML = s;
|
||||||
|
return span.textContent || span.innerText;
|
||||||
|
}
|
||||||
|
render(isElectron = false) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.load();
|
||||||
|
var content = this.read_text();
|
||||||
|
var bookDoc = domParser.parseFromString(content, "text/html")
|
||||||
|
.documentElement;
|
||||||
|
let lines = Array.from(
|
||||||
|
bookDoc.querySelectorAll("p,b,font,h3,h2,h1")
|
||||||
|
);
|
||||||
|
let parseContent = [];
|
||||||
|
for (let i = 0, len = lines.length; i < len - 1; i++) {
|
||||||
|
lines[i].innerText &&
|
||||||
|
lines[i].innerText !== parseContent[parseContent.length - 1] &&
|
||||||
|
parseContent.push(lines[i].innerText);
|
||||||
|
let imgDoms = lines[i].getElementsByTagName("img");
|
||||||
|
if (imgDoms.length > 0) {
|
||||||
|
for (let i = 0; i < imgDoms.length; i++) {
|
||||||
|
parseContent.push("#image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleImage = async () => {
|
||||||
|
var imgDoms = bookDoc.getElementsByTagName("img");
|
||||||
|
parseContent.push("~image");
|
||||||
|
for (let i = 0; i < imgDoms.length; i++) {
|
||||||
|
const src = await this.render_image(imgDoms, i);
|
||||||
|
parseContent.push(
|
||||||
|
src + " " + imgDoms[i].width + " " + imgDoms[i].height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (imgDoms.length > 200 || !isElectron) {
|
||||||
|
resolve(bookDoc);
|
||||||
|
} else {
|
||||||
|
resolve(parseContent.join("\n \n"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleImage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
render_image = (imgDoms, i) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
var imgDom = imgDoms[i];
|
||||||
|
var idx = +imgDom.getAttribute("recindex");
|
||||||
|
var blob = this.read_image(idx - 1);
|
||||||
|
var imgReader = new FileReader();
|
||||||
|
imgReader.onload = (e) => {
|
||||||
|
imgDom.src = e.target?.result;
|
||||||
|
resolve(e.target?.result);
|
||||||
|
};
|
||||||
|
imgReader.onerror = function (err) {
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
imgReader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MobiFile;
|
|
@ -24,6 +24,9 @@
|
||||||
.material-icons.text-icon {
|
.material-icons.text-icon {
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
}
|
}
|
||||||
|
.material-icons.text-lg {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
.material-icons.text-base {
|
.material-icons.text-base {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,17 @@
|
||||||
<!-- <button class="mx-1" @click="editAudiobook(ab)">
|
<!-- <button class="mx-1" @click="editAudiobook(ab)">
|
||||||
<span class="material-icons text-icon pb-px">edit</span>
|
<span class="material-icons text-icon pb-px">edit</span>
|
||||||
</button> -->
|
</button> -->
|
||||||
<button v-if="!isPlaying" class="mx-1 rounded-full w-6 h-6" @click="playAudiobook">
|
<button v-if="showRead" class="mx-1 rounded-full w-6 h-6" @click="readBook">
|
||||||
|
<span class="material-icons">auto_stories</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="showPlay" class="mx-1 rounded-full w-6 h-6" @click="playAudiobook">
|
||||||
<span class="material-icons">play_arrow</span>
|
<span class="material-icons">play_arrow</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="audiobook.book.subtitle" class="text-gray-200 leading-6 truncate" style="font-size: 0.9rem">{{ audiobook.book.subtitle }}</p>
|
<p v-if="audiobook.book.subtitle" class="text-gray-200 leading-6 truncate" style="font-size: 0.9rem">{{ audiobook.book.subtitle }}</p>
|
||||||
<p class="text-sm text-gray-200">by {{ audiobook.book.author }}</p>
|
<p class="text-sm text-gray-200">by {{ audiobook.book.author }}</p>
|
||||||
<div class="flex items-center py-1">
|
<div v-if="numTracks" class="flex items-center py-1">
|
||||||
<p class="text-xs text-gray-300">{{ $elapsedPretty(audiobook.duration) }}</p>
|
<p class="text-xs text-gray-300">{{ $elapsedPretty(audiobook.duration) }}</p>
|
||||||
<span class="px-3 text-xs text-gray-300">•</span>
|
<span class="px-3 text-xs text-gray-300">•</span>
|
||||||
<p class="text-xs text-gray-300 font-mono">{{ $bytesPretty(audiobook.size, 0) }}</p>
|
<p class="text-xs text-gray-300 font-mono">{{ $bytesPretty(audiobook.size, 0) }}</p>
|
||||||
|
@ -38,6 +41,9 @@
|
||||||
<div v-if="isPlaying" class="w-min my-1 mx-1">
|
<div v-if="isPlaying" class="w-min my-1 mx-1">
|
||||||
<div class="bg-info bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">{{ isStreaming ? 'Streaming' : 'Playing' }}</div>
|
<div class="bg-info bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">{{ isStreaming ? 'Streaming' : 'Playing' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="hasEbook" class="w-min my-1 mx-1">
|
||||||
|
<div class="bg-bg bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">{{ ebookFormat }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,12 +102,34 @@ export default {
|
||||||
isPlaying() {
|
isPlaying() {
|
||||||
return this.$store.getters['isAudiobookPlaying'](this.audiobookId)
|
return this.$store.getters['isAudiobookPlaying'](this.audiobookId)
|
||||||
},
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.audiobook.isMissing
|
||||||
|
},
|
||||||
|
isIncomplete() {
|
||||||
|
return this.audiobook.isIncomplete
|
||||||
|
},
|
||||||
numTracks() {
|
numTracks() {
|
||||||
if (this.audiobook.tracks) return this.audiobook.tracks.length
|
if (this.audiobook.tracks) return this.audiobook.tracks.length
|
||||||
return this.audiobook.numTracks || 0
|
return this.audiobook.numTracks || 0
|
||||||
|
},
|
||||||
|
showPlay() {
|
||||||
|
return !this.isPlaying && !this.isMissing && !this.isIncomplete && this.numTracks
|
||||||
|
},
|
||||||
|
showRead() {
|
||||||
|
return this.hasEbook && this.ebookFormat !== '.pdf'
|
||||||
|
},
|
||||||
|
hasEbook() {
|
||||||
|
return this.audiobook.numEbooks
|
||||||
|
},
|
||||||
|
ebookFormat() {
|
||||||
|
if (!this.audiobook || !this.audiobook.ebooks || !this.audiobook.ebooks.length) return null
|
||||||
|
return this.audiobook.ebooks[0].ext.substr(1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
readBook() {
|
||||||
|
this.$store.commit('openReader', this.audiobook)
|
||||||
|
},
|
||||||
playAudiobook() {
|
playAudiobook() {
|
||||||
if (this.isPlaying) {
|
if (this.isPlaying) {
|
||||||
return
|
return
|
||||||
|
|
226
components/readers/ComicReader.vue
Normal file
226
components/readers/ComicReader.vue
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
<template>
|
||||||
|
<div id="comic-reader" class="w-full h-full">
|
||||||
|
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute right-20 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52" style="top: 72px">
|
||||||
|
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index)">
|
||||||
|
<p class="text-sm truncate">{{ file }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-20 right-0 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-full" style="top: 72px">
|
||||||
|
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
||||||
|
<p class="text-xs">
|
||||||
|
<strong>{{ key }}</strong
|
||||||
|
>: {{ comicMetadata[key] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="comicMetadata" class="absolute top-8 right-36 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showInfoMenu = !showInfoMenu">
|
||||||
|
<span class="material-icons text-lg">more</span>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" style="right: 92px" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
|
||||||
|
<span class="material-icons text-lg">menu</span>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-8 right-4 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
|
||||||
|
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-hidden m-auto comicwrapper relative">
|
||||||
|
<div class="h-full flex justify-center">
|
||||||
|
<img v-if="mainImg" :src="mainImg" class="object-contain comicimg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Path from 'path'
|
||||||
|
import { Archive } from 'libarchive.js/main.js'
|
||||||
|
|
||||||
|
Archive.init({
|
||||||
|
workerUrl: '/libarchive/worker-bundle.js'
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
pages: null,
|
||||||
|
filesObject: null,
|
||||||
|
mainImg: null,
|
||||||
|
page: 0,
|
||||||
|
numPages: 0,
|
||||||
|
showPageMenu: false,
|
||||||
|
showInfoMenu: false,
|
||||||
|
loadTimeout: null,
|
||||||
|
loadedFirstPage: false,
|
||||||
|
comicMetadata: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
url: {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.extract()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
comicMetadataKeys() {
|
||||||
|
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
|
||||||
|
},
|
||||||
|
canGoNext() {
|
||||||
|
return this.page < this.numPages - 1
|
||||||
|
},
|
||||||
|
canGoPrev() {
|
||||||
|
return this.page > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickOutside() {
|
||||||
|
if (this.showPageMenu) this.showPageMenu = false
|
||||||
|
if (this.showInfoMenu) this.showInfoMenu = false
|
||||||
|
},
|
||||||
|
next() {
|
||||||
|
if (!this.canGoNext) return
|
||||||
|
this.setPage(this.page + 1)
|
||||||
|
},
|
||||||
|
prev() {
|
||||||
|
if (!this.canGoPrev) return
|
||||||
|
this.setPage(this.page - 1)
|
||||||
|
},
|
||||||
|
setPage(index) {
|
||||||
|
if (index < 0 || index > this.numPages - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var filename = this.pages[index]
|
||||||
|
this.page = index
|
||||||
|
return this.extractFile(filename)
|
||||||
|
},
|
||||||
|
setLoadTimeout() {
|
||||||
|
this.loadTimeout = setTimeout(() => {
|
||||||
|
this.loading = true
|
||||||
|
}, 150)
|
||||||
|
},
|
||||||
|
extractFile(filename) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
this.setLoadTimeout()
|
||||||
|
var file = await this.filesObject[filename].extract()
|
||||||
|
var reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.mainImg = e.target.result
|
||||||
|
this.loading = false
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
reader.onerror = (e) => {
|
||||||
|
console.error(e)
|
||||||
|
this.$toast.error('Read page file failed')
|
||||||
|
this.loading = false
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
clearTimeout(this.loadTimeout)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async extract() {
|
||||||
|
this.loading = true
|
||||||
|
console.log('Extracting', this.url)
|
||||||
|
|
||||||
|
var buff = await this.$axios.$get(this.url, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
const archive = await Archive.open(buff)
|
||||||
|
this.filesObject = await archive.getFilesObject()
|
||||||
|
var filenames = Object.keys(this.filesObject)
|
||||||
|
this.parseFilenames(filenames)
|
||||||
|
|
||||||
|
var xmlFile = filenames.find((f) => (Path.extname(f) || '').toLowerCase() === '.xml')
|
||||||
|
if (xmlFile) await this.extractXmlFile(xmlFile)
|
||||||
|
|
||||||
|
this.numPages = this.pages.length
|
||||||
|
|
||||||
|
if (this.pages.length) {
|
||||||
|
this.loading = false
|
||||||
|
await this.setPage(0)
|
||||||
|
this.loadedFirstPage = true
|
||||||
|
} else {
|
||||||
|
this.$toast.error('Unable to extract pages')
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async extractXmlFile(filename) {
|
||||||
|
console.log('extracting xml filename', filename)
|
||||||
|
try {
|
||||||
|
var file = await this.filesObject[filename].extract()
|
||||||
|
var reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.comicMetadata = this.$xmlToJson(e.target.result)
|
||||||
|
console.log('Metadata', this.comicMetadata)
|
||||||
|
}
|
||||||
|
reader.onerror = (e) => {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseImageFilename(filename) {
|
||||||
|
var basename = Path.basename(filename, Path.extname(filename))
|
||||||
|
var numbersinpath = basename.match(/\d{1,4}/g)
|
||||||
|
if (!numbersinpath || !numbersinpath.length) {
|
||||||
|
return {
|
||||||
|
index: -1,
|
||||||
|
filename
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
index: Number(numbersinpath[numbersinpath.length - 1]),
|
||||||
|
filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseFilenames(filenames) {
|
||||||
|
const acceptableImages = ['.jpeg', '.jpg', '.png']
|
||||||
|
var imageFiles = filenames.filter((f) => {
|
||||||
|
return acceptableImages.includes((Path.extname(f) || '').toLowerCase())
|
||||||
|
})
|
||||||
|
var imageFileObjs = imageFiles.map((img) => {
|
||||||
|
return this.parseImageFilename(img)
|
||||||
|
})
|
||||||
|
|
||||||
|
var imagesWithNum = imageFileObjs.filter((i) => i.index >= 0)
|
||||||
|
var orderedImages = imagesWithNum.sort((a, b) => a.index - b.index).map((i) => i.filename)
|
||||||
|
var noNumImages = imageFileObjs.filter((i) => i.index < 0)
|
||||||
|
orderedImages = orderedImages.concat(noNumImages.map((i) => i.filename))
|
||||||
|
|
||||||
|
this.pages = orderedImages
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#comic-reader {
|
||||||
|
height: calc(100vh - 32px);
|
||||||
|
}
|
||||||
|
.pagemenu {
|
||||||
|
max-height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
.comicimg {
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.comicwrapper {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
130
components/readers/EpubReader.vue
Normal file
130
components/readers/EpubReader.vue
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
<template>
|
||||||
|
<div id="epub-frame" class="w-full">
|
||||||
|
<div id="viewer" class="border border-gray-100 bg-white shadow-md h-full w-full"></div>
|
||||||
|
<div class="fixed bottom-0 left-0 h-8 w-full bg-bg px-2 flex items-center">
|
||||||
|
<p class="text-xs">epub</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<p class="text-sm">{{ progress }}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ePub from 'epubjs'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
book: null,
|
||||||
|
rendition: null,
|
||||||
|
chapters: [],
|
||||||
|
title: '',
|
||||||
|
author: '',
|
||||||
|
progress: 0,
|
||||||
|
hasNext: true,
|
||||||
|
hasPrev: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
prev() {
|
||||||
|
if (this.rendition) {
|
||||||
|
this.rendition.prev()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
next() {
|
||||||
|
if (this.rendition) {
|
||||||
|
this.rendition.next()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyUp() {
|
||||||
|
if ((e.keyCode || e.which) == 37) {
|
||||||
|
this.prev()
|
||||||
|
} else if ((e.keyCode || e.which) == 39) {
|
||||||
|
this.next()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initEpub() {
|
||||||
|
var book = ePub(this.url)
|
||||||
|
this.book = book
|
||||||
|
|
||||||
|
this.rendition = book.renderTo('viewer', {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight - 64,
|
||||||
|
snap: true,
|
||||||
|
manager: 'continuous',
|
||||||
|
flow: 'paginated'
|
||||||
|
})
|
||||||
|
var displayed = this.rendition.display()
|
||||||
|
|
||||||
|
book.ready
|
||||||
|
.then(() => {
|
||||||
|
console.log('Book ready')
|
||||||
|
return book.locations.generate(1600)
|
||||||
|
})
|
||||||
|
.then((locations) => {
|
||||||
|
// console.log('Loaded locations', locations)
|
||||||
|
// Wait for book to be rendered to get current page
|
||||||
|
displayed.then(() => {
|
||||||
|
// Get the current CFI
|
||||||
|
var currentLocation = this.rendition.currentLocation()
|
||||||
|
if (!currentLocation.start) {
|
||||||
|
console.error('No Start', currentLocation)
|
||||||
|
} else {
|
||||||
|
var currentPage = book.locations.percentageFromCfi(currentLocation.start.cfi)
|
||||||
|
// console.log('current page', currentPage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
book.loaded.navigation.then((toc) => {
|
||||||
|
var _chapters = []
|
||||||
|
toc.forEach((chapter) => {
|
||||||
|
_chapters.push(chapter)
|
||||||
|
})
|
||||||
|
this.chapters = _chapters
|
||||||
|
})
|
||||||
|
book.loaded.metadata.then((metadata) => {
|
||||||
|
// this.author = metadata.creator
|
||||||
|
// this.title = metadata.title
|
||||||
|
})
|
||||||
|
|
||||||
|
// const spine_get = book.spine.get.bind(book.spine)
|
||||||
|
// book.spine.get = function (target) {
|
||||||
|
// var t = spine_get(target)
|
||||||
|
// console.log(t, target)
|
||||||
|
// // while (t == null && target.includes('#')) {
|
||||||
|
// // target = target.split('#')[0]
|
||||||
|
// // t = spine_get(target)
|
||||||
|
// // }
|
||||||
|
// return t
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.rendition.on('keyup', this.keyUp)
|
||||||
|
|
||||||
|
this.rendition.on('relocated', (location) => {
|
||||||
|
var percent = book.locations.percentageFromCfi(location.start.cfi)
|
||||||
|
this.progress = Math.floor(percent * 100)
|
||||||
|
|
||||||
|
this.hasNext = !location.atEnd
|
||||||
|
this.hasPrev = !location.atStart
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initEpub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#epub-frame {
|
||||||
|
height: calc(100% - 32px);
|
||||||
|
max-height: calc(100% - 32px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
120
components/readers/MobiReader.vue
Normal file
120
components/readers/MobiReader.vue
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<template>
|
||||||
|
<div class="ebook-viewer w-full h-full">
|
||||||
|
<div class="absolute overflow-y-scroll left-0 right-0 w-full max-w-screen m-auto z-10 border border-black border-opacity-20 shadow-md bg-white">
|
||||||
|
<iframe title="html-viewer" class="w-screen"> Loading </iframe>
|
||||||
|
</div>
|
||||||
|
<div class="fixed bottom-0 left-0 h-8 w-full bg-bg px-2 flex items-center z-20">
|
||||||
|
<p class="text-xs">mobi</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MobiParser from '@/assets/ebooks/mobi.js'
|
||||||
|
import HtmlParser from '@/assets/ebooks/htmlParser.js'
|
||||||
|
import defaultCss from '@/assets/ebooks/basic.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
addHtmlCss() {
|
||||||
|
let iframe = document.getElementsByTagName('iframe')[0]
|
||||||
|
if (!iframe) return
|
||||||
|
let doc = iframe.contentDocument
|
||||||
|
if (!doc) return
|
||||||
|
let style = doc.createElement('style')
|
||||||
|
style.id = 'default-style'
|
||||||
|
style.textContent = defaultCss
|
||||||
|
doc.head.appendChild(style)
|
||||||
|
},
|
||||||
|
handleIFrameHeight(iFrame) {
|
||||||
|
const isElement = (obj) => !!(obj && obj.nodeType === 1)
|
||||||
|
|
||||||
|
var body = iFrame.contentWindow.document.body,
|
||||||
|
html = iFrame.contentWindow.document.documentElement
|
||||||
|
iFrame.height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight) * 2
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
let lastchild = body.lastElementChild
|
||||||
|
let lastEle = body.lastChild
|
||||||
|
|
||||||
|
let itemAs = body.querySelectorAll('a')
|
||||||
|
let itemPs = body.querySelectorAll('p')
|
||||||
|
let lastItemA = itemAs[itemAs.length - 1]
|
||||||
|
let lastItemP = itemPs[itemPs.length - 1]
|
||||||
|
let lastItem
|
||||||
|
if (isElement(lastItemA) && isElement(lastItemP)) {
|
||||||
|
if (lastItemA.clientHeight + lastItemA.offsetTop > lastItemP.clientHeight + lastItemP.offsetTop) {
|
||||||
|
lastItem = lastItemA
|
||||||
|
} else {
|
||||||
|
lastItem = lastItemP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastchild && !lastItem && !lastEle) return
|
||||||
|
if (lastEle.nodeType === 3 && !lastchild && !lastItem) return
|
||||||
|
|
||||||
|
let nodeHeight = 0
|
||||||
|
if (lastEle.nodeType === 3 && document.createRange) {
|
||||||
|
let range = document.createRange()
|
||||||
|
range.selectNodeContents(lastEle)
|
||||||
|
if (range.getBoundingClientRect) {
|
||||||
|
let rect = range.getBoundingClientRect()
|
||||||
|
if (rect) {
|
||||||
|
nodeHeight = rect.bottom - rect.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var lastChildHeight = isElement(lastchild) ? lastchild.clientHeight + lastchild.offsetTop : 0
|
||||||
|
var lastEleHeight = isElement(lastEle) ? lastEle.clientHeight + lastEle.offsetTop : 0
|
||||||
|
var lastItemHeight = isElement(lastItem) ? lastItem.clientHeight + lastItem.offsetTop : 0
|
||||||
|
iFrame.height = Math.max(lastChildHeight, lastEleHeight, lastItemHeight) + 100 + nodeHeight
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
async initMobi() {
|
||||||
|
// Fetch mobi file as blob
|
||||||
|
var buff = await this.$axios.$get(this.url, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
var reader = new FileReader()
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
var file_content = event.target.result
|
||||||
|
|
||||||
|
let mobiFile = new MobiParser(file_content)
|
||||||
|
|
||||||
|
let content = await mobiFile.render()
|
||||||
|
let htmlParser = new HtmlParser(new DOMParser().parseFromString(content.outerHTML, 'text/html'))
|
||||||
|
var anchoredDoc = htmlParser.getAnchoredDoc()
|
||||||
|
|
||||||
|
let iFrame = document.getElementsByTagName('iframe')[0]
|
||||||
|
iFrame.contentDocument.body.innerHTML = anchoredDoc.documentElement.outerHTML
|
||||||
|
|
||||||
|
// Add css
|
||||||
|
let style = iFrame.contentDocument.createElement('style')
|
||||||
|
style.id = 'default-style'
|
||||||
|
style.textContent = defaultCss
|
||||||
|
iFrame.contentDocument.head.appendChild(style)
|
||||||
|
|
||||||
|
this.handleIFrameHeight(iFrame)
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(buff)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initMobi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ebook-viewer {
|
||||||
|
height: calc(100% - 32px);
|
||||||
|
}
|
||||||
|
</style>
|
130
components/readers/Reader.vue
Normal file
130
components/readers/Reader.vue
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="show" class="absolute top-0 left-0 w-full h-full bg-bg z-40 pt-8">
|
||||||
|
<div class="h-8 w-full bg-primary flex items-center px-2 fixed top-0 left-0 z-30 box-shadow-sm">
|
||||||
|
<p class="w-5/6 truncate">{{ title }}</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<span class="material-icons text-xl text-white" @click.stop="show = false">close</span>
|
||||||
|
</div>
|
||||||
|
<component v-if="readerComponentName" ref="readerComponent" :is="readerComponentName" :url="ebookUrl" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ebookType: null,
|
||||||
|
ebookUrl: null,
|
||||||
|
touchstartX: 0,
|
||||||
|
touchendX: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
this.registerListeners()
|
||||||
|
} else {
|
||||||
|
this.unregisterListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.showReader
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('setShowReader', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.selectedBook ? this.selectedBook.book.title : null
|
||||||
|
},
|
||||||
|
selectedBook() {
|
||||||
|
return this.$store.state.selectedBook
|
||||||
|
},
|
||||||
|
readerComponentName() {
|
||||||
|
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||||
|
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
||||||
|
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
ebook() {
|
||||||
|
if (!this.selectedBook || !this.selectedBook.ebooks || !this.selectedBook.ebooks.length) return null
|
||||||
|
return this.selectedBook.ebooks[0]
|
||||||
|
},
|
||||||
|
ebookPath() {
|
||||||
|
return this.ebook ? this.ebook.path : null
|
||||||
|
},
|
||||||
|
folderId() {
|
||||||
|
return this.selectedBook ? this.selectedBook.folderId : null
|
||||||
|
},
|
||||||
|
libraryId() {
|
||||||
|
return this.selectedBook ? this.selectedBook.libraryId : null
|
||||||
|
},
|
||||||
|
ebookRelPath() {
|
||||||
|
return `/ebook/${this.libraryId}/${this.folderId}/${this.ebookPath}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
if (!this.ebook) {
|
||||||
|
console.error('No ebook for book', this.selectedBook)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.ebook.ext === '.epub') {
|
||||||
|
this.ebookType = 'epub'
|
||||||
|
} else if (this.ebook.ext === '.mobi' || this.ebook.ext === '.azw3') {
|
||||||
|
this.ebookType = 'mobi'
|
||||||
|
} else if (this.ebook.ext === '.cbr' || this.ebook.ext === '.cbz') {
|
||||||
|
this.ebookType = 'comic'
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverUrl = this.$store.state.serverUrl
|
||||||
|
this.ebookUrl = `${serverUrl}${this.ebookRelPath}`
|
||||||
|
},
|
||||||
|
next() {
|
||||||
|
if (this.$refs.readerComponent && this.$refs.readerComponent.next) {
|
||||||
|
this.$refs.readerComponent.next()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prev() {
|
||||||
|
if (this.$refs.readerComponent && this.$refs.readerComponent.prev) {
|
||||||
|
this.$refs.readerComponent.prev()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleGesture() {
|
||||||
|
if (this.touchendX < this.touchstartX) {
|
||||||
|
console.log('swiped left')
|
||||||
|
|
||||||
|
this.next()
|
||||||
|
}
|
||||||
|
if (this.touchendX > this.touchstartX) {
|
||||||
|
console.log('swiped right')
|
||||||
|
this.prev()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
touchstart(e) {
|
||||||
|
this.touchstartX = e.changedTouches[0].screenX
|
||||||
|
},
|
||||||
|
touchend(e) {
|
||||||
|
this.touchendX = e.changedTouches[0].screenX
|
||||||
|
this.handleGesture()
|
||||||
|
},
|
||||||
|
registerListeners() {
|
||||||
|
document.body.addEventListener('touchstart', this.touchstart)
|
||||||
|
document.body.addEventListener('touchend', this.touchend)
|
||||||
|
},
|
||||||
|
unregisterListeners() {
|
||||||
|
document.body.removeEventListener('touchstart', this.touchstart)
|
||||||
|
document.body.removeEventListener('touchend', this.touchend)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.unregisterListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
70
components/ui/LoadingIndicator.vue
Normal file
70
components/ui/LoadingIndicator.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-40">
|
||||||
|
<div class="bg-bg border border-gray-500 py-2 px-5 rounded-lg flex items-center flex-col box-shadow-md">
|
||||||
|
<div class="loader-dots block relative w-20 h-5 mt-2">
|
||||||
|
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
|
||||||
|
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
|
||||||
|
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
|
||||||
|
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: 'Please Wait...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.loader-dots div {
|
||||||
|
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||||
|
}
|
||||||
|
.loader-dots div:nth-child(1) {
|
||||||
|
left: 8px;
|
||||||
|
animation: loader-dots1 0.6s infinite;
|
||||||
|
}
|
||||||
|
.loader-dots div:nth-child(2) {
|
||||||
|
left: 8px;
|
||||||
|
animation: loader-dots2 0.6s infinite;
|
||||||
|
}
|
||||||
|
.loader-dots div:nth-child(3) {
|
||||||
|
left: 32px;
|
||||||
|
animation: loader-dots2 0.6s infinite;
|
||||||
|
}
|
||||||
|
.loader-dots div:nth-child(4) {
|
||||||
|
left: 56px;
|
||||||
|
animation: loader-dots3 0.6s infinite;
|
||||||
|
}
|
||||||
|
@keyframes loader-dots1 {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes loader-dots3 {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes loader-dots2 {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(24px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,6 +7,7 @@
|
||||||
<app-stream-container ref="streamContainer" />
|
<app-stream-container ref="streamContainer" />
|
||||||
<modals-downloads-modal ref="downloadsModal" @selectDownload="selectDownload" @deleteDownload="deleteDownload" />
|
<modals-downloads-modal ref="downloadsModal" @selectDownload="selectDownload" @deleteDownload="deleteDownload" />
|
||||||
<modals-libraries-modal />
|
<modals-libraries-modal />
|
||||||
|
<readers-reader />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
146
package-lock.json
generated
146
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.4.0-beta",
|
"version": "v0.9.6-beta",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -2711,6 +2711,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
||||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
|
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
|
||||||
},
|
},
|
||||||
|
"@types/localforage": {
|
||||||
|
"version": "0.0.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz",
|
||||||
|
"integrity": "sha1-XjHDLdh5HsS5/z70fJy1Wy0NlDg=",
|
||||||
|
"requires": {
|
||||||
|
"localforage": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "16.4.12",
|
"version": "16.4.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.12.tgz",
|
||||||
|
@ -5134,6 +5142,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
|
||||||
"integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk="
|
"integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk="
|
||||||
},
|
},
|
||||||
|
"d": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
|
||||||
|
"requires": {
|
||||||
|
"es5-ext": "^0.10.50",
|
||||||
|
"type": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"de-indent": {
|
"de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
@ -5564,6 +5581,29 @@
|
||||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="
|
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="
|
||||||
},
|
},
|
||||||
|
"epubjs": {
|
||||||
|
"version": "0.3.88",
|
||||||
|
"resolved": "https://registry.npmjs.org/epubjs/-/epubjs-0.3.88.tgz",
|
||||||
|
"integrity": "sha512-VRumULpUELYmYwzypyfbDwoSIqDp2LXOXCtY3o55o3YDW5Zm32UjtZuX/xaWFGqyZORNNMWWQ8VlMaY1djnDYg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/localforage": "0.0.34",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"event-emitter": "^0.3.5",
|
||||||
|
"jszip": "^3.4.0",
|
||||||
|
"localforage": "^1.7.3",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"marks-pane": "^1.0.9",
|
||||||
|
"path-webpack": "0.0.3",
|
||||||
|
"xmldom": "^0.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"xmldom": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-z9s6k3wxE+aZHgXYxSTpGDo7BYOUfJsIRyoZiX6HTjwpwfS2wpQBQKa2fD+ShLyPkqDYo5ud7KitmLZ2Cd6r0g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"errno": {
|
"errno": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
|
||||||
|
@ -5622,12 +5662,41 @@
|
||||||
"is-symbol": "^1.0.2"
|
"is-symbol": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"es5-ext": {
|
||||||
|
"version": "0.10.53",
|
||||||
|
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
|
||||||
|
"integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
|
||||||
|
"requires": {
|
||||||
|
"es6-iterator": "~2.0.3",
|
||||||
|
"es6-symbol": "~3.1.3",
|
||||||
|
"next-tick": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"es6-error": {
|
"es6-error": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"es6-iterator": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||||
|
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
|
||||||
|
"requires": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "^0.10.35",
|
||||||
|
"es6-symbol": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es6-symbol": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
|
||||||
|
"requires": {
|
||||||
|
"d": "^1.0.1",
|
||||||
|
"ext": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"escalade": {
|
"escalade": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||||
|
@ -5687,6 +5756,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
||||||
},
|
},
|
||||||
|
"event-emitter": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||||
|
"integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
|
||||||
|
"requires": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "~0.10.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"eventemitter3": {
|
"eventemitter3": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
@ -5777,6 +5855,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ext": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==",
|
||||||
|
"requires": {
|
||||||
|
"type": "^2.5.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"type": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"extend-shallow": {
|
"extend-shallow": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
|
||||||
|
@ -7438,6 +7531,27 @@
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jszip": {
|
||||||
|
"version": "3.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
|
||||||
|
"integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
|
||||||
|
"requires": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"set-immediate-shim": "~1.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"requires": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"keygrip": {
|
"keygrip": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
|
||||||
|
@ -7610,6 +7724,11 @@
|
||||||
"launch-editor": "^2.2.1"
|
"launch-editor": "^2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"libarchive.js": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/libarchive.js/-/libarchive.js-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg=="
|
||||||
|
},
|
||||||
"lie": {
|
"lie": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||||
|
@ -7780,6 +7899,11 @@
|
||||||
"object-visit": "^1.0.0"
|
"object-visit": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"marks-pane": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/marks-pane/-/marks-pane-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg=="
|
||||||
|
},
|
||||||
"matcher": {
|
"matcher": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||||
|
@ -8147,6 +8271,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||||
},
|
},
|
||||||
|
"next-tick": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
|
||||||
|
},
|
||||||
"no-case": {
|
"no-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||||
|
@ -8775,6 +8904,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
|
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
|
||||||
},
|
},
|
||||||
|
"path-webpack": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-webpack/-/path-webpack-0.0.3.tgz",
|
||||||
|
"integrity": "sha1-/23sdJ7sWpRgXATV9j/FVgegOhY="
|
||||||
|
},
|
||||||
"pbkdf2": {
|
"pbkdf2": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
|
||||||
|
@ -12202,6 +12336,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
|
||||||
"integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0="
|
"integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0="
|
||||||
},
|
},
|
||||||
|
"set-immediate-shim": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
|
||||||
|
},
|
||||||
"set-value": {
|
"set-value": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
||||||
|
@ -13364,6 +13503,11 @@
|
||||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"type": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
|
||||||
|
},
|
||||||
"type-fest": {
|
"type-fest": {
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.9.6-beta",
|
"version": "v0.9.7-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
@ -23,7 +23,9 @@
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"capacitor-data-storage-sqlite": "^3.2.0",
|
"capacitor-data-storage-sqlite": "^3.2.0",
|
||||||
"core-js": "^3.15.1",
|
"core-js": "^3.15.1",
|
||||||
|
"epubjs": "^0.3.88",
|
||||||
"hls.js": "^1.0.9",
|
"hls.js": "^1.0.9",
|
||||||
|
"libarchive.js": "^1.3.0",
|
||||||
"nuxt": "^2.15.7",
|
"nuxt": "^2.15.7",
|
||||||
"socket.io-client": "^4.1.3",
|
"socket.io-client": "^4.1.3",
|
||||||
"vue-toastification": "^1.7.11"
|
"vue-toastification": "^1.7.11"
|
||||||
|
|
|
@ -7,14 +7,14 @@
|
||||||
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :style="{ width: 128 * progressPercent + 'px' }"></div>
|
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :style="{ width: 128 * progressPercent + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex my-4">
|
<div class="flex my-4">
|
||||||
<p class="text-sm">{{ numTracks }} Tracks</p>
|
<p v-if="numTracks" class="text-sm">{{ numTracks }} Tracks</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-3">
|
<div class="flex-grow px-3">
|
||||||
<h1 class="text-lg">{{ title }}</h1>
|
<h1 class="text-lg">{{ title }}</h1>
|
||||||
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
||||||
<p class="text-sm text-gray-400">by {{ author }}</p>
|
<p class="text-sm text-gray-400">by {{ author }}</p>
|
||||||
<p class="text-gray-300 text-sm my-1">
|
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
||||||
{{ $elapsedPretty(duration) }}<span class="px-4">{{ $bytesPretty(size) }}</span>
|
{{ $elapsedPretty(duration) }}<span class="px-4">{{ $bytesPretty(size) }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -26,13 +26,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isConnected || isDownloadPlayable" class="flex mt-4">
|
<div v-if="(isConnected && (showPlay || showRead)) || isDownloadPlayable" class="flex mt-4 -mr-2">
|
||||||
<ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center w-full mr-2" :padding-x="4" @click="playClick">
|
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
||||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||||
<span class="px-1">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isDownloadPlayable ? 'Play local' : 'Play stream' }}</span>
|
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isDownloadPlayable ? 'Play local' : 'Play stream' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-if="isConnected" color="primary" :disabled="isPlaying" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
<ui-btn v-if="showRead && isConnected" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
||||||
<span class="material-icons" :class="isDownloaded ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
|
<span class="material-icons">auto_stories</span>
|
||||||
|
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||||
|
</ui-btn>
|
||||||
|
<ui-btn v-if="isConnected && showPlay" color="primary" :disabled="isPlaying" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
||||||
|
<span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -150,9 +154,28 @@ export default {
|
||||||
if (this.audiobook.tracks) return this.audiobook.tracks.length
|
if (this.audiobook.tracks) return this.audiobook.tracks.length
|
||||||
return this.audiobook.numTracks || 0
|
return this.audiobook.numTracks || 0
|
||||||
},
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.audiobook.isMissing
|
||||||
|
},
|
||||||
|
isIncomplete() {
|
||||||
|
return this.audiobook.isIncomplete
|
||||||
|
},
|
||||||
isDownloading() {
|
isDownloading() {
|
||||||
return this.downloadObj ? this.downloadObj.isDownloading : false
|
return this.downloadObj ? this.downloadObj.isDownloading : false
|
||||||
},
|
},
|
||||||
|
showPlay() {
|
||||||
|
return !this.isMissing && !this.isIncomplete && this.numTracks
|
||||||
|
},
|
||||||
|
showRead() {
|
||||||
|
return this.hasEbook && this.ebookFormat !== '.pdf'
|
||||||
|
},
|
||||||
|
hasEbook() {
|
||||||
|
return this.audiobook.numEbooks
|
||||||
|
},
|
||||||
|
ebookFormat() {
|
||||||
|
if (!this.audiobook || !this.audiobook.ebooks || !this.audiobook.ebooks.length) return null
|
||||||
|
return this.audiobook.ebooks[0].ext.substr(1)
|
||||||
|
},
|
||||||
isDownloadPreparing() {
|
isDownloadPreparing() {
|
||||||
return this.downloadObj ? this.downloadObj.isPreparing : false
|
return this.downloadObj ? this.downloadObj.isPreparing : false
|
||||||
},
|
},
|
||||||
|
@ -170,6 +193,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
readBook() {
|
||||||
|
this.$store.commit('openReader', this.audiobook)
|
||||||
|
},
|
||||||
playClick() {
|
playClick() {
|
||||||
this.$store.commit('setPlayOnLoad', true)
|
this.$store.commit('setPlayOnLoad', true)
|
||||||
if (!this.isDownloadPlayable) {
|
if (!this.isDownloadPlayable) {
|
||||||
|
|
|
@ -76,6 +76,18 @@ Vue.directive('click-outside', {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function xmlToJson(xml) {
|
||||||
|
const json = {};
|
||||||
|
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
|
||||||
|
const key = res[1] || res[3];
|
||||||
|
const value = res[2] && xmlToJson(res[2]);
|
||||||
|
json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null;
|
||||||
|
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
Vue.prototype.$xmlToJson = xmlToJson
|
||||||
|
|
||||||
const encode = (text) => encodeURIComponent(Buffer.from(text).toString('base64'))
|
const encode = (text) => encodeURIComponent(Buffer.from(text).toString('base64'))
|
||||||
Vue.prototype.$encode = encode
|
Vue.prototype.$encode = encode
|
||||||
const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString()
|
const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||||
|
|
15
static/libarchive/wasm-gen/libarchive.js
Normal file
15
static/libarchive/wasm-gen/libarchive.js
Normal file
File diff suppressed because one or more lines are too long
BIN
static/libarchive/wasm-gen/libarchive.wasm
Normal file
BIN
static/libarchive/wasm-gen/libarchive.wasm
Normal file
Binary file not shown.
1
static/libarchive/worker-bundle.js
Normal file
1
static/libarchive/worker-bundle.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -11,7 +11,9 @@ export const state = () => ({
|
||||||
networkConnectionType: null,
|
networkConnectionType: null,
|
||||||
streamListener: null,
|
streamListener: null,
|
||||||
isFirstLoad: true,
|
isFirstLoad: true,
|
||||||
hasStoragePermission: false
|
hasStoragePermission: false,
|
||||||
|
selectedBook: null,
|
||||||
|
showReader: false
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -80,5 +82,12 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
removeStreamListener(state) {
|
removeStreamListener(state) {
|
||||||
state.streamListener = null
|
state.streamListener = null
|
||||||
|
},
|
||||||
|
openReader(state, audiobook) {
|
||||||
|
state.selectedBook = audiobook
|
||||||
|
state.showReader = true
|
||||||
|
},
|
||||||
|
setShowReader(state, val) {
|
||||||
|
state.showReader = val
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue