mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-06 10:42:44 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48f0e039e5 | |||
| aa872948d5 | |||
| 88e2bac3f5 | |||
| 18c1d8f1a3 | |||
| b9dee8704f |
@@ -1,5 +1,6 @@
|
|||||||
@import url('./transitions.css');
|
@import './fonts.css';
|
||||||
@import url('./draggable.css');
|
@import './transitions.css';
|
||||||
|
@import './draggable.css';
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/* fallback */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Icons';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(/fonts/material-icons.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons {
|
||||||
|
font-family: 'Material Icons';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: none;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: normal;
|
||||||
|
direction: ltr;
|
||||||
|
-webkit-font-feature-settings: 'liga';
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
.material-icons.text-icon {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
.material-icons.text-base {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Gentium Book Basic';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Gentium Book Basic';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative">
|
<div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative">
|
||||||
<!-- Cover size widget -->
|
<!-- Cover size widget -->
|
||||||
<div v-show="!isSelectionMode" class="fixed bottom-2 right-4 z-20">
|
<div v-show="!isSelectionMode" class="fixed bottom-2 right-4 z-30">
|
||||||
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
||||||
<span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span>
|
<span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span>
|
||||||
<p class="px-2 font-mono">{{ bookCoverWidth }}</p>
|
<p class="px-2 font-mono">{{ bookCoverWidth }}</p>
|
||||||
@@ -16,6 +16,14 @@
|
|||||||
<ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="page === 'search'" id="bookshelf-categorized" class="w-full flex flex-col items-center">
|
||||||
|
<template v-for="(shelf, index) in categorizedShelves">
|
||||||
|
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" />
|
||||||
|
</template>
|
||||||
|
<div v-show="!categorizedShelves.length" class="w-full py-16 text-center text-xl">
|
||||||
|
<div class="py-4 mb-6"><p class="text-2xl">No Results</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else id="bookshelf" class="w-full flex flex-col items-center">
|
<div v-else id="bookshelf" class="w-full flex flex-col items-center">
|
||||||
<template v-for="(shelf, index) in shelves">
|
<template v-for="(shelf, index) in shelves">
|
||||||
<div :key="index" class="w-full bookshelfRow relative">
|
<div :key="index" class="w-full bookshelfRow relative">
|
||||||
@@ -45,8 +53,8 @@ export default {
|
|||||||
page: String,
|
page: String,
|
||||||
selectedSeries: String,
|
selectedSeries: String,
|
||||||
searchResults: {
|
searchResults: {
|
||||||
type: Array,
|
type: Object,
|
||||||
default: () => []
|
default: () => {}
|
||||||
},
|
},
|
||||||
searchQuery: String
|
searchQuery: String
|
||||||
},
|
},
|
||||||
@@ -74,7 +82,7 @@ export default {
|
|||||||
},
|
},
|
||||||
searchResults() {
|
searchResults() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$store.commit('audiobooks/setSearchResults', this.searchResults)
|
// this.$store.commit('audiobooks/setSearchResults', this.searchResults)
|
||||||
this.setBookshelfEntities()
|
this.setBookshelfEntities()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -94,6 +102,9 @@ export default {
|
|||||||
audiobooks() {
|
audiobooks() {
|
||||||
return this.$store.state.audiobooks.audiobooks
|
return this.$store.state.audiobooks.audiobooks
|
||||||
},
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.bookCoverWidth / 120
|
||||||
|
},
|
||||||
bookCoverWidth() {
|
bookCoverWidth() {
|
||||||
return this.availableSizes[this.selectedSizeIndex]
|
return this.availableSizes[this.selectedSizeIndex]
|
||||||
},
|
},
|
||||||
@@ -122,11 +133,54 @@ export default {
|
|||||||
showGroups() {
|
showGroups() {
|
||||||
return this.page !== '' && this.page !== 'search' && !this.selectedSeries
|
return this.page !== '' && this.page !== 'search' && !this.selectedSeries
|
||||||
},
|
},
|
||||||
|
categorizedShelves() {
|
||||||
|
if (this.page !== 'search') return []
|
||||||
|
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
|
||||||
|
const shelves = []
|
||||||
|
|
||||||
|
if (audiobookSearchResults.length) {
|
||||||
|
shelves.push({
|
||||||
|
label: 'Books',
|
||||||
|
books: audiobookSearchResults.map((absr) => absr.audiobook)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.searchResults.series && this.searchResults.series.length) {
|
||||||
|
var seriesGroups = this.searchResults.series.map((seriesResult) => {
|
||||||
|
return {
|
||||||
|
type: 'series',
|
||||||
|
name: seriesResult.series || '',
|
||||||
|
books: seriesResult.audiobooks || []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
shelves.push({
|
||||||
|
label: 'Series',
|
||||||
|
series: seriesGroups
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.searchResults.tags && this.searchResults.tags.length) {
|
||||||
|
var tagGroups = this.searchResults.tags.map((tagResult) => {
|
||||||
|
return {
|
||||||
|
type: 'tags',
|
||||||
|
name: tagResult.tag || '',
|
||||||
|
books: tagResult.audiobooks || []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
shelves.push({
|
||||||
|
label: 'Tags',
|
||||||
|
series: tagGroups
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return shelves
|
||||||
|
},
|
||||||
entities() {
|
entities() {
|
||||||
if (this.page === '') {
|
if (this.page === '') {
|
||||||
return this.$store.getters['audiobooks/getFilteredAndSorted']()
|
return this.$store.getters['audiobooks/getFilteredAndSorted']()
|
||||||
} else if (this.page === 'search') {
|
} else if (this.page === 'search') {
|
||||||
return this.searchResults || []
|
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
|
||||||
|
return audiobookSearchResults.map((absr) => absr.audiobook)
|
||||||
} else {
|
} else {
|
||||||
var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']()
|
var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']()
|
||||||
if (this.selectedSeries) {
|
if (this.selectedSeries) {
|
||||||
|
|||||||
@@ -51,9 +51,6 @@ export default {
|
|||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
return this.bookCoverWidth / 120
|
return this.bookCoverWidth / 120
|
||||||
},
|
},
|
||||||
signSizeMultiplier() {
|
|
||||||
return (1 - this.sizeMultiplier) / 2 + this.sizeMultiplier
|
|
||||||
},
|
|
||||||
paddingX() {
|
paddingX() {
|
||||||
return 16 * this.sizeMultiplier
|
return 16 * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,11 +2,23 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div ref="shelf" class="w-full max-w-full bookshelfRowCategorized relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: 2.5 * sizeMultiplier + 'rem' }" @scroll="scrolled">
|
<div ref="shelf" class="w-full max-w-full bookshelfRowCategorized relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: 2.5 * sizeMultiplier + 'rem' }" @scroll="scrolled">
|
||||||
<div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }">
|
<div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }">
|
||||||
<div class="flex items-center -mb-2">
|
<div v-if="shelf.books" class="flex items-center -mb-2">
|
||||||
<template v-for="entity in shelf.books">
|
<template v-for="entity in shelf.books">
|
||||||
<cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" @edit="editBook" />
|
<cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" @edit="editBook" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="shelf.series" class="flex items-center -mb-2">
|
||||||
|
<template v-for="entity in shelf.series">
|
||||||
|
<cards-group-card :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="shelf.tags" class="flex items-center -mb-2">
|
||||||
|
<template v-for="entity in shelf.tags">
|
||||||
|
<nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
|
||||||
|
<cards-group-card :width="bookCoverWidth" :group="entity" />
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -67,7 +79,6 @@ export default {
|
|||||||
},
|
},
|
||||||
scrollLeft() {
|
scrollLeft() {
|
||||||
if (!this.$refs.shelf) {
|
if (!this.$refs.shelf) {
|
||||||
console.error('No Shelf', this.index)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isScrolling = true
|
this.isScrolling = true
|
||||||
@@ -75,7 +86,6 @@ export default {
|
|||||||
},
|
},
|
||||||
scrollRight() {
|
scrollRight() {
|
||||||
if (!this.$refs.shelf) {
|
if (!this.$refs.shelf) {
|
||||||
console.error('No Shelf', this.index)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isScrolling = true
|
this.isScrolling = true
|
||||||
@@ -89,7 +99,6 @@ export default {
|
|||||||
},
|
},
|
||||||
checkCanScroll() {
|
checkCanScroll() {
|
||||||
if (!this.$refs.shelf) {
|
if (!this.$refs.shelf) {
|
||||||
console.error('No Shelf', this.index)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var clientWidth = this.$refs.shelf.clientWidth
|
var clientWidth = this.$refs.shelf.clientWidth
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export default {
|
|||||||
isHome: Boolean,
|
isHome: Boolean,
|
||||||
selectedSeries: String,
|
selectedSeries: String,
|
||||||
searchResults: {
|
searchResults: {
|
||||||
type: Array,
|
type: Object,
|
||||||
default: () => []
|
default: () => {}
|
||||||
},
|
},
|
||||||
searchQuery: String
|
searchQuery: String
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,8 @@ export default {
|
|||||||
if (this.page === '') {
|
if (this.page === '') {
|
||||||
return this.$store.getters['audiobooks/getFiltered']().length
|
return this.$store.getters['audiobooks/getFiltered']().length
|
||||||
} else if (this.page === 'search') {
|
} else if (this.page === 'search') {
|
||||||
return (this.searchResults || []).length
|
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
|
||||||
|
return audiobookSearchResults.length
|
||||||
} else {
|
} else {
|
||||||
var groups = this.$store.getters['audiobooks/getSeriesGroups']()
|
var groups = this.$store.getters['audiobooks/getSeriesGroups']()
|
||||||
if (this.selectedSeries) {
|
if (this.selectedSeries) {
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 right-20 rounded-md overflow-y-auto bg-white shadow-lg z-20 border border-gray-400">
|
||||||
|
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-gray-200 px-2 py-1" @click="setPage(index)">
|
||||||
|
<p class="text-sm">{{ file }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute top-0 right-40 border-b border-l border-r border-gray-400 hover:bg-gray-200 cursor-pointer rounded-b-md bg-gray-50 w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
|
||||||
|
<span class="material-icons">menu</span>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-0 right-20 border-b border-l border-r border-gray-400 rounded-b-md bg-gray-50 px-2 h-9 flex items-center text-center">
|
||||||
|
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-hidden m-auto comicwrapper relative">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<div class="px-12">
|
||||||
|
<span v-show="loadedFirstPage" class="material-icons text-5xl text-black" :class="!canGoPrev ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goPrevPage" @mousedown.prevent>arrow_back_ios</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img v-if="mainImg" :src="mainImg" class="object-contain comicimg" />
|
||||||
|
|
||||||
|
<div class="px-12">
|
||||||
|
<span v-show="loadedFirstPage" class="material-icons text-5xl text-black" :class="!canGoNext ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goNextPage" @mousedown.prevent>arrow_forward_ios</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div v-show="loading" class="w-screen h-screen absolute top-0 left-0 bg-black bg-opacity-20 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Path from 'path'
|
||||||
|
import { Archive } from 'libarchive.js/main.js'
|
||||||
|
|
||||||
|
Archive.init({
|
||||||
|
workerUrl: '/libarchive/worker-bundle.js'
|
||||||
|
})
|
||||||
|
// Archive.init()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
src: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
pages: null,
|
||||||
|
filesObject: null,
|
||||||
|
mainImg: null,
|
||||||
|
page: 0,
|
||||||
|
numPages: 0,
|
||||||
|
showPageMenu: false,
|
||||||
|
loadTimeout: null,
|
||||||
|
loadedFirstPage: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
src: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
this.extract()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canGoNext() {
|
||||||
|
return this.page < this.numPages - 1
|
||||||
|
},
|
||||||
|
canGoPrev() {
|
||||||
|
return this.page > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickOutside() {
|
||||||
|
if (this.showPageMenu) this.showPageMenu = false
|
||||||
|
},
|
||||||
|
goNextPage() {
|
||||||
|
if (!this.canGoNext) return
|
||||||
|
this.setPage(this.page + 1)
|
||||||
|
},
|
||||||
|
goPrevPage() {
|
||||||
|
if (!this.canGoPrev) return
|
||||||
|
this.setPage(this.page - 1)
|
||||||
|
},
|
||||||
|
setPage(index) {
|
||||||
|
if (index < 0 || index > this.numPages - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var filename = this.pages[index]
|
||||||
|
this.page = index
|
||||||
|
return this.extractFile(filename)
|
||||||
|
},
|
||||||
|
setLoadTimeout() {
|
||||||
|
this.loadTimeout = setTimeout(() => {
|
||||||
|
this.loading = true
|
||||||
|
}, 150)
|
||||||
|
},
|
||||||
|
extractFile(filename) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
this.setLoadTimeout()
|
||||||
|
var file = await this.filesObject[filename].extract()
|
||||||
|
var reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.mainImg = e.target.result
|
||||||
|
this.loading = false
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
reader.onerror = (e) => {
|
||||||
|
console.error(e)
|
||||||
|
this.$toast.error('Read page file failed')
|
||||||
|
this.loading = false
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
clearTimeout(this.loadTimeout)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async extract() {
|
||||||
|
this.loading = true
|
||||||
|
console.log('Extracting', this.src)
|
||||||
|
|
||||||
|
var buff = await this.$axios.$get(this.src, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
const archive = await Archive.open(buff)
|
||||||
|
this.filesObject = await archive.getFilesObject()
|
||||||
|
var filenames = Object.keys(this.filesObject)
|
||||||
|
this.parseFilenames(filenames)
|
||||||
|
|
||||||
|
this.numPages = this.pages.length
|
||||||
|
|
||||||
|
if (this.pages.length) {
|
||||||
|
this.loading = false
|
||||||
|
await this.setPage(0)
|
||||||
|
this.loadedFirstPage = true
|
||||||
|
} else {
|
||||||
|
this.$toast.error('Unable to extract pages')
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseImageFilename(filename) {
|
||||||
|
var basename = Path.basename(filename, Path.extname(filename))
|
||||||
|
var numbersinpath = basename.match(/\d{1,4}/g)
|
||||||
|
if (!numbersinpath || !numbersinpath.length) {
|
||||||
|
return {
|
||||||
|
index: -1,
|
||||||
|
filename
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
index: Number(numbersinpath[numbersinpath.length - 1]),
|
||||||
|
filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseFilenames(filenames) {
|
||||||
|
const acceptableImages = ['.jpeg', '.jpg', '.png']
|
||||||
|
var imageFiles = filenames.filter((f) => {
|
||||||
|
return acceptableImages.includes((Path.extname(f) || '').toLowerCase())
|
||||||
|
})
|
||||||
|
var imageFileObjs = imageFiles.map((img) => {
|
||||||
|
return this.parseImageFilename(img)
|
||||||
|
})
|
||||||
|
|
||||||
|
var imagesWithNum = imageFileObjs.filter((i) => i.index >= 0)
|
||||||
|
var orderedImages = imagesWithNum.sort((a, b) => a.index - b.index).map((i) => i.filename)
|
||||||
|
var noNumImages = imageFileObjs.filter((i) => i.index < 0)
|
||||||
|
orderedImages = orderedImages.concat(noNumImages.map((i) => i.filename))
|
||||||
|
|
||||||
|
this.pages = orderedImages
|
||||||
|
},
|
||||||
|
keyUp(e) {
|
||||||
|
if ((e.keyCode || e.which) == 37) {
|
||||||
|
this.goPrevPage()
|
||||||
|
} else if ((e.keyCode || e.which) == 39) {
|
||||||
|
this.goNextPage()
|
||||||
|
} else if ((e.keyCode || e.which) == 27) {
|
||||||
|
this.unregisterListeners()
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
registerListeners() {
|
||||||
|
document.addEventListener('keyup', this.keyUp)
|
||||||
|
},
|
||||||
|
unregisterListeners() {
|
||||||
|
document.removeEventListener('keyup', this.keyUp)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.registerListeners()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.unregisterListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pagemenu {
|
||||||
|
max-height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
.comicimg {
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.comicwrapper {
|
||||||
|
width: calc(100vw - 300px);
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -39,6 +39,10 @@
|
|||||||
<div v-else-if="ebookType === 'pdf'" class="h-full flex items-center">
|
<div v-else-if="ebookType === 'pdf'" class="h-full flex items-center">
|
||||||
<app-pdf-reader :src="ebookUrl" />
|
<app-pdf-reader :src="ebookUrl" />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- COMIC -->
|
||||||
|
<div v-else-if="ebookType === 'comic'" class="h-full flex items-center">
|
||||||
|
<app-comic-reader :src="ebookUrl" @close="show = false" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,6 +115,9 @@ export default {
|
|||||||
pdfEbook() {
|
pdfEbook() {
|
||||||
return this.ebooks.find((eb) => eb.ext === '.pdf')
|
return this.ebooks.find((eb) => eb.ext === '.pdf')
|
||||||
},
|
},
|
||||||
|
comicEbook() {
|
||||||
|
return this.ebooks.find((eb) => eb.ext === '.cbz' || eb.ext === '.cbr')
|
||||||
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
@@ -158,7 +165,6 @@ export default {
|
|||||||
document.removeEventListener('keyup', this.keyUp)
|
document.removeEventListener('keyup', this.keyUp)
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.registerListeners()
|
|
||||||
if (this.selectedAudiobookFile) {
|
if (this.selectedAudiobookFile) {
|
||||||
this.ebookUrl = this.getEbookUrl(this.selectedAudiobookFile.path)
|
this.ebookUrl = this.getEbookUrl(this.selectedAudiobookFile.path)
|
||||||
if (this.selectedAudiobookFile.ext === '.pdf') {
|
if (this.selectedAudiobookFile.ext === '.pdf') {
|
||||||
@@ -169,6 +175,8 @@ export default {
|
|||||||
} else if (this.selectedAudiobookFile.ext === '.epub') {
|
} else if (this.selectedAudiobookFile.ext === '.epub') {
|
||||||
this.ebookType = 'epub'
|
this.ebookType = 'epub'
|
||||||
this.initEpub()
|
this.initEpub()
|
||||||
|
} else if (this.selectedAudiobookFile.ext === '.cbr' || this.selectedAudiobookFile.ext === '.cbz') {
|
||||||
|
this.ebookType = 'comic'
|
||||||
}
|
}
|
||||||
} else if (this.epubEbook) {
|
} else if (this.epubEbook) {
|
||||||
this.ebookType = 'epub'
|
this.ebookType = 'epub'
|
||||||
@@ -181,6 +189,9 @@ export default {
|
|||||||
} else if (this.pdfEbook) {
|
} else if (this.pdfEbook) {
|
||||||
this.ebookType = 'pdf'
|
this.ebookType = 'pdf'
|
||||||
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
|
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
|
||||||
|
} else if (this.comicEbook) {
|
||||||
|
this.ebookType = 'comic'
|
||||||
|
this.ebookUrl = this.getEbookUrl(this.comicEbook.path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addHtmlCss() {
|
addHtmlCss() {
|
||||||
@@ -266,6 +277,7 @@ export default {
|
|||||||
reader.readAsArrayBuffer(buff)
|
reader.readAsArrayBuffer(buff)
|
||||||
},
|
},
|
||||||
initEpub() {
|
initEpub() {
|
||||||
|
this.registerListeners()
|
||||||
// var book = ePub(this.url, {
|
// var book = ePub(this.url, {
|
||||||
// requestHeaders: {
|
// requestHeaders: {
|
||||||
// Authorization: `Bearer ${this.userToken}`
|
// Authorization: `Bearer ${this.userToken}`
|
||||||
@@ -327,14 +339,16 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
this.unregisterListeners()
|
if (this.ebookType === 'epub') {
|
||||||
|
this.unregisterListeners()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.show) this.init()
|
if (this.show) this.init()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.unregisterListeners()
|
this.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full px-1 overflow-hidden">
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
<cards-book-cover :audiobook="audiobook" :width="50" />
|
<cards-book-cover :audiobook="audiobook" :width="50" />
|
||||||
<div class="flex-grow px-2 searchCardContent h-full">
|
<div class="flex-grow px-2 audiobookSearchCardContent">
|
||||||
<p class="truncate text-sm">{{ title }}</p>
|
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
||||||
<p class="text-xs text-gray-200 truncate">by {{ author }}</p>
|
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
||||||
|
|
||||||
|
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
|
||||||
|
|
||||||
|
<p v-if="matchKey !== 'author'" class="text-xs text-gray-200 truncate">by {{ author }}</p>
|
||||||
|
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||||
|
|
||||||
|
<div v-if="matchKey === 'series' || matchKey === 'tags'" class="m-0 p-0 truncate" v-html="matchHtml" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -14,7 +21,10 @@ export default {
|
|||||||
audiobook: {
|
audiobook: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
},
|
||||||
|
search: String,
|
||||||
|
matchKey: String,
|
||||||
|
matchText: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@@ -26,8 +36,34 @@ export default {
|
|||||||
title() {
|
title() {
|
||||||
return this.book ? this.book.title : 'No Title'
|
return this.book ? this.book.title : 'No Title'
|
||||||
},
|
},
|
||||||
|
subtitle() {
|
||||||
|
return this.book ? this.book.subtitle : ''
|
||||||
|
},
|
||||||
author() {
|
author() {
|
||||||
return this.book ? this.book.author : 'Unknown'
|
return this.book ? this.book.author : 'Unknown'
|
||||||
|
},
|
||||||
|
matchHtml() {
|
||||||
|
if (!this.matchText || !this.search) return ''
|
||||||
|
if (this.matchKey === 'subtitle') return ''
|
||||||
|
var matchSplit = this.matchText.toLowerCase().split(this.search.toLowerCase().trim())
|
||||||
|
if (matchSplit.length < 2) return ''
|
||||||
|
|
||||||
|
var html = ''
|
||||||
|
var totalLenSoFar = 0
|
||||||
|
for (let i = 0; i < matchSplit.length - 1; i++) {
|
||||||
|
var indexOf = matchSplit[i].length
|
||||||
|
var firstPart = this.matchText.substr(totalLenSoFar, indexOf)
|
||||||
|
var actualWasThere = this.matchText.substr(totalLenSoFar + indexOf, this.search.length)
|
||||||
|
totalLenSoFar += indexOf + this.search.length
|
||||||
|
|
||||||
|
html += `${firstPart}<strong class="text-warning">${actualWasThere}</strong>`
|
||||||
|
}
|
||||||
|
var lastPart = this.matchText.substr(totalLenSoFar)
|
||||||
|
html += lastPart
|
||||||
|
|
||||||
|
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
||||||
|
if (this.matchKey === 'author') return `by ${html}`
|
||||||
|
return `${html}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {},
|
||||||
@@ -36,9 +72,9 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.searchCardContent {
|
.audiobookSearchCardContent {
|
||||||
width: calc(100% - 80px);
|
width: calc(100% - 80px);
|
||||||
height: calc(50px * 1.5);
|
height: 75px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full px-1 overflow-hidden">
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
<img src="https://rpgplanner.com/wp-content/uploads/2020/06/no-photo-available.png" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" />
|
<img src="/icons/NoUserPhoto.png" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" />
|
||||||
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
||||||
<p class="truncate text-sm">{{ author }}</p>
|
<p class="truncate text-sm">{{ author }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,7 +22,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.searchCardContent {
|
.authorSearchCardContent {
|
||||||
width: calc(100% - 80px);
|
width: calc(100% - 80px);
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="isHovering = true" @mouseleave="isHovering = false" @click="clickCard">
|
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="isHovering = true" @mouseleave="isHovering = false" @click="clickCard">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series?${groupType}=${groupEncode}`" class="cursor-pointer">
|
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||||
<div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: height + 'px', width: height + 'px' }">
|
<div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: height + 'px', width: height + 'px' }">
|
||||||
<cards-group-cover ref="groupcover" :name="groupName" :book-items="bookItems" :width="height" :height="height" />
|
<cards-group-cover ref="groupcover" :name="groupName" :book-items="bookItems" :width="height" :height="height" />
|
||||||
|
|
||||||
@@ -54,6 +54,16 @@ export default {
|
|||||||
_group() {
|
_group() {
|
||||||
return this.group || {}
|
return this.group || {}
|
||||||
},
|
},
|
||||||
|
groupType() {
|
||||||
|
return this._group.type
|
||||||
|
},
|
||||||
|
groupTo() {
|
||||||
|
if (this.groupType === 'series') {
|
||||||
|
return `/library/${this.currentLibraryId}/bookshelf/series?series=${this.groupEncode}`
|
||||||
|
} else {
|
||||||
|
return `/library/${this.currentLibraryId}/bookshelf?filter=tags.${this.groupEncode}`
|
||||||
|
}
|
||||||
|
},
|
||||||
height() {
|
height() {
|
||||||
return this.width * 1.6
|
return this.width * 1.6
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
|
<div class="w-10 h-10 flex items-center justify-center">
|
||||||
|
<span class="material-icons text-2xl text-gray-200">local_offer</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 tagSearchCardContent h-full">
|
||||||
|
<p class="truncate text-sm">{{ tag }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
tag: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tagSearchCardContent {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<template v-for="item in audiobookResults">
|
<template v-for="item in audiobookResults">
|
||||||
<li :key="item.audiobook.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
<li :key="item.audiobook.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||||
<nuxt-link :to="`/audiobook/${item.audiobook.id}`">
|
<nuxt-link :to="`/audiobook/${item.audiobook.id}`">
|
||||||
<cards-audiobook-search-card :audiobook="item.audiobook" />
|
<cards-audiobook-search-card :audiobook="item.audiobook" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,12 +39,21 @@
|
|||||||
|
|
||||||
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
|
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
|
||||||
<template v-for="item in seriesResults">
|
<template v-for="item in seriesResults">
|
||||||
<li :key="item.series" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickedOption(item.series)">
|
<li :key="item.series" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series?series=${$encode(item.series)}`">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series?series=${$encode(item.series)}`">
|
||||||
<cards-series-search-card :series="item.series" :book-items="item.audiobooks" />
|
<cards-series-search-card :series="item.series" :book-items="item.audiobooks" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p>
|
||||||
|
<template v-for="item in tagResults">
|
||||||
|
<li :key="item.tag" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||||
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.tag)}`">
|
||||||
|
<cards-tag-search-card :tag="item.tag" />
|
||||||
|
</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,6 +73,7 @@ export default {
|
|||||||
audiobookResults: [],
|
audiobookResults: [],
|
||||||
authorResults: [],
|
authorResults: [],
|
||||||
seriesResults: [],
|
seriesResults: [],
|
||||||
|
tagResults: [],
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
lastSearch: null
|
lastSearch: null
|
||||||
}
|
}
|
||||||
@@ -76,19 +86,27 @@ export default {
|
|||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
totalResults() {
|
totalResults() {
|
||||||
return this.audiobookResults.length + this.seriesResults.length + this.authorResults.length
|
return this.audiobookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submitSearch() {
|
submitSearch() {
|
||||||
if (!this.search) return
|
if (!this.search) return
|
||||||
this.$router.push(`/library/${this.currentLibraryId}/bookshelf/search?query=${this.search}`)
|
var search = this.search
|
||||||
|
this.clearResults()
|
||||||
|
this.$router.push(`/library/${this.currentLibraryId}/bookshelf/search?query=${search}`)
|
||||||
|
},
|
||||||
|
clearResults() {
|
||||||
this.search = null
|
this.search = null
|
||||||
|
this.lastSearch = null
|
||||||
this.audiobookResults = []
|
this.audiobookResults = []
|
||||||
this.authorResults = []
|
this.authorResults = []
|
||||||
this.seriesResults = []
|
this.seriesResults = []
|
||||||
|
this.tagResults = []
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
|
this.isFetching = false
|
||||||
|
this.isTyping = false
|
||||||
|
clearTimeout(this.searchTimeout)
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.input) {
|
if (this.$refs.input) {
|
||||||
this.$refs.input.blur()
|
this.$refs.input.blur()
|
||||||
@@ -117,9 +135,14 @@ export default {
|
|||||||
console.error('Search error', error)
|
console.error('Search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Search was canceled
|
||||||
|
if (!this.isFetching) return
|
||||||
|
|
||||||
this.audiobookResults = searchResults.audiobooks || []
|
this.audiobookResults = searchResults.audiobooks || []
|
||||||
this.authorResults = searchResults.authors || []
|
this.authorResults = searchResults.authors || []
|
||||||
this.seriesResults = searchResults.series || []
|
this.seriesResults = searchResults.series || []
|
||||||
|
this.tagResults = searchResults.tags || []
|
||||||
|
|
||||||
this.isFetching = false
|
this.isFetching = false
|
||||||
if (!this.showMenu) {
|
if (!this.showMenu) {
|
||||||
@@ -135,19 +158,15 @@ export default {
|
|||||||
}
|
}
|
||||||
this.isTyping = true
|
this.isTyping = true
|
||||||
this.searchTimeout = setTimeout(() => {
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
// Canceled search
|
||||||
|
if (!this.isTyping) return
|
||||||
|
|
||||||
this.isTyping = false
|
this.isTyping = false
|
||||||
this.runSearch(val)
|
this.runSearch(val)
|
||||||
}, 750)
|
}, 750)
|
||||||
},
|
},
|
||||||
clickClear() {
|
clickClear() {
|
||||||
if (this.search) {
|
this.clearResults()
|
||||||
this.search = null
|
|
||||||
this.lastSearch = null
|
|
||||||
this.audiobookResults = []
|
|
||||||
this.authorResults = []
|
|
||||||
this.seriesResults = []
|
|
||||||
this.showMenu = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
|
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
|
||||||
<p v-if="!coversFound.length">No Covers Found</p>
|
<p v-if="!coversFound.length">No Covers Found</p>
|
||||||
<template v-for="cover in coversFound">
|
<template v-for="cover in coversFound">
|
||||||
<div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
<div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
|
||||||
<cards-preview-cover :src="cover" :width="80" show-open-new-tab />
|
<cards-preview-cover :src="cover" :width="80" show-open-new-tab />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -79,8 +79,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Path from 'path'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
processing: Boolean,
|
processing: Boolean,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ module.exports = {
|
|||||||
|
|
||||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||||
head: {
|
head: {
|
||||||
title: 'AudioBookshelf',
|
title: 'Audiobookshelf',
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: 'en'
|
lang: 'en'
|
||||||
},
|
},
|
||||||
@@ -35,8 +35,8 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
||||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Fira+Mono&family=Ubuntu+Mono&family=Gentium+Book+Basic&&family=Source+Sans+Pro:wght@300;400;600' },
|
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Source+Sans+Pro:wght@300;400;600' },
|
||||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }
|
// { rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -98,8 +98,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Build Configuration: https://go.nuxtjs.dev/config-build
|
// Build Configuration: https://go.nuxtjs.dev/config-build
|
||||||
build: {
|
build: {},
|
||||||
},
|
|
||||||
watchers: {
|
watchers: {
|
||||||
webpack: {
|
webpack: {
|
||||||
aggregateTimeout: 300,
|
aggregateTimeout: 300,
|
||||||
|
|||||||
Generated
+6
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.4.8",
|
"version": "1.4.10",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7385,6 +7385,11 @@
|
|||||||
"launch-editor": "^2.2.1"
|
"launch-editor": "^2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"libarchive.js": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/libarchive.js/-/libarchive.js-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg=="
|
||||||
|
},
|
||||||
"lie": {
|
"lie": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.4.9",
|
"version": "1.4.11",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
|
"libarchive.js": "^1.3.0",
|
||||||
"nuxt": "^2.15.7",
|
"nuxt": "^2.15.7",
|
||||||
"nuxt-socket-io": "^1.1.18",
|
"nuxt-socket-io": "^1.1.18",
|
||||||
"vue-pdf": "^4.3.0",
|
"vue-pdf": "^4.3.0",
|
||||||
|
|||||||
@@ -56,6 +56,28 @@
|
|||||||
</li>
|
</li>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
|
||||||
|
<div v-if="showExperimentalFeatures" class="p-4">
|
||||||
|
<ui-btn :loading="checkingTrackNumbers" small @click="checkTrackNumbers">Check Track Numbers</ui-btn>
|
||||||
|
<div v-if="trackNumData && trackNumData.length" class="w-full max-w-4xl py-2">
|
||||||
|
<table class="tracksTable">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">Filename</th>
|
||||||
|
<th class="w-32">Index</th>
|
||||||
|
<th class="w-32"># From Metadata</th>
|
||||||
|
<th class="w-32"># From Filename</th>
|
||||||
|
<th class="w-32"># From Probe</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="trackData in trackNumData" :key="trackData.filename">
|
||||||
|
<td class="text-xs">{{ trackData.filename }}</td>
|
||||||
|
<td class="text-center">{{ trackData.currentTrackNum }}</td>
|
||||||
|
<td class="text-center">{{ trackData.trackNumFromMeta }}</td>
|
||||||
|
<td class="text-center">{{ trackData.trackNumFromFilename }}</td>
|
||||||
|
<td class="text-center">{{ trackData.scanDataTrackNum }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -95,7 +117,9 @@ export default {
|
|||||||
group: 'description',
|
group: 'description',
|
||||||
ghostClass: 'ghost'
|
ghostClass: 'ghost'
|
||||||
},
|
},
|
||||||
saving: false
|
saving: false,
|
||||||
|
checkingTrackNumbers: false,
|
||||||
|
trackNumData: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -172,9 +196,26 @@ export default {
|
|||||||
},
|
},
|
||||||
streamAudiobook() {
|
streamAudiobook() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
|
},
|
||||||
|
showExperimentalFeatures() {
|
||||||
|
return this.$store.state.showExperimentalFeatures
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
checkTrackNumbers() {
|
||||||
|
this.checkingTrackNumbers = true
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/scantracks/${this.audiobookId}`)
|
||||||
|
.then((res) => {
|
||||||
|
console.log('RES', res)
|
||||||
|
this.trackNumData = res
|
||||||
|
this.checkingTrackNumbers = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.checkingTrackNumbers = false
|
||||||
|
})
|
||||||
|
},
|
||||||
includeToggled(audio) {
|
includeToggled(audio) {
|
||||||
var new_index = 0
|
var new_index = 0
|
||||||
if (audio.include) {
|
if (audio.include) {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-btn v-if="showExperimentalFeatures && (epubEbook || mobiEbook)" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
<ui-btn v-if="showExperimentalFeatures && numEbooks" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
||||||
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
|
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
|
||||||
Read
|
Read
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
@@ -322,16 +322,14 @@ export default {
|
|||||||
return this.audiobook.ebooks
|
return this.audiobook.ebooks
|
||||||
},
|
},
|
||||||
showEpubAlert() {
|
showEpubAlert() {
|
||||||
return this.ebooks.length && !this.epubEbook && !this.tracks.length
|
return this.ebooks.length && !this.numEbooks && !this.tracks.length
|
||||||
},
|
},
|
||||||
showExperimentalReadAlert() {
|
showExperimentalReadAlert() {
|
||||||
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
|
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
epubEbook() {
|
numEbooks() {
|
||||||
return this.ebooks.find((eb) => eb.ext === '.epub')
|
// Number of currently supported for reading ebooks, not all ebooks
|
||||||
},
|
return this.audiobook.numEbooks
|
||||||
mobiEbook() {
|
|
||||||
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
|
|
||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
|||||||
@@ -19,27 +19,37 @@ export default {
|
|||||||
return redirect('/oops?message=Library not found')
|
return redirect('/oops?message=Library not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set filter by
|
||||||
if (query.filter) {
|
if (query.filter) {
|
||||||
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
||||||
}
|
}
|
||||||
var searchResults = []
|
|
||||||
|
// Search page
|
||||||
|
var searchResults = {}
|
||||||
|
var audiobookSearchResults = []
|
||||||
var searchQuery = null
|
var searchQuery = null
|
||||||
if (params.id === 'search' && query.query) {
|
if (params.id === 'search' && query.query) {
|
||||||
searchQuery = query.query
|
searchQuery = query.query
|
||||||
searchResults = await app.$axios.$get(`/api/library/${libraryId}/audiobooks?q=${query.query}`).catch((error) => {
|
|
||||||
|
searchResults = await app.$axios.$get(`/api/library/${libraryId}/search?q=${searchQuery}`).catch((error) => {
|
||||||
console.error('Search error', error)
|
console.error('Search error', error)
|
||||||
return []
|
return {}
|
||||||
})
|
})
|
||||||
|
audiobookSearchResults = searchResults.audiobooks || []
|
||||||
store.commit('audiobooks/setSearchResults', searchResults)
|
store.commit('audiobooks/setSearchResults', searchResults)
|
||||||
if (searchResults.length) searchResults.forEach((ab) => store.commit('audiobooks/addUpdate', ab))
|
if (audiobookSearchResults.length) audiobookSearchResults.forEach((ab) => store.commit('audiobooks/addUpdate', ab.audiobook))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Series page
|
||||||
var selectedSeries = query.series ? app.$decode(query.series) : null
|
var selectedSeries = query.series ? app.$decode(query.series) : null
|
||||||
store.commit('audiobooks/setSelectedSeries', selectedSeries)
|
store.commit('audiobooks/setSelectedSeries', selectedSeries)
|
||||||
|
|
||||||
var libraryPage = params.id || ''
|
var libraryPage = params.id || ''
|
||||||
store.commit('audiobooks/setLibraryPage', libraryPage)
|
store.commit('audiobooks/setLibraryPage', libraryPage)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: libraryPage,
|
id: libraryPage,
|
||||||
|
libraryId,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
searchResults,
|
searchResults,
|
||||||
selectedSeries
|
selectedSeries
|
||||||
@@ -65,9 +75,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async newQuery() {
|
async newQuery() {
|
||||||
var query = this.$route.query.query
|
var query = this.$route.query.query
|
||||||
this.searchResults = await this.$axios.$get(`/api/audiobooks?q=${query}`).catch((error) => {
|
this.searchResults = await this.$axios.$get(`/api/library/${this.libraryId}/search?q=${query}`).catch((error) => {
|
||||||
console.error('Search error', error)
|
console.error('Search error', error)
|
||||||
return []
|
return {}
|
||||||
})
|
})
|
||||||
this.searchQuery = query
|
this.searchQuery = query
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -14,7 +14,8 @@ export const state = () => ({
|
|||||||
keywordFilter: null,
|
keywordFilter: null,
|
||||||
selectedSeries: null,
|
selectedSeries: null,
|
||||||
libraryPage: null,
|
libraryPage: null,
|
||||||
searchResults: []
|
searchResults: {},
|
||||||
|
searchResultAudiobooks: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -25,7 +26,7 @@ export const getters = {
|
|||||||
if (!state.libraryPage) {
|
if (!state.libraryPage) {
|
||||||
return getters.getFiltered()
|
return getters.getFiltered()
|
||||||
} else if (state.libraryPage === 'search') {
|
} else if (state.libraryPage === 'search') {
|
||||||
return state.searchResults
|
return state.searchResultAudiobooks
|
||||||
} else if (state.libraryPage === 'series') {
|
} else if (state.libraryPage === 'series') {
|
||||||
var series = getters.getSeriesGroups()
|
var series = getters.getSeriesGroups()
|
||||||
if (state.selectedSeries) {
|
if (state.selectedSeries) {
|
||||||
@@ -222,6 +223,7 @@ export const mutations = {
|
|||||||
},
|
},
|
||||||
setSearchResults(state, val) {
|
setSearchResults(state, val) {
|
||||||
state.searchResults = val
|
state.searchResults = val
|
||||||
|
state.searchResultAudiobooks = val && val.audiobooks ? val.audiobooks.map(ab => ab.audiobook) : []
|
||||||
},
|
},
|
||||||
set(state, audiobooks) {
|
set(state, audiobooks) {
|
||||||
// GENRES
|
// GENRES
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.4.9",
|
"version": "1.4.11",
|
||||||
"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": {
|
||||||
|
|||||||
+38
-4
@@ -1,10 +1,13 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
|
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
const User = require('./objects/User')
|
|
||||||
const { isObject } = require('./utils/index')
|
const { isObject } = require('./utils/index')
|
||||||
|
const audioFileScanner = require('./utils/audioFileScanner')
|
||||||
|
|
||||||
const Library = require('./objects/Library')
|
const Library = require('./objects/Library')
|
||||||
|
const User = require('./objects/User')
|
||||||
|
|
||||||
class ApiController {
|
class ApiController {
|
||||||
constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, emitter, clientEmitter) {
|
constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, emitter, clientEmitter) {
|
||||||
@@ -77,6 +80,8 @@ class ApiController {
|
|||||||
this.router.get('/download/:id', this.download.bind(this))
|
this.router.get('/download/:id', this.download.bind(this))
|
||||||
|
|
||||||
this.router.get('/filesystem', this.getFileSystemPaths.bind(this))
|
this.router.get('/filesystem', this.getFileSystemPaths.bind(this))
|
||||||
|
|
||||||
|
this.router.get('/scantracks/:id', this.scanAudioTrackNums.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
find(req, res) {
|
find(req, res) {
|
||||||
@@ -143,15 +148,18 @@ class ApiController {
|
|||||||
var bookMatches = []
|
var bookMatches = []
|
||||||
var authorMatches = {}
|
var authorMatches = {}
|
||||||
var seriesMatches = {}
|
var seriesMatches = {}
|
||||||
|
var tagMatches = {}
|
||||||
|
|
||||||
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
|
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
|
||||||
audiobooksInLibrary.forEach((ab) => {
|
audiobooksInLibrary.forEach((ab) => {
|
||||||
var queryResult = ab.searchQuery(req.query.q)
|
var queryResult = ab.searchQuery(req.query.q)
|
||||||
if (queryResult.book) {
|
if (queryResult.book) {
|
||||||
bookMatches.push({
|
var bookMatchObj = {
|
||||||
audiobook: ab,
|
audiobook: ab,
|
||||||
matchKey: queryResult.book
|
matchKey: queryResult.book,
|
||||||
})
|
matchText: queryResult.bookMatchText
|
||||||
|
}
|
||||||
|
bookMatches.push(bookMatchObj)
|
||||||
}
|
}
|
||||||
if (queryResult.author && !authorMatches[queryResult.author]) {
|
if (queryResult.author && !authorMatches[queryResult.author]) {
|
||||||
authorMatches[queryResult.author] = {
|
authorMatches[queryResult.author] = {
|
||||||
@@ -168,10 +176,23 @@ class ApiController {
|
|||||||
seriesMatches[queryResult.series].audiobooks.push(ab)
|
seriesMatches[queryResult.series].audiobooks.push(ab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (queryResult.tags && queryResult.tags.length) {
|
||||||
|
queryResult.tags.forEach((tag) => {
|
||||||
|
if (!tagMatches[tag]) {
|
||||||
|
tagMatches[tag] = {
|
||||||
|
tag,
|
||||||
|
audiobooks: [ab]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tagMatches[tag].audiobooks.push(ab)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
audiobooks: bookMatches.slice(0, maxResults),
|
audiobooks: bookMatches.slice(0, maxResults),
|
||||||
|
tags: Object.values(tagMatches).slice(0, maxResults),
|
||||||
authors: Object.values(authorMatches).slice(0, maxResults),
|
authors: Object.values(authorMatches).slice(0, maxResults),
|
||||||
series: Object.values(seriesMatches).slice(0, maxResults)
|
series: Object.values(seriesMatches).slice(0, maxResults)
|
||||||
})
|
})
|
||||||
@@ -783,5 +804,18 @@ class ApiController {
|
|||||||
var dirs = await this.getDirectories(global.appRoot, '/', excludedDirs)
|
var dirs = await this.getDirectories(global.appRoot, '/', excludedDirs)
|
||||||
res.json(dirs)
|
res.json(dirs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async scanAudioTrackNums(req, res) {
|
||||||
|
if (!req.user || !req.user.isRoot) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
|
||||||
|
if (!audiobook) {
|
||||||
|
return res.status(404).send('Audiobook not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
var scandata = await audioFileScanner.scanTrackNumbers(audiobook)
|
||||||
|
res.json(scandata)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = ApiController
|
module.exports = ApiController
|
||||||
+5
-34
@@ -82,10 +82,14 @@ class Db {
|
|||||||
await this.load()
|
await this.load()
|
||||||
|
|
||||||
// Insert Defaults
|
// Insert Defaults
|
||||||
if (!this.users.find(u => u.type === 'root')) {
|
var rootUser = this.users.find(u => u.type === 'root')
|
||||||
|
if (!rootUser) {
|
||||||
var token = await jwt.sign({ userId: 'root' }, process.env.TOKEN_SECRET)
|
var token = await jwt.sign({ userId: 'root' }, process.env.TOKEN_SECRET)
|
||||||
Logger.debug('Generated default token', token)
|
Logger.debug('Generated default token', token)
|
||||||
|
Logger.info('[Db] Root user created')
|
||||||
await this.insertEntity('user', this.getDefaultUser(token))
|
await this.insertEntity('user', this.getDefaultUser(token))
|
||||||
|
} else {
|
||||||
|
Logger.info(`[Db] Root user exists, pw: ${rootUser.hasPw}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.libraries.length) {
|
if (!this.libraries.length) {
|
||||||
@@ -123,19 +127,6 @@ class Db {
|
|||||||
await Promise.all([p1, p2, p3, p4])
|
await Promise.all([p1, p2, p3, p4])
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertAudiobook(audiobook) {
|
|
||||||
// return this.insertAudiobooks([audiobook])
|
|
||||||
// }
|
|
||||||
|
|
||||||
// insertAudiobooks(audiobooks) {
|
|
||||||
// return this.audiobooksDb.insert(audiobooks).then((results) => {
|
|
||||||
// Logger.debug(`[DB] Inserted ${results.inserted} audiobooks`)
|
|
||||||
// this.audiobooks = this.audiobooks.concat(audiobooks)
|
|
||||||
// }).catch((error) => {
|
|
||||||
// Logger.error(`[DB] Insert audiobooks Failed ${error}`)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
updateAudiobook(audiobook) {
|
updateAudiobook(audiobook) {
|
||||||
return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => {
|
return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => {
|
||||||
Logger.debug(`[DB] Audiobook updated ${results.updated}`)
|
Logger.debug(`[DB] Audiobook updated ${results.updated}`)
|
||||||
@@ -146,26 +137,6 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertUser(user) {
|
|
||||||
// return this.usersDb.insert([user]).then((results) => {
|
|
||||||
// Logger.debug(`[DB] Inserted user ${results.inserted}`)
|
|
||||||
// this.users.push(user)
|
|
||||||
// return true
|
|
||||||
// }).catch((error) => {
|
|
||||||
// Logger.error(`[DB] Insert user Failed ${error}`)
|
|
||||||
// return false
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// insertSettings(settings) {
|
|
||||||
// return this.settingsDb.insert([settings]).then((results) => {
|
|
||||||
// Logger.debug(`[DB] Inserted ${results.inserted} settings`)
|
|
||||||
// this.settings = this.settings.concat(settings)
|
|
||||||
// }).catch((error) => {
|
|
||||||
// Logger.error(`[DB] Insert settings Failed ${error}`)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
updateUserStream(userId, streamId) {
|
updateUserStream(userId, streamId) {
|
||||||
return this.usersDb.update((record) => record.id === userId, (user) => {
|
return this.usersDb.update((record) => record.id === userId, (user) => {
|
||||||
user.stream = streamId
|
user.stream = streamId
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ class Audiobook {
|
|||||||
return this.ebooks.find(file => file.ext === '.pdf')
|
return this.ebooks.find(file => file.ext === '.pdf')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasComic() {
|
||||||
|
return this.ebooks.find(file => file.ext === '.cbr' || file.ext === '.cbz')
|
||||||
|
}
|
||||||
|
|
||||||
get hasMissingIno() {
|
get hasMissingIno() {
|
||||||
return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || this._tracks.find(t => !t.ino)
|
return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || this._tracks.find(t => !t.ino)
|
||||||
}
|
}
|
||||||
@@ -210,7 +214,7 @@ class Audiobook {
|
|||||||
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
|
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
|
||||||
// numEbooks: this.ebooks.length,
|
// numEbooks: this.ebooks.length,
|
||||||
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
|
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
|
||||||
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf) ? 1 : 0, // Only supporting epubs in the reader currently
|
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf || this.hasComic) ? 1 : 0,
|
||||||
numTracks: this.tracks.length,
|
numTracks: this.tracks.length,
|
||||||
chapters: this.chapters || [],
|
chapters: this.chapters || [],
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
@@ -237,7 +241,7 @@ class Audiobook {
|
|||||||
audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
|
audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
|
||||||
otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
|
otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
|
||||||
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
|
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
|
||||||
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf) ? 1 : 0,
|
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf || this.hasComic) ? 1 : 0,
|
||||||
numTracks: this.tracks.length,
|
numTracks: this.tracks.length,
|
||||||
tags: this.tags,
|
tags: this.tags,
|
||||||
book: this.bookToJSON(),
|
book: this.bookToJSON(),
|
||||||
@@ -650,11 +654,22 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSearchMatch(search) {
|
isSearchMatch(search) {
|
||||||
return this.book.isSearchMatch(search.toLowerCase().trim())
|
var tagMatch = this.tags.filter(tag => {
|
||||||
|
return tag.toLowerCase().includes(search.toLowerCase().trim())
|
||||||
|
})
|
||||||
|
return this.book.isSearchMatch(search.toLowerCase().trim()) || tagMatch.length
|
||||||
}
|
}
|
||||||
|
|
||||||
searchQuery(search) {
|
searchQuery(search) {
|
||||||
return this.book.getQueryMatches(search.toLowerCase().trim())
|
var matches = this.book.getQueryMatches(search.toLowerCase().trim())
|
||||||
|
matches.tags = this.tags.filter(tag => {
|
||||||
|
return tag.toLowerCase().includes(search.toLowerCase().trim())
|
||||||
|
})
|
||||||
|
if (!matches.book && matches.tags.length) {
|
||||||
|
matches.book = 'tags'
|
||||||
|
matches.bookMatchText = matches.tags.join(', ')
|
||||||
|
}
|
||||||
|
return matches
|
||||||
}
|
}
|
||||||
|
|
||||||
getAudioFileByIno(ino) {
|
getAudioFileByIno(ino) {
|
||||||
|
|||||||
@@ -220,11 +220,16 @@ class Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getQueryMatches(search) {
|
getQueryMatches(search) {
|
||||||
var titleMatch = this._title.toLowerCase().includes(search) || this._subtitle.toLowerCase().includes(search)
|
var titleMatch = this._title.toLowerCase().includes(search)
|
||||||
|
var subtitleMatch = this._subtitle.toLowerCase().includes(search)
|
||||||
var authorMatch = this._author.toLowerCase().includes(search)
|
var authorMatch = this._author.toLowerCase().includes(search)
|
||||||
var seriesMatch = this._series.toLowerCase().includes(search)
|
var seriesMatch = this._series.toLowerCase().includes(search)
|
||||||
|
|
||||||
|
var bookMatchKey = titleMatch ? 'title' : subtitleMatch ? 'subtitle' : authorMatch ? 'author' : seriesMatch ? 'series' : false
|
||||||
|
var bookMatchText = bookMatchKey ? this[bookMatchKey] : ''
|
||||||
return {
|
return {
|
||||||
book: titleMatch ? 'title' : authorMatch ? 'author' : seriesMatch ? 'series' : false,
|
book: bookMatchKey,
|
||||||
|
bookMatchText,
|
||||||
author: authorMatch ? this._author : false,
|
author: authorMatch ? this._author : false,
|
||||||
series: seriesMatch ? this._series : false
|
series: seriesMatch ? this._series : false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,15 @@ class User {
|
|||||||
get canUpload() {
|
get canUpload() {
|
||||||
return !!this.permissions.upload && this.isActive
|
return !!this.permissions.upload && this.isActive
|
||||||
}
|
}
|
||||||
|
get hasPw() {
|
||||||
|
return !!this.pash && !!this.pash.length
|
||||||
|
}
|
||||||
|
|
||||||
getDefaultUserSettings() {
|
getDefaultUserSettings() {
|
||||||
return {
|
return {
|
||||||
|
mobileOrderBy: 'recent',
|
||||||
|
mobileOrderDesc: true,
|
||||||
|
mobileFilterBy: 'all',
|
||||||
orderBy: 'book.title',
|
orderBy: 'book.title',
|
||||||
orderDesc: false,
|
orderDesc: false,
|
||||||
filterBy: 'all',
|
filterBy: 'all',
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
|
|||||||
var scanData = await scan(audioFile.fullPath)
|
var scanData = await scan(audioFile.fullPath)
|
||||||
if (!scanData || scanData.error) {
|
if (!scanData || scanData.error) {
|
||||||
Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
|
Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
|
||||||
// audiobook.invalidAudioFiles.push(parts[i])
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,4 +232,27 @@ async function rescanAudioFiles(audiobook) {
|
|||||||
|
|
||||||
return updates
|
return updates
|
||||||
}
|
}
|
||||||
module.exports.rescanAudioFiles = rescanAudioFiles
|
module.exports.rescanAudioFiles = rescanAudioFiles
|
||||||
|
|
||||||
|
async function scanTrackNumbers(audiobook) {
|
||||||
|
var tracks = audiobook.tracks || []
|
||||||
|
var scannedTrackNumData = []
|
||||||
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
|
var track = tracks[i]
|
||||||
|
var scanData = await scan(track.fullPath)
|
||||||
|
|
||||||
|
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
||||||
|
var book = audiobook.book || {}
|
||||||
|
var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, track.filename)
|
||||||
|
Logger.info(`[AudioFileScanner] Track # for "${track.filename}", Metadata: "${trackNumFromMeta}", Filename: "${trackNumFromFilename}", Current: "${track.index}"`)
|
||||||
|
scannedTrackNumData.push({
|
||||||
|
filename: track.filename,
|
||||||
|
currentTrackNum: track.index,
|
||||||
|
trackNumFromFilename,
|
||||||
|
trackNumFromMeta,
|
||||||
|
scanDataTrackNum: scanData.file_tag_track
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return scannedTrackNumData
|
||||||
|
}
|
||||||
|
module.exports.scanTrackNumbers = scanTrackNumbers
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
const globals = {
|
const globals = {
|
||||||
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
||||||
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4'],
|
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4'],
|
||||||
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3']
|
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz']
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = globals
|
module.exports = globals
|
||||||
|
|||||||
Reference in New Issue
Block a user