mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Update users table info #94, Reorder libraries in config #95, Use dropdown for library menu #96, update mobi reader
This commit is contained in:
parent
9715c53332
commit
cd6e99b4c3
@ -1,4 +1,5 @@
|
|||||||
@import url('./transitions.css');
|
@import url('./transitions.css');
|
||||||
|
@import url('./draggable.css');
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
38
client/assets/draggable.css
Normal file
38
client/assets/draggable.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.flip-list-move {
|
||||||
|
transition: transform 0.5s;
|
||||||
|
}
|
||||||
|
.no-move {
|
||||||
|
transition: transform 0s;
|
||||||
|
}
|
||||||
|
.ghost {
|
||||||
|
opacity: 0.5;
|
||||||
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
.list-group {
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
#librariesTable .item {
|
||||||
|
cursor: n-resize;
|
||||||
|
}
|
||||||
|
.list-group-item:not(.exclude) {
|
||||||
|
cursor: n-resize;
|
||||||
|
}
|
||||||
|
.list-group-item.exclude {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.list-group-item:not(.ghost):not(.exclude):hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.list-group-item:nth-child(even):not(.ghost):not(.exclude) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.list-group-item:nth-child(even):not(.ghost):not(.exclude):hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item.exclude:not(.ghost) {
|
||||||
|
background-color: rgba(255, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.list-group-item.exclude:not(.ghost):hover {
|
||||||
|
background-color: rgba(223, 0, 0, 0.25);
|
||||||
|
}
|
506
client/assets/ebooks/basic.js
Normal file
506
client/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
client/assets/ebooks/htmlParser.js
Normal file
248
client/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;
|
@ -1,3 +1,7 @@
|
|||||||
|
/*
|
||||||
|
This is borrowed from koodo-reader https://github.com/troyeguo/koodo-reader/tree/master/src
|
||||||
|
*/
|
||||||
|
|
||||||
function ab2str(buf) {
|
function ab2str(buf) {
|
||||||
if (buf instanceof ArrayBuffer) {
|
if (buf instanceof ArrayBuffer) {
|
||||||
buf = new Uint8Array(buf);
|
buf = new Uint8Array(buf);
|
||||||
@ -8,9 +12,14 @@ function ab2str(buf) {
|
|||||||
var domParser = new DOMParser();
|
var domParser = new DOMParser();
|
||||||
|
|
||||||
class Buffer {
|
class Buffer {
|
||||||
|
capacity;
|
||||||
|
fragment_list;
|
||||||
|
imageArray;
|
||||||
|
cur_fragment;
|
||||||
constructor(capacity) {
|
constructor(capacity) {
|
||||||
this.capacity = capacity;
|
this.capacity = capacity;
|
||||||
this.fragment_list = [];
|
this.fragment_list = [];
|
||||||
|
this.imageArray = [];
|
||||||
this.cur_fragment = new Fragment(capacity);
|
this.cur_fragment = new Fragment(capacity);
|
||||||
this.fragment_list.push(this.cur_fragment);
|
this.fragment_list.push(this.cur_fragment);
|
||||||
}
|
}
|
||||||
@ -57,23 +66,26 @@ class Buffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var combine_uint8array = function (buffers) {
|
var copagesne_uint8array = function (buffers) {
|
||||||
var total_size = 0;
|
var total_size = 0;
|
||||||
for (var i = 0; i < buffers.length; i++) {
|
for (let i = 0; i < buffers.length; i++) {
|
||||||
var buffer = buffers[i];
|
var buffer = buffers[i];
|
||||||
total_size += buffer.length;
|
total_size += buffer.length;
|
||||||
}
|
}
|
||||||
var total_buffer = new Uint8Array(total_size);
|
var total_buffer = new Uint8Array(total_size);
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
for (var i = 0; i < buffers.length; i++) {
|
for (let i = 0; i < buffers.length; i++) {
|
||||||
var buffer = buffers[i];
|
buffer = buffers[i];
|
||||||
total_buffer.set(buffer, offset);
|
total_buffer.set(buffer, offset);
|
||||||
offset += buffer.length;
|
offset += buffer.length;
|
||||||
}
|
}
|
||||||
return total_buffer;
|
return total_buffer;
|
||||||
}
|
};
|
||||||
|
|
||||||
class Fragment {
|
class Fragment {
|
||||||
|
buffer;
|
||||||
|
capacity;
|
||||||
|
size;
|
||||||
constructor(capacity) {
|
constructor(capacity) {
|
||||||
this.buffer = new Uint8Array(capacity);
|
this.buffer = new Uint8Array(capacity);
|
||||||
this.capacity = capacity;
|
this.capacity = capacity;
|
||||||
@ -105,7 +117,7 @@ var uncompression_lz77 = function (data) {
|
|||||||
var char = data[offset];
|
var char = data[offset];
|
||||||
offset += 1;
|
offset += 1;
|
||||||
|
|
||||||
if (char == 0) {
|
if (char === 0) {
|
||||||
buffer.write(char);
|
buffer.write(char);
|
||||||
} else if (char <= 8) {
|
} else if (char <= 8) {
|
||||||
for (var i = offset; i < offset + char; i++) {
|
for (var i = offset; i < offset + char; i++) {
|
||||||
@ -117,12 +129,12 @@ var uncompression_lz77 = function (data) {
|
|||||||
} else if (char <= 0xbf) {
|
} else if (char <= 0xbf) {
|
||||||
var next = data[offset];
|
var next = data[offset];
|
||||||
offset += 1;
|
offset += 1;
|
||||||
var distance = ((char << 8 | next) >> 3) & 0x7ff;
|
var distance = (((char << 8) | next) >> 3) & 0x7ff;
|
||||||
var lz_length = (next & 0x7) + 3;
|
var lz_length = (next & 0x7) + 3;
|
||||||
|
|
||||||
var buffer_size = buffer.size();
|
var buffer_size = buffer.size();
|
||||||
for (var i = 0; i < lz_length; i++) {
|
for (let i = 0; i < lz_length; i++) {
|
||||||
buffer.write(buffer.get(buffer_size - distance))
|
buffer.write(buffer.get(buffer_size - distance));
|
||||||
buffer_size += 1;
|
buffer_size += 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -134,6 +146,13 @@ var uncompression_lz77 = function (data) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class MobiFile {
|
class MobiFile {
|
||||||
|
view;
|
||||||
|
buffer;
|
||||||
|
offset;
|
||||||
|
header;
|
||||||
|
palm_header;
|
||||||
|
mobi_header;
|
||||||
|
reclist;
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
this.view = new DataView(data);
|
this.view = new DataView(data);
|
||||||
this.buffer = this.view.buffer;
|
this.buffer = this.view.buffer;
|
||||||
@ -141,9 +160,7 @@ class MobiFile {
|
|||||||
this.header = null;
|
this.header = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
parse() {
|
parse() { }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getUint8() {
|
getUint8() {
|
||||||
var v = this.view.getUint8(this.offset);
|
var v = this.view.getUint8(this.offset);
|
||||||
@ -220,14 +237,14 @@ class MobiFile {
|
|||||||
}
|
}
|
||||||
return [size, l, pos];
|
return [size, l, pos];
|
||||||
}
|
}
|
||||||
|
// 读出文本内容
|
||||||
read_text() {
|
read_text() {
|
||||||
var text_end = this.palm_header.record_count;
|
var text_end = this.palm_header.record_count;
|
||||||
var buffers = [];
|
var buffers = [];
|
||||||
for (var i = 1; i <= text_end; i++) {
|
for (var i = 1; i <= text_end; i++) {
|
||||||
buffers.push(this.read_text_record(i));
|
buffers.push(this.read_text_record(i));
|
||||||
}
|
}
|
||||||
var all = combine_uint8array(buffers)
|
var all = copagesne_uint8array(buffers);
|
||||||
return ab2str(all);
|
return ab2str(all);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +253,7 @@ class MobiFile {
|
|||||||
var begin = this.reclist[i].offset;
|
var begin = this.reclist[i].offset;
|
||||||
var end = this.reclist[i + 1].offset;
|
var end = this.reclist[i + 1].offset;
|
||||||
|
|
||||||
var data = new Uint8Array(this.buffer.slice(begin, end))
|
var data = new Uint8Array(this.buffer.slice(begin, end));
|
||||||
var ex = this.get_record_extrasize(data, flags);
|
var ex = this.get_record_extrasize(data, flags);
|
||||||
|
|
||||||
data = new Uint8Array(this.buffer.slice(begin, end - ex));
|
data = new Uint8Array(this.buffer.slice(begin, end - ex));
|
||||||
@ -247,12 +264,12 @@ class MobiFile {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 从buffer中读出image
|
||||||
read_image(idx) {
|
read_image(idx) {
|
||||||
var first_image_idx = this.mobi_header.first_image_idx;
|
var first_image_idx = this.mobi_header.first_image_idx;
|
||||||
var begin = this.reclist[first_image_idx + idx].offset;
|
var begin = this.reclist[first_image_idx + idx].offset;
|
||||||
var end = this.reclist[first_image_idx + idx + 1].offset;
|
var end = this.reclist[first_image_idx + idx + 1].offset;
|
||||||
var data = new Uint8Array(this.buffer.slice(begin, end))
|
var data = new Uint8Array(this.buffer.slice(begin, end));
|
||||||
return new Blob([data.buffer]);
|
return new Blob([data.buffer]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +295,6 @@ class MobiFile {
|
|||||||
header.uid = this.getUint32();
|
header.uid = this.getUint32();
|
||||||
header.next_rec = this.getUint32();
|
header.next_rec = this.getUint32();
|
||||||
header.record_num = this.getUint16();
|
header.record_num = this.getUint16();
|
||||||
|
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,7 +377,7 @@ class MobiFile {
|
|||||||
|
|
||||||
mobi_header.extra_flags = this.getUint16();
|
mobi_header.extra_flags = this.getUint16();
|
||||||
|
|
||||||
this.setoffset(start_offset + mobi_header.header_length)
|
this.setoffset(start_offset + mobi_header.header_length);
|
||||||
|
|
||||||
return mobi_header;
|
return mobi_header;
|
||||||
}
|
}
|
||||||
@ -369,37 +385,66 @@ class MobiFile {
|
|||||||
// TODO
|
// TODO
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
extractContent(s) {
|
||||||
render_to(id) {
|
var span = document.createElement("span");
|
||||||
|
span.innerHTML = s;
|
||||||
|
return span.textContent || span.innerText;
|
||||||
|
}
|
||||||
|
render(isElectron = false) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
this.load();
|
this.load();
|
||||||
var content = this.read_text();
|
var content = this.read_text();
|
||||||
|
var bookDoc = domParser.parseFromString(content, "text/html")
|
||||||
var bookDom = document.getElementById(id);
|
.documentElement;
|
||||||
while (bookDom.firstChild) {
|
let lines = Array.from(
|
||||||
bookDom.removeChild(bookDom.firstChild);
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
var bookDoc = domParser.parseFromString(content, "text/html");
|
|
||||||
bookDoc.body.childNodes.forEach(function (x) {
|
|
||||||
if (x instanceof Element) {
|
|
||||||
bookDom.appendChild(x);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
var imgDoms = bookDom.getElementsByTagName("img");
|
|
||||||
for (var i = 0; i < imgDoms.length; i++) {
|
|
||||||
this.render_image(imgDoms, i);
|
|
||||||
}
|
}
|
||||||
}
|
render_image = (imgDoms, i) => {
|
||||||
render_image(imgDoms, i) {
|
return new Promise((resolve, reject) => {
|
||||||
var imgDom = imgDoms[i];
|
var imgDom = imgDoms[i];
|
||||||
var idx = +imgDom.getAttribute("recindex");
|
var idx = +imgDom.getAttribute("recindex");
|
||||||
var blob = this.read_image(idx - 1);
|
var blob = this.read_image(idx - 1);
|
||||||
var imgReader = new FileReader();
|
var imgReader = new FileReader();
|
||||||
imgReader.onload = function (e) {
|
imgReader.onload = (e) => {
|
||||||
imgDom.src = e.target.result;
|
imgDom.src = e.target?.result;
|
||||||
|
resolve(e.target?.result);
|
||||||
|
};
|
||||||
|
imgReader.onerror = function (err) {
|
||||||
|
reject(err);
|
||||||
};
|
};
|
||||||
imgReader.readAsDataURL(blob);
|
imgReader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
module.exports = MobiFile
|
export default MobiFile;
|
@ -7,24 +7,15 @@
|
|||||||
<span class="material-icons text-4xl text-white">arrow_back</span>
|
<span class="material-icons text-4xl text-white">arrow_back</span>
|
||||||
</a>
|
</a>
|
||||||
<h1 class="text-2xl font-book mr-6">AudioBookshelf</h1>
|
<h1 class="text-2xl font-book mr-6">AudioBookshelf</h1>
|
||||||
<!-- <div class="-mb-2 mr-6"> -->
|
|
||||||
<!-- <h1 class="text-base font-book leading-3 px-1">AudioBookshelf</h1> -->
|
|
||||||
|
|
||||||
<!-- <div class="bg-black bg-opacity-20 rounded-sm py-1 px-2 flex items-center border border-bg mt-1.5 cursor-pointer" @click="clickLibrary">
|
<!-- <div class="bg-black bg-opacity-20 rounded-md py-1.5 px-3 flex items-center text-white text-opacity-70 cursor-pointer hover:bg-opacity-10 hover:text-opacity-90" @click="clickLibrary">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-white text-opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<p class="text-sm text-white text-opacity-70 leading-3 font-book pl-2">{{ libraryName }}</p>
|
|
||||||
</div> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
<div class="bg-black bg-opacity-20 rounded-md py-1.5 px-3 flex items-center text-white text-opacity-70 cursor-pointer hover:bg-opacity-10 hover:text-opacity-90" @click="clickLibrary">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="text-sm leading-3 font-sans pl-2">{{ libraryName }}</p>
|
<p class="text-sm leading-3 font-sans pl-2">{{ libraryName }}</p>
|
||||||
</div>
|
</div> -->
|
||||||
|
<ui-libraries-dropdown />
|
||||||
|
|
||||||
<controls-global-search />
|
<controls-global-search />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
@ -12,10 +12,8 @@
|
|||||||
<h1 class="text-2xl mb-1">{{ title || abTitle }}</h1>
|
<h1 class="text-2xl mb-1">{{ title || abTitle }}</h1>
|
||||||
<p v-if="author || abAuthor">by {{ author || abAuthor }}</p>
|
<p v-if="author || abAuthor">by {{ author || abAuthor }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!epubEbook && mobiEbook" class="absolute top-4 left-0 w-full flex justify-center">
|
|
||||||
<p class="text-error font-semibold">Warning: Reading mobi & azw3 files is in the very early stages</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<!-- EPUB -->
|
||||||
<div v-if="epubEbook" class="h-full flex items-center">
|
<div v-if="epubEbook" class="h-full flex items-center">
|
||||||
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden">
|
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden">
|
||||||
<span v-show="hasPrev" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageLeft">chevron_left</span>
|
<span v-show="hasPrev" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageLeft">chevron_left</span>
|
||||||
@ -31,9 +29,10 @@
|
|||||||
<span v-show="hasNext" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageRight">chevron_right</span>
|
<span v-show="hasNext" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageRight">chevron_right</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="h-full flex items-center justify-center">
|
<!-- MOBI/AZW3 -->
|
||||||
<div class="w-full max-w-4xl overflow-y-auto border border-black border-opacity-10 p-4" style="max-height: 80vh">
|
<div v-else class="h-full max-h-full w-full">
|
||||||
<div id="viewer" />
|
<div class="ebook-viewer absolute overflow-y-scroll left-0 right-0 top-12 w-full max-w-4xl m-auto z-10 border border-black border-opacity-20">
|
||||||
|
<iframe title="html-viewer" width="100%"> Loading </iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,11 +40,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ePub from 'epubjs'
|
import ePub from 'epubjs'
|
||||||
import mobijs from '@/assets/mobi.js'
|
import MobiParser from '@/assets/ebooks/mobi.js'
|
||||||
|
import HtmlParser from '@/assets/ebooks/htmlParser.js'
|
||||||
|
import defaultCss from '@/assets/ebooks/basic.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
scale: 1,
|
||||||
book: null,
|
book: null,
|
||||||
rendition: null,
|
rendition: null,
|
||||||
chapters: [],
|
chapters: [],
|
||||||
@ -115,10 +117,6 @@ export default {
|
|||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
}
|
}
|
||||||
// fullUrl() {
|
|
||||||
// var serverUrl = process.env.serverUrl || `/s/book/${this.audiobookId}`
|
|
||||||
// return `${serverUrl}/${this.url}`
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changedChapter() {
|
changedChapter() {
|
||||||
@ -165,14 +163,85 @@ export default {
|
|||||||
this.initMobi()
|
this.initMobi()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
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() {
|
async initMobi() {
|
||||||
|
// Fetch mobi file as blob
|
||||||
var buff = await this.$axios.$get(this.mobiUrl, {
|
var buff = await this.$axios.$get(this.mobiUrl, {
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
})
|
})
|
||||||
var reader = new FileReader()
|
var reader = new FileReader()
|
||||||
reader.onload = function (event) {
|
reader.onload = async (event) => {
|
||||||
var file_content = event.target.result
|
var file_content = event.target.result
|
||||||
new mobijs(file_content).render_to('viewer')
|
|
||||||
|
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)
|
reader.readAsArrayBuffer(buff)
|
||||||
},
|
},
|
||||||
@ -249,3 +318,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* @import url(@/assets/calibre/basic.css); */
|
||||||
|
.ebook-viewer {
|
||||||
|
height: calc(100% - 96px);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-4 h-12 border border-white border-opacity-10 cursor-pointer flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false" @click="itemClicked">
|
<div class="w-full px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
|
||||||
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
|
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
|
||||||
<svg v-if="!libraryScan" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" :class="mouseover ? 'text-opacity-90' : 'text-opacity-50'" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg v-if="!libraryScan" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-xl font-book pl-4" :class="mouseover ? 'underline' : ''">{{ library.name }}</p>
|
<p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-show="mouseover && !libraryScan && canScan" small color="bg" @click.stop="scan">Scan</ui-btn>
|
<ui-btn v-show="isHovering && !libraryScan && canScan" small color="bg" @click.stop="scan">Scan</ui-btn>
|
||||||
<span v-show="mouseover && !libraryScan && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4" @click.stop="editClick">edit</span>
|
<span v-show="isHovering && !libraryScan && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
|
||||||
<span v-show="!libraryScan && mouseover && showEdit && canDelete && !isDeleting" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">delete</span>
|
<span v-show="!libraryScan && isHovering && showEdit && canDelete && !isDeleting" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50 cursor-pointer'" @click.stop="deleteClick">delete</span>
|
||||||
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">
|
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
|
||||||
<svg viewBox="0 0 24 24" class="w-6 h-6">
|
<svg viewBox="0 0 24 24" class="w-6 h-6">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -28,7 +28,8 @@ export default {
|
|||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
showEdit: Boolean
|
showEdit: Boolean,
|
||||||
|
dragging: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -37,6 +38,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isHovering() {
|
||||||
|
return this.mouseover && !this.dragging
|
||||||
|
},
|
||||||
isMain() {
|
isMain() {
|
||||||
return this.library.id === 'main'
|
return this.library.id === 'main'
|
||||||
},
|
},
|
||||||
@ -55,7 +59,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
itemClicked() {
|
itemClicked() {
|
||||||
this.$emit('click', this.library)
|
// this.$emit('click', this.library)
|
||||||
},
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
this.$emit('edit', this.library)
|
this.$emit('edit', this.library)
|
||||||
|
@ -1,25 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div id="librariesTable" class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<h1 class="text-xl">Libraries</h1>
|
<h1 class="text-xl">Libraries</h1>
|
||||||
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddLibrary">
|
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddLibrary">
|
||||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<draggable v-model="libraryCopies" v-bind="dragOptions" class="list-group" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
||||||
<template v-for="library in libraries">
|
<!-- <transition-group type="transition" :name="!drag ? 'flip-list' : null"> -->
|
||||||
<modals-libraries-library-item :key="library.id" :library="library" :selected="currentLibraryId === library.id" :show-edit="true" @edit="editLibrary" @click="clickLibrary" />
|
<template v-for="library in libraryCopies">
|
||||||
|
<modals-libraries-library-item :key="library.id" :library="library" :selected="currentLibraryId === library.id" :show-edit="true" :dragging="drag" @edit="editLibrary" @click="setLibrary" class="item" />
|
||||||
</template>
|
</template>
|
||||||
|
<!-- </transition-group> -->
|
||||||
|
</draggable>
|
||||||
<modals-edit-library-modal v-model="showLibraryModal" :library="selectedLibrary" />
|
<modals-edit-library-modal v-model="showLibraryModal" :library="selectedLibrary" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
draggable
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
libraryCopies: [],
|
||||||
|
currentOrder: [],
|
||||||
showLibraryModal: false,
|
showLibraryModal: false,
|
||||||
selectedLibrary: null
|
selectedLibrary: null,
|
||||||
|
drag: false,
|
||||||
|
dragOptions: {
|
||||||
|
animation: 200,
|
||||||
|
group: 'description',
|
||||||
|
ghostClass: 'ghost'
|
||||||
|
},
|
||||||
|
orderTimeout: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -30,11 +47,46 @@ export default {
|
|||||||
return this.currentLibrary ? this.currentLibrary.id : null
|
return this.currentLibrary ? this.currentLibrary.id : null
|
||||||
},
|
},
|
||||||
libraries() {
|
libraries() {
|
||||||
return this.$store.state.libraries.libraries
|
return this.$store.getters['libraries/getSortedLibraries']()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async clickLibrary(library) {
|
startDrag() {
|
||||||
|
this.drag = true
|
||||||
|
clearTimeout(this.orderTimeout)
|
||||||
|
},
|
||||||
|
endDrag() {
|
||||||
|
this.drag = false
|
||||||
|
this.checkOrder()
|
||||||
|
console.log('DRAG END')
|
||||||
|
},
|
||||||
|
checkOrder() {
|
||||||
|
clearTimeout(this.orderTimeout)
|
||||||
|
this.orderTimeout = setTimeout(() => {
|
||||||
|
this.saveOrder()
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
saveOrder() {
|
||||||
|
var _newOrder = 1
|
||||||
|
var currOrder = this.libraries.map((lib) => lib.id).join(',')
|
||||||
|
var libraryOrderData = this.libraryCopies.map((library) => {
|
||||||
|
return {
|
||||||
|
newOrder: _newOrder++,
|
||||||
|
oldOrder: library.displayOrder,
|
||||||
|
id: library.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
||||||
|
if (currOrder !== newOrder) {
|
||||||
|
this.$axios.$patch('/api/libraries/order', libraryOrderData).then((libraries) => {
|
||||||
|
if (libraries && libraries.length) {
|
||||||
|
this.$toast.success('Library order saved', { timeout: 1500 })
|
||||||
|
this.$store.commit('libraries/set', libraries)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setLibrary(library) {
|
||||||
await this.$store.dispatch('libraries/fetch', library.id)
|
await this.$store.dispatch('libraries/fetch', library.id)
|
||||||
this.$router.push(`/library/${library.id}`)
|
this.$router.push(`/library/${library.id}`)
|
||||||
},
|
},
|
||||||
@ -46,7 +98,11 @@ export default {
|
|||||||
this.selectedLibrary = library
|
this.selectedLibrary = library
|
||||||
this.showLibraryModal = true
|
this.showLibraryModal = true
|
||||||
},
|
},
|
||||||
init() {}
|
init() {
|
||||||
|
this.libraryCopies = this.libraries.map((lib) => {
|
||||||
|
return { ...lib }
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
|
@ -11,17 +11,43 @@
|
|||||||
<table id="accounts">
|
<table id="accounts">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Account Type</th>
|
<th class="w-20">Type</th>
|
||||||
<th style="width: 200px">Created At</th>
|
<th>Activity</th>
|
||||||
<th style="width: 100px"></th>
|
<th class="w-32">Last Seen</th>
|
||||||
|
<th class="w-32">Created</th>
|
||||||
|
<th class="w-32"></th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="user in users" :key="user.id" :class="user.isActive ? '' : 'bg-error bg-opacity-20'">
|
<tr v-for="user in users" :key="user.id" :class="user.isActive ? '' : 'bg-error bg-opacity-20'">
|
||||||
<td>
|
<td>
|
||||||
{{ user.username }} <span class="text-xs text-gray-400 italic pl-4">({{ user.id }})</span>
|
<div class="flex items-center">
|
||||||
|
<span v-if="usersOnline[user.id]" class="w-3 h-3 text-sm mr-2 text-success animate-pulse"
|
||||||
|
><svg viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg
|
||||||
|
></span>
|
||||||
|
<svg v-else class="w-3 h-3 mr-2 text-white text-opacity-20" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||||
|
</svg>
|
||||||
|
{{ user.username }} <span v-show="$isDev" class="text-xs text-gray-400 italic pl-4">({{ user.id }})</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ user.type }}</td>
|
<td class="text-sm">{{ user.type }}</td>
|
||||||
<td class="text-sm font-mono">
|
<td>
|
||||||
{{ new Date(user.createdAt).toISOString() }}
|
<div v-if="usersOnline[user.id] && usersOnline[user.id].stream && usersOnline[user.id].stream.audiobook && usersOnline[user.id].stream.audiobook.book">
|
||||||
|
<p class="truncate text-xs">Reading: {{ usersOnline[user.id].stream.audiobook.book.title || '' }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="user.audiobooks && getLastRead(user.audiobooks)">
|
||||||
|
<p class="truncate text-xs">Last: {{ getLastRead(user.audiobooks) }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-xs font-mono">
|
||||||
|
<ui-tooltip v-if="user.lastSeen" direction="top" :text="$formatDate(user.lastSeen, 'MMMM do, yyyy HH:mm')">
|
||||||
|
{{ $dateDistanceFromNow(user.lastSeen) }}
|
||||||
|
</ui-tooltip>
|
||||||
|
</td>
|
||||||
|
<td class="text-xs font-mono">
|
||||||
|
<ui-tooltip direction="top" :text="$formatDate(user.createdAt, 'MMMM do, yyyy HH:mm')">
|
||||||
|
{{ $formatDate(user.createdAt, 'MMM d, yyyy') }}
|
||||||
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
@ -47,8 +73,41 @@ export default {
|
|||||||
isDeletingUser: false
|
isDeletingUser: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {
|
||||||
|
currentUserId() {
|
||||||
|
return this.$store.state.user.user.id
|
||||||
|
},
|
||||||
|
userStream() {
|
||||||
|
return this.$store.state.streamAudiobook
|
||||||
|
},
|
||||||
|
usersOnline() {
|
||||||
|
var _users = this.$store.state.users.users
|
||||||
|
|
||||||
|
var currUserStream = null
|
||||||
|
if (this.userStream) {
|
||||||
|
currUserStream = {
|
||||||
|
audiobook: this.userStream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var usermap = {
|
||||||
|
[this.currentUserId]: {
|
||||||
|
online: true,
|
||||||
|
stream: currUserStream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_users.forEach((u) => (usermap[u.id] = { online: true, stream: u.stream }))
|
||||||
|
return usermap
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getLastRead(audiobooks) {
|
||||||
|
var abs = Object.values(audiobooks)
|
||||||
|
if (abs.length) {
|
||||||
|
abs = abs.sort((a, b) => a.lastUpdate - b.lastUpdate)
|
||||||
|
return abs[0] && abs[0].audiobookTitle ? abs[0].audiobookTitle : null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
deleteUserClick(user) {
|
deleteUserClick(user) {
|
||||||
if (this.isDeletingUser) return
|
if (this.isDeletingUser) return
|
||||||
if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) {
|
if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) {
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative w-full" v-click-outside="clickOutside">
|
<div class="relative w-full" v-click-outside="clickOutside">
|
||||||
<p class="text-sm text-opacity-75 mb-1">{{ label }}</p>
|
<p class="text-sm text-opacity-75 mb-1">{{ label }}</p>
|
||||||
<button type="button" :disabled="disabled" class="relative h-10 w-full bg-fg border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer bg-primary" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="relative h-10 w-full border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer bg-primary" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<span class="block truncate">{{ selectedText }}</span>
|
<span class="block truncate">{{ selectedText }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<span class="material-icons text-gray-100">chevron_down</span>
|
<span class="material-icons text-gray-100">expand_more</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
71
client/components/ui/LibrariesDropdown.vue
Normal file
71
client/components/ui/LibrariesDropdown.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="currentLibrary" class="relative w-36 h-8" v-click-outside="clickOutside">
|
||||||
|
<button type="button" :disabled="disabled" class="relative h-full w-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm pl-3 pr-10 text-left focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<widgets-library-icon :icon="currentLibraryIcon" class="mr-2" />
|
||||||
|
<span class="block truncate text-sm">{{ currentLibrary.name }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="menu">
|
||||||
|
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||||
|
<template v-for="library in libraries">
|
||||||
|
<li :key="library.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)">
|
||||||
|
<div class="flex items-center px-3">
|
||||||
|
<widgets-library-icon :icon="currentLibraryIcon" class="mr-2" />
|
||||||
|
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showMenu: false,
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
currentLibrary() {
|
||||||
|
return this.libraries.find((lib) => lib.id === this.currentLibraryId)
|
||||||
|
},
|
||||||
|
currentLibraryIcon() {
|
||||||
|
return this.currentLibrary ? this.currentLibrary.icon || 'database' : 'database'
|
||||||
|
},
|
||||||
|
libraries() {
|
||||||
|
return this.$store.getters['libraries/getSortedLibraries']()
|
||||||
|
},
|
||||||
|
libraryItems() {
|
||||||
|
return this.libraries.map((lib) => ({ value: lib.id, text: lib.name }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickShowMenu() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = !this.showMenu
|
||||||
|
},
|
||||||
|
clickOutside() {
|
||||||
|
this.showMenu = false
|
||||||
|
},
|
||||||
|
selectLibrary(library) {
|
||||||
|
this.updateLibrary(library)
|
||||||
|
this.showMenu = false
|
||||||
|
},
|
||||||
|
async updateLibrary(library) {
|
||||||
|
this.disabled = true
|
||||||
|
await this.$store.dispatch('libraries/fetch', library.id)
|
||||||
|
this.$router.push(`/library/${library.id}`)
|
||||||
|
this.disabled = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input ref="input" v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
<input ref="input" v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
||||||
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
||||||
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
21
client/components/widgets/LibraryIcon.vue
Normal file
21
client/components/widgets/LibraryIcon.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-4 w-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
icon: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -163,6 +163,15 @@ export default {
|
|||||||
this.$store.commit('user/setSettings', user.settings)
|
this.$store.commit('user/setSettings', user.settings)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
userOnline(user) {
|
||||||
|
this.$store.commit('users/updateUser', user)
|
||||||
|
},
|
||||||
|
userOffline(user) {
|
||||||
|
this.$store.commit('users/removeUser', user)
|
||||||
|
},
|
||||||
|
userStreamUpdate(user) {
|
||||||
|
this.$store.commit('users/updateUser', user)
|
||||||
|
},
|
||||||
downloadToastClick(download) {
|
downloadToastClick(download) {
|
||||||
if (!download || !download.audiobookId) {
|
if (!download || !download.audiobookId) {
|
||||||
return console.error('Invalid download object', download)
|
return console.error('Invalid download object', download)
|
||||||
@ -268,6 +277,9 @@ export default {
|
|||||||
|
|
||||||
// User Listeners
|
// User Listeners
|
||||||
this.socket.on('user_updated', this.userUpdated)
|
this.socket.on('user_updated', this.userUpdated)
|
||||||
|
this.socket.on('user_online', this.userOnline)
|
||||||
|
this.socket.on('user_offline', this.userOffline)
|
||||||
|
this.socket.on('user_stream_update', this.userStreamUpdate)
|
||||||
|
|
||||||
// Scan Listeners
|
// Scan Listeners
|
||||||
this.socket.on('scan_start', this.scanStart)
|
this.socket.on('scan_start', this.scanStart)
|
||||||
|
7
client/package-lock.json
generated
7
client/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.3.4",
|
"version": "1.4.6",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -4995,6 +4995,11 @@
|
|||||||
"type": "^1.0.1"
|
"type": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"version": "2.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.25.0.tgz",
|
||||||
|
"integrity": "sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w=="
|
||||||
|
},
|
||||||
"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",
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.4.6",
|
"version": "1.4.7",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt",
|
"dev": "nuxt",
|
||||||
|
"dev2": "nuxt --hostname localhost --port 1337",
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"start": "nuxt start",
|
"start": "nuxt start",
|
||||||
"generate": "nuxt generate"
|
"generate": "nuxt generate"
|
||||||
@ -15,6 +16,7 @@
|
|||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
"nuxt": "^2.15.7",
|
"nuxt": "^2.15.7",
|
||||||
|
@ -224,41 +224,3 @@ export default {
|
|||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.flip-list-move {
|
|
||||||
transition: transform 0.5s;
|
|
||||||
}
|
|
||||||
.no-move {
|
|
||||||
transition: transform 0s;
|
|
||||||
}
|
|
||||||
.ghost {
|
|
||||||
opacity: 0.5;
|
|
||||||
background-color: rgba(255, 255, 255, 0.25);
|
|
||||||
}
|
|
||||||
.list-group {
|
|
||||||
min-height: 30px;
|
|
||||||
}
|
|
||||||
.list-group-item:not(.exclude) {
|
|
||||||
cursor: n-resize;
|
|
||||||
}
|
|
||||||
.list-group-item.exclude {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.list-group-item:not(.ghost):not(.exclude):hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.list-group-item:nth-child(even):not(.ghost):not(.exclude) {
|
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
.list-group-item:nth-child(even):not(.ghost):not(.exclude):hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item.exclude:not(.ghost) {
|
|
||||||
background-color: rgba(255, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
.list-group-item.exclude:not(.ghost):hover {
|
|
||||||
background-color: rgba(223, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -22,7 +22,7 @@
|
|||||||
by <nuxt-link v-if="author" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}</nuxt-link
|
by <nuxt-link v-if="author" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}</nuxt-link
|
||||||
><span v-else>Unknown</span>
|
><span v-else>Unknown</span>
|
||||||
</p>
|
</p>
|
||||||
<nuxt-link v-if="series" :to="`/library/${libraryId}/bookshelf?filter=series.${$encode(series)}`" class="hover:underline font-sans text-gray-300 text-lg leading-7 mb-4"> {{ seriesText }}</nuxt-link>
|
<nuxt-link v-if="series" :to="`/library/${libraryId}/bookshelf/series?series=${$encode(series)}`" class="hover:underline font-sans text-gray-300 text-lg leading-7 mb-4"> {{ seriesText }}</nuxt-link>
|
||||||
|
|
||||||
<!-- <div class="w-min">
|
<!-- <div class="w-min">
|
||||||
<ui-tooltip :text="authorTooltipText" direction="bottom">
|
<ui-tooltip :text="authorTooltipText" direction="bottom">
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import { formatDistance, format } from 'date-fns'
|
||||||
|
|
||||||
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
|
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||||
|
if (!unixms) return ''
|
||||||
|
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
||||||
|
}
|
||||||
|
Vue.prototype.$formatDate = (unixms, fnsFormat = 'MM/dd/yyyy HH:mm') => {
|
||||||
|
if (!unixms) return ''
|
||||||
|
return format(unixms, fnsFormat)
|
||||||
|
}
|
||||||
|
|
||||||
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
||||||
if (bytes === 0) {
|
if (bytes === 0) {
|
||||||
return '0 Bytes'
|
return '0 Bytes'
|
||||||
|
@ -11,6 +11,9 @@ export const state = () => ({
|
|||||||
export const getters = {
|
export const getters = {
|
||||||
getCurrentLibrary: state => {
|
getCurrentLibrary: state => {
|
||||||
return state.libraries.find(lib => lib.id === state.currentLibraryId)
|
return state.libraries.find(lib => lib.id === state.currentLibraryId)
|
||||||
|
},
|
||||||
|
getSortedLibraries: state => () => {
|
||||||
|
return state.libraries.map(lib => ({ ...lib })).sort((a, b) => a.displayOrder - b.displayOrder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
client/store/users.js
Normal file
26
client/store/users.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
export const state = () => ({
|
||||||
|
users: []
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
updateUser(state, user) {
|
||||||
|
var index = state.users.findIndex(u => u.id === user.id)
|
||||||
|
if (index >= 0) {
|
||||||
|
state.users.splice(index, 1, user)
|
||||||
|
} else {
|
||||||
|
state.users.push(user)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeUser(state, user) {
|
||||||
|
state.users = state.users.filter(u => u.id !== user.id)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.4.6",
|
"version": "1.4.7",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -30,6 +30,7 @@ class ApiController {
|
|||||||
this.router.get('/find/:method', this.find.bind(this))
|
this.router.get('/find/:method', this.find.bind(this))
|
||||||
|
|
||||||
this.router.get('/libraries', this.getLibraries.bind(this))
|
this.router.get('/libraries', this.getLibraries.bind(this))
|
||||||
|
this.router.patch('/libraries/order', this.reorderLibraries.bind(this))
|
||||||
this.router.get('/library/:id/search', this.searchLibrary.bind(this))
|
this.router.get('/library/:id/search', this.searchLibrary.bind(this))
|
||||||
this.router.get('/library/:id', this.getLibrary.bind(this))
|
this.router.get('/library/:id', this.getLibrary.bind(this))
|
||||||
this.router.delete('/library/:id', this.deleteLibrary.bind(this))
|
this.router.delete('/library/:id', this.deleteLibrary.bind(this))
|
||||||
@ -98,6 +99,36 @@ class ApiController {
|
|||||||
res.json(libraries)
|
res.json(libraries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reorderLibraries(req, res) {
|
||||||
|
if (!req.user || !req.user.isRoot) {
|
||||||
|
Logger.error('[ApiController] ReorderLibraries invalid user', req.user)
|
||||||
|
return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderdata = req.body
|
||||||
|
var hasUpdates = false
|
||||||
|
for (let i = 0; i < orderdata.length; i++) {
|
||||||
|
var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
|
||||||
|
if (!library) {
|
||||||
|
Logger.error(`[ApiController] Invalid library not found in reorder ${orderdata[i].id}`)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
if (library.update({ displayOrder: orderdata[i].newOrder })) {
|
||||||
|
hasUpdates = true
|
||||||
|
await this.db.updateEntity('library', library)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUpdates) {
|
||||||
|
Logger.info(`[ApiController] Updated library display orders`)
|
||||||
|
} else {
|
||||||
|
Logger.info(`[ApiController] Library orders were up to date`)
|
||||||
|
}
|
||||||
|
|
||||||
|
var libraries = this.db.libraries.map(lib => lib.toJSON())
|
||||||
|
res.json(libraries)
|
||||||
|
}
|
||||||
|
|
||||||
searchLibrary(req, res) {
|
searchLibrary(req, res) {
|
||||||
var library = this.db.libraries.find(lib => lib.id === req.params.id)
|
var library = this.db.libraries.find(lib => lib.id === req.params.id)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
@ -226,6 +257,7 @@ class ApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var library = new Library()
|
var library = new Library()
|
||||||
|
newLibraryPayload.displayOrder = this.db.libraries.length + 1
|
||||||
library.setData(newLibraryPayload)
|
library.setData(newLibraryPayload)
|
||||||
await this.db.insertEntity('library', library)
|
await this.db.insertEntity('library', library)
|
||||||
this.emitter('library_added', library.toJSON())
|
this.emitter('library_added', library.toJSON())
|
||||||
|
@ -92,6 +92,10 @@ class Server {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authMiddleware(req, res, next) {
|
||||||
|
this.auth.authMiddleware(req, res, next)
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
Logger.info('[Server] Init v' + version)
|
Logger.info('[Server] Init v' + version)
|
||||||
await this.streamManager.ensureStreamsDir()
|
await this.streamManager.ensureStreamsDir()
|
||||||
@ -270,6 +274,8 @@ class Server {
|
|||||||
Logger.info('[SOCKET] Unauth socket disconnected ' + socket.id)
|
Logger.info('[SOCKET] Unauth socket disconnected ' + socket.id)
|
||||||
delete this.clients[socket.id]
|
delete this.clients[socket.id]
|
||||||
} else {
|
} else {
|
||||||
|
socket.broadcast.emit('user_offline', _client.user.toJSONForPublic(this.streamManager.streams))
|
||||||
|
|
||||||
const disconnectTime = Date.now() - _client.connected_at
|
const disconnectTime = Date.now() - _client.connected_at
|
||||||
Logger.info(`[SOCKET] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
|
Logger.info(`[SOCKET] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
|
||||||
delete this.clients[socket.id]
|
delete this.clients[socket.id]
|
||||||
@ -341,10 +347,6 @@ class Server {
|
|||||||
return purged
|
return purged
|
||||||
}
|
}
|
||||||
|
|
||||||
authMiddleware(req, res, next) {
|
|
||||||
this.auth.authMiddleware(req, res, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleUpload(req, res) {
|
async handleUpload(req, res) {
|
||||||
if (!req.user.canUpload) {
|
if (!req.user.canUpload) {
|
||||||
Logger.warn('User attempted to upload without permission', req.user)
|
Logger.warn('User attempted to upload without permission', req.user)
|
||||||
@ -460,6 +462,11 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
socket.broadcast.emit('user_online', client.user.toJSONForPublic(this.streamManager.streams))
|
||||||
|
|
||||||
|
user.lastSeen = Date.now()
|
||||||
|
await this.db.updateEntity('user', user)
|
||||||
|
|
||||||
const initialPayload = {
|
const initialPayload = {
|
||||||
serverSettings: this.serverSettings.toJSON(),
|
serverSettings: this.serverSettings.toJSON(),
|
||||||
audiobookPath: this.AudiobookPath,
|
audiobookPath: this.AudiobookPath,
|
||||||
|
@ -111,6 +111,8 @@ class StreamManager {
|
|||||||
|
|
||||||
var stream = await this.openStream(client, audiobook)
|
var stream = await this.openStream(client, audiobook)
|
||||||
this.db.updateUserStream(client.user.id, stream.id)
|
this.db.updateUserStream(client.user.id, stream.id)
|
||||||
|
|
||||||
|
socket.broadcast.emit('user_stream_update', client.user.toJSONForPublic(this.streams))
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeStreamRequest(socket) {
|
async closeStreamRequest(socket) {
|
||||||
@ -126,6 +128,8 @@ class StreamManager {
|
|||||||
client.user.stream = null
|
client.user.stream = null
|
||||||
client.stream = null
|
client.stream = null
|
||||||
this.db.updateUserStream(client.user.id, null)
|
this.db.updateUserStream(client.user.id, null)
|
||||||
|
|
||||||
|
socket.broadcast.emit('user_stream_update', client.user.toJSONForPublic(this.streams))
|
||||||
}
|
}
|
||||||
|
|
||||||
async openTestStream(StreamsPath, audiobookId) {
|
async openTestStream(StreamsPath, audiobookId) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
class AudiobookProgress {
|
class AudiobookProgress {
|
||||||
constructor(progress) {
|
constructor(progress) {
|
||||||
this.audiobookId = null
|
this.audiobookId = null
|
||||||
|
this.audiobookTitle = null
|
||||||
this.totalDuration = null // seconds
|
this.totalDuration = null // seconds
|
||||||
this.progress = null // 0 to 1
|
this.progress = null // 0 to 1
|
||||||
this.currentTime = null // seconds
|
this.currentTime = null // seconds
|
||||||
@ -17,6 +18,7 @@ class AudiobookProgress {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
audiobookId: this.audiobookId,
|
audiobookId: this.audiobookId,
|
||||||
|
audiobookTitle: this.audiobookTitle,
|
||||||
totalDuration: this.totalDuration,
|
totalDuration: this.totalDuration,
|
||||||
progress: this.progress,
|
progress: this.progress,
|
||||||
currentTime: this.currentTime,
|
currentTime: this.currentTime,
|
||||||
@ -29,6 +31,7 @@ class AudiobookProgress {
|
|||||||
|
|
||||||
construct(progress) {
|
construct(progress) {
|
||||||
this.audiobookId = progress.audiobookId
|
this.audiobookId = progress.audiobookId
|
||||||
|
this.audiobookTitle = progress.audiobookTitle || null
|
||||||
this.totalDuration = progress.totalDuration
|
this.totalDuration = progress.totalDuration
|
||||||
this.progress = progress.progress
|
this.progress = progress.progress
|
||||||
this.currentTime = progress.currentTime
|
this.currentTime = progress.currentTime
|
||||||
@ -40,6 +43,7 @@ class AudiobookProgress {
|
|||||||
|
|
||||||
updateFromStream(stream) {
|
updateFromStream(stream) {
|
||||||
this.audiobookId = stream.audiobookId
|
this.audiobookId = stream.audiobookId
|
||||||
|
this.audiobookTitle = stream.audiobookTitle
|
||||||
this.totalDuration = stream.totalDuration
|
this.totalDuration = stream.totalDuration
|
||||||
this.progress = stream.clientProgress
|
this.progress = stream.clientProgress
|
||||||
this.currentTime = stream.clientCurrentTime
|
this.currentTime = stream.clientCurrentTime
|
||||||
|
@ -5,6 +5,7 @@ class Library {
|
|||||||
this.id = null
|
this.id = null
|
||||||
this.name = null
|
this.name = null
|
||||||
this.folders = []
|
this.folders = []
|
||||||
|
this.displayOrder = 1
|
||||||
this.icon = 'database'
|
this.icon = 'database'
|
||||||
|
|
||||||
this.lastScan = 0
|
this.lastScan = 0
|
||||||
@ -25,6 +26,7 @@ class Library {
|
|||||||
this.id = library.id
|
this.id = library.id
|
||||||
this.name = library.name
|
this.name = library.name
|
||||||
this.folders = (library.folders || []).map(f => new Folder(f))
|
this.folders = (library.folders || []).map(f => new Folder(f))
|
||||||
|
this.displayOrder = library.displayOrder || 1
|
||||||
this.icon = library.icon || 'database'
|
this.icon = library.icon || 'database'
|
||||||
|
|
||||||
this.createdAt = library.createdAt
|
this.createdAt = library.createdAt
|
||||||
@ -36,6 +38,7 @@ class Library {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
folders: (this.folders || []).map(f => f.toJSON()),
|
folders: (this.folders || []).map(f => f.toJSON()),
|
||||||
|
displayOrder: this.displayOrder,
|
||||||
icon: this.icon,
|
icon: this.icon,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
lastUpdate: this.lastUpdate
|
lastUpdate: this.lastUpdate
|
||||||
@ -59,6 +62,7 @@ class Library {
|
|||||||
return newFolder
|
return newFolder
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
this.displayOrder = data.displayOrder || 1
|
||||||
this.icon = data.icon || 'database'
|
this.icon = data.icon || 'database'
|
||||||
this.createdAt = Date.now()
|
this.createdAt = Date.now()
|
||||||
this.lastUpdate = Date.now()
|
this.lastUpdate = Date.now()
|
||||||
@ -70,6 +74,10 @@ class Library {
|
|||||||
this.name = payload.name
|
this.name = payload.name
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
|
if (!isNaN(payload.displayOrder) && payload.displayOrder !== this.displayOrder) {
|
||||||
|
this.displayOrder = Number(payload.displayOrder)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
if (payload.folders) {
|
if (payload.folders) {
|
||||||
var newFolders = payload.folders.filter(f => !f.id)
|
var newFolders = payload.folders.filter(f => !f.id)
|
||||||
var removedFolders = this.folders.filter(f => !payload.folders.find(_f => _f.id === f.id))
|
var removedFolders = this.folders.filter(f => !payload.folders.find(_f => _f.id === f.id))
|
||||||
|
@ -10,6 +10,7 @@ class User {
|
|||||||
this.token = null
|
this.token = null
|
||||||
this.isActive = true
|
this.isActive = true
|
||||||
this.isLocked = false
|
this.isLocked = false
|
||||||
|
this.lastSeen = null
|
||||||
this.createdAt = null
|
this.createdAt = null
|
||||||
this.audiobooks = null
|
this.audiobooks = null
|
||||||
|
|
||||||
@ -78,6 +79,7 @@ class User {
|
|||||||
audiobooks: this.audiobooksToJSON(),
|
audiobooks: this.audiobooksToJSON(),
|
||||||
isActive: this.isActive,
|
isActive: this.isActive,
|
||||||
isLocked: this.isLocked,
|
isLocked: this.isLocked,
|
||||||
|
lastSeen: this.lastSeen,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
settings: this.settings,
|
settings: this.settings,
|
||||||
permissions: this.permissions
|
permissions: this.permissions
|
||||||
@ -94,12 +96,25 @@ class User {
|
|||||||
audiobooks: this.audiobooksToJSON(),
|
audiobooks: this.audiobooksToJSON(),
|
||||||
isActive: this.isActive,
|
isActive: this.isActive,
|
||||||
isLocked: this.isLocked,
|
isLocked: this.isLocked,
|
||||||
|
lastSeen: this.lastSeen,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
settings: this.settings,
|
settings: this.settings,
|
||||||
permissions: this.permissions
|
permissions: this.permissions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForPublic(streams) {
|
||||||
|
var stream = this.stream && streams ? streams.find(s => s.id === this.stream) : null
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
username: this.username,
|
||||||
|
type: this.type,
|
||||||
|
stream: stream ? stream.toJSON() : null,
|
||||||
|
lastSeen: this.lastSeen,
|
||||||
|
createdAt: this.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
construct(user) {
|
construct(user) {
|
||||||
this.id = user.id
|
this.id = user.id
|
||||||
this.username = user.username
|
this.username = user.username
|
||||||
@ -117,6 +132,7 @@ class User {
|
|||||||
}
|
}
|
||||||
this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive
|
this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive
|
||||||
this.isLocked = user.type === 'root' ? false : !!user.isLocked
|
this.isLocked = user.type === 'root' ? false : !!user.isLocked
|
||||||
|
this.lastSeen = user.lastSeen || null
|
||||||
this.createdAt = user.createdAt || Date.now()
|
this.createdAt = user.createdAt || Date.now()
|
||||||
this.settings = user.settings || this.getDefaultUserSettings()
|
this.settings = user.settings || this.getDefaultUserSettings()
|
||||||
this.permissions = user.permissions || this.getDefaultUserPermissions()
|
this.permissions = user.permissions || this.getDefaultUserPermissions()
|
||||||
|
@ -1,404 +0,0 @@
|
|||||||
function ab2str(buf) {
|
|
||||||
if (buf instanceof ArrayBuffer) {
|
|
||||||
buf = new Uint8Array(buf);
|
|
||||||
}
|
|
||||||
return new TextDecoder("utf-8").decode(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
var domParser = new DOMParser();
|
|
||||||
|
|
||||||
class Buffer {
|
|
||||||
constructor(capacity) {
|
|
||||||
this.capacity = capacity;
|
|
||||||
this.fragment_list = [];
|
|
||||||
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 combine_uint8array = function (buffers) {
|
|
||||||
var total_size = 0;
|
|
||||||
for (var 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 (var i = 0; i < buffers.length; i++) {
|
|
||||||
var buffer = buffers[i];
|
|
||||||
total_buffer.set(buffer, offset);
|
|
||||||
offset += buffer.length;
|
|
||||||
}
|
|
||||||
return total_buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Fragment {
|
|
||||||
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 (var 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 {
|
|
||||||
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 = combine_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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {};
|
|
||||||
}
|
|
||||||
|
|
||||||
render_to(id) {
|
|
||||||
this.load();
|
|
||||||
var content = this.read_text();
|
|
||||||
|
|
||||||
var bookDom = document.getElementById(id);
|
|
||||||
while (bookDom.firstChild) {
|
|
||||||
bookDom.removeChild(bookDom.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bookDoc = domParser.parseFromString(content, "text/html");
|
|
||||||
bookDoc.body.childNodes.forEach(function (x) {
|
|
||||||
if (x instanceof Element) {
|
|
||||||
bookDom.appendChild(x);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var imgDoms = bookDom.getElementsByTagName("img");
|
|
||||||
for (var i = 0; i < imgDoms.length; i++) {
|
|
||||||
this.render_image(imgDoms, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render_image(imgDoms, i) {
|
|
||||||
var imgDom = imgDoms[i];
|
|
||||||
var idx = +imgDom.getAttribute("recindex");
|
|
||||||
var blob = this.read_image(idx - 1);
|
|
||||||
var imgReader = new FileReader();
|
|
||||||
imgReader.onload = function (e) {
|
|
||||||
imgDom.src = e.target.result;
|
|
||||||
};
|
|
||||||
imgReader.readAsDataURL(blob);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user