mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-27 11:41:36 +00:00
merge upstream
This commit is contained in:
commit
61bf30e08a
89 changed files with 2963 additions and 1805 deletions
120
client/components/cards/BookUploadCard.vue
Normal file
120
client/components/cards/BookUploadCard.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<div class="relative w-full py-4 px-6 border border-white border-opacity-10 shadow-lg rounded-md my-6">
|
||||
<div class="absolute -top-3 -left-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full">
|
||||
<p class="text-base text-white text-opacity-80 font-mono">#{{ book.index }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')">
|
||||
<span class="text-base text-white text-opacity-80 font-mono material-icons">close</span>
|
||||
</div>
|
||||
|
||||
<template v-if="!uploadSuccess && !uploadFailed">
|
||||
<widgets-alert v-if="error" type="error">
|
||||
<p class="text-base">{{ error }}</p>
|
||||
</widgets-alert>
|
||||
|
||||
<div class="flex my-2 -mx-2">
|
||||
<div class="w-1/2 px-2">
|
||||
<ui-text-input-with-label v-model="bookData.title" :disabled="processing" label="Title" @input="titleUpdated" />
|
||||
</div>
|
||||
<div class="w-1/2 px-2">
|
||||
<ui-text-input-with-label v-model="bookData.author" :disabled="processing" label="Author" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex my-2 -mx-2">
|
||||
<div class="w-1/2 px-2">
|
||||
<ui-text-input-with-label v-model="bookData.series" :disabled="processing" label="Series" note="(optional)" />
|
||||
</div>
|
||||
<div class="w-1/2 px-2">
|
||||
<div class="w-full">
|
||||
<p class="px-1 text-sm font-semibold">Directory <em class="font-normal text-xs pl-2">(auto)</em></p>
|
||||
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<tables-uploaded-files-table :files="book.bookFiles" title="Book Files" class="mt-8" />
|
||||
<tables-uploaded-files-table v-if="book.otherFiles.length" title="Other Files" :files="book.otherFiles" />
|
||||
<tables-uploaded-files-table v-if="book.ignoredFiles.length" title="Ignored Files" :files="book.ignoredFiles" />
|
||||
</template>
|
||||
<widgets-alert v-if="uploadSuccess" type="success">
|
||||
<p class="text-base">Successfully Uploaded!</p>
|
||||
</widgets-alert>
|
||||
<widgets-alert v-if="uploadFailed" type="error">
|
||||
<p class="text-base">Failed to upload</p>
|
||||
</widgets-alert>
|
||||
|
||||
<div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
|
||||
<ui-loading-indicator text="Uploading..." />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Path from 'path'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
book: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
processing: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bookData: {
|
||||
title: '',
|
||||
author: '',
|
||||
series: ''
|
||||
},
|
||||
error: '',
|
||||
isUploading: false,
|
||||
uploadFailed: false,
|
||||
uploadSuccess: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
directory() {
|
||||
if (!this.bookData.title) return ''
|
||||
if (this.bookData.series && this.bookData.author) {
|
||||
return Path.join(this.bookData.author, this.bookData.series, this.bookData.title)
|
||||
} else if (this.bookData.author) {
|
||||
return Path.join(this.bookData.author, this.bookData.title)
|
||||
} else {
|
||||
return this.bookData.title
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setUploadStatus(status) {
|
||||
this.isUploading = status === 'uploading'
|
||||
this.uploadFailed = status === 'failed'
|
||||
this.uploadSuccess = status === 'success'
|
||||
},
|
||||
titleUpdated() {
|
||||
this.error = ''
|
||||
},
|
||||
getData() {
|
||||
if (!this.bookData.title) {
|
||||
this.error = 'Must have a title'
|
||||
return null
|
||||
}
|
||||
this.error = ''
|
||||
var files = this.book.bookFiles.concat(this.book.otherFiles)
|
||||
return {
|
||||
index: this.book.index,
|
||||
...this.bookData,
|
||||
files
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.book) {
|
||||
this.bookData.title = this.book.title
|
||||
this.bookData.author = this.book.author
|
||||
this.bookData.series = this.book.series
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -5,11 +5,13 @@
|
|||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
<div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${sizeMultiplier * 3}rem` }">
|
||||
<!-- Alternative bookshelf title/author/sort -->
|
||||
<div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||
<span v-if="volumeNumber">#{{ volumeNumber }} </span>{{ title }}
|
||||
<span v-if="volumeNumber">#{{ volumeNumber }} </span>{{ displayTitle }}
|
||||
</p>
|
||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ authorFL }}</p>
|
||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div>
|
||||
|
|
@ -38,13 +40,13 @@
|
|||
<!-- Overlay is not shown if collapsing series in library -->
|
||||
<div v-show="!booksInSeries && audiobook && (isHovering || isSelectionMode || isMoreMenuOpen)" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
|
||||
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
||||
<div class="hover:text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
|
||||
<div class="hover:text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="clickReadEBook">
|
||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="clickReadEBook">
|
||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -61,16 +63,20 @@
|
|||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Series name overlay -->
|
||||
<div v-if="booksInSeries && audiobook && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
|
||||
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Error widget -->
|
||||
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
|
||||
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- Volume number -->
|
||||
<div v-if="volumeNumber && showVolumeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ volumeNumber }}</p>
|
||||
</div>
|
||||
|
|
@ -99,7 +105,10 @@ export default {
|
|||
// Book can be passed as prop or set with setEntity()
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
orderBy: String,
|
||||
filterBy: String,
|
||||
sortingIgnorePrefix: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -114,6 +123,15 @@ export default {
|
|||
showCoverBg: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bookMount: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.audiobook = newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showExperimentalFeatures() {
|
||||
return this.store.state.showExperimentalFeatures
|
||||
|
|
@ -180,6 +198,24 @@ export default {
|
|||
volumeNumber() {
|
||||
return this.book.volumeNumber || null
|
||||
},
|
||||
displayTitle() {
|
||||
if (this.orderBy === 'book.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
|
||||
return this.title.substr(4) + ', The'
|
||||
}
|
||||
return this.title
|
||||
},
|
||||
displayAuthor() {
|
||||
if (this.orderBy === 'book.authorLF') return this.authorLF
|
||||
return this.authorFL
|
||||
},
|
||||
displaySortLine() {
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._audiobook.mtimeMs)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._audiobook.birthtimeMs)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._audiobook.addedAt)
|
||||
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this._audiobook.duration, false)
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._audiobook.size)
|
||||
return null
|
||||
},
|
||||
userProgress() {
|
||||
return this.store.getters['user/getUserAudiobook'](this.audiobookId)
|
||||
},
|
||||
|
|
@ -322,6 +358,11 @@ export default {
|
|||
isAlternativeBookshelfView() {
|
||||
var constants = this.$constants || this.$nuxt.$constants
|
||||
return this.bookshelfView === constants.BookshelfView.TITLES
|
||||
},
|
||||
titleDisplayBottomOffset() {
|
||||
if (!this.isAlternativeBookshelfView) return 0
|
||||
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
||||
return 4.25 * this.sizeMultiplier
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -461,8 +502,8 @@ export default {
|
|||
this.$emit('select', this.audiobook)
|
||||
},
|
||||
play() {
|
||||
this.store.commit('setStreamAudiobook', this.audiobook)
|
||||
this._socket.emit('open_stream', this.audiobookId)
|
||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||
eventBus.$emit('play-audiobook', this.audiobookId)
|
||||
},
|
||||
mouseover() {
|
||||
this.isHovering = true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue