Merge branch 'master' into groupcoverlimit

This commit is contained in:
mx03 2022-01-22 11:26:23 +01:00
commit a9eb64fdc6
60 changed files with 1650 additions and 545 deletions

View file

@ -18,6 +18,7 @@
}
#bookshelf {
height: calc(100% - 40px);
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f);
}
@media (max-width: 768px) {
#bookshelf {
@ -25,6 +26,10 @@
}
}
#page-wrapper {
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f);
}
/* width */
::-webkit-scrollbar {
width: 8px;
@ -121,10 +126,6 @@ input[type=number] {
font-size: 1.1rem;
}
#page-wrapper {
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f);
}
.box-shadow-md {
box-shadow: 2px 8px 6px #111111aa;
}

View file

@ -15,7 +15,7 @@
<span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
<nuxt-link v-if="isRootUser" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<nuxt-link to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<span class="material-icons">equalizer</span>
</nuxt-link>

View file

@ -3,29 +3,10 @@
<div class="md:hidden flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
<span class="material-icons text-2xl">arrow_back</span>
</div>
<nuxt-link to="/config" class="w-full px-4 h-12 border-b border-opacity-0 flex items-center cursor-pointer relative" :class="routeName === 'config' ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p>Settings</p>
<div v-show="routeName === 'config'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link to="/config/libraries" class="w-full px-4 h-12 border-b border-opacity-0 flex items-center cursor-pointer relative" :class="routeName === 'config-libraries' ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p>Libraries</p>
<div v-show="routeName === 'config-libraries'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link to="/config/users" class="w-full px-4 h-12 border-b border-opacity-0 flex items-center cursor-pointer relative" :class="routeName === 'config-users' ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p>Users</p>
<div v-show="routeName === 'config-users'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link to="/config/backups" class="w-full px-4 h-12 border-b border-opacity-0 flex items-center cursor-pointer relative" :class="routeName === 'config-backups' ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p>Backups</p>
<div v-show="routeName === 'config-backups'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link to="/config/log" class="w-full px-4 h-12 border-b border-opacity-0 flex items-center cursor-pointer relative" :class="routeName === 'config-log' ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p>Log</p>
<div v-show="routeName === 'config-log'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link to="/config/stats" class="w-full px-4 h-12 border-b border-opacity-0 flex items-center cursor-pointer relative" :class="routeName === 'config-stats' ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p>Stats</p>
<div v-show="routeName === 'config-stats'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-4 h-12 border-b border-opacity-0 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p>{{ route.title }}</p>
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div class="w-full h-10 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamAudiobook && windowHeight > 700 && !isMobile ? '300px' : '65px' }">
@ -47,6 +28,57 @@ export default {
}
},
computed: {
userIsRoot() {
return this.$store.getters['user/getIsRoot']
},
configRoutes() {
if (!this.userIsRoot) {
return [
{
id: 'config-stats',
title: 'Your Stats',
path: '/config/stats'
}
]
}
return [
{
id: 'config',
title: 'Settings',
path: '/config'
},
{
id: 'config-libraries',
title: 'Libraries',
path: '/config/libraries'
},
{
id: 'config-users',
title: 'Users',
path: '/config/users'
},
{
id: 'config-backups',
title: 'Backups',
path: '/config/backups'
},
{
id: 'config-log',
title: 'Log',
path: '/config/log'
},
{
id: 'config-library-stats',
title: 'Library Stats',
path: '/config/library-stats'
},
{
id: 'config-stats',
title: 'Your Stats',
path: '/config/stats'
}
]
},
wrapperClass() {
var classes = []
if (this.drawerOpen) classes.push('translate-x-0')

View file

@ -1,11 +1,11 @@
<template>
<div id="bookshelf" class="w-full overflow-y-auto">
<template v-for="shelf in totalShelves">
<div :key="shelf" class="w-full px-4 sm:px-8 bookshelfRow relative" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4 sm:px-8 relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
<!-- <div class="absolute top-0 left-0 bottom-0 p-4 z-10">
<p class="text-white text-2xl">{{ shelf }}</p>
</div> -->
<div class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20" :class="`h-${shelfDividerHeightIndex}`" />
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20" :class="`h-${shelfDividerHeightIndex}`" />
</div>
</template>
@ -18,6 +18,9 @@
</div>
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
<p class="text-xl text-center">{{ emptyMessage }}</p>
<div class="flex justify-center mt-2">
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">Clear Filter</ui-btn>
</div>
</div>
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" />
@ -86,6 +89,7 @@ export default {
emptyMessage() {
if (this.page === 'series') return `You have no series`
if (this.page === 'collections') return "You haven't made any collections yet"
if (this.hasFilter) return `No Results for filter "${this.filterValue}"`
return 'No results'
},
entityName() {
@ -104,15 +108,33 @@ export default {
coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio')
},
bookshelfView() {
return this.$store.getters['getServerSetting']('bookshelfView')
},
isCoverSquareAspectRatio() {
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
},
isAlternativeBookshelfView() {
if (!this.isEntityBook) return false // Only used for bookshelf showing books
return this.bookshelfView === this.$constants.BookshelfView.TITLES
},
bookCoverAspectRatio() {
return this.isCoverSquareAspectRatio ? 1 : 1.6
},
hasFilter() {
return this.filterBy && this.filterBy !== 'all'
},
filterName() {
if (!this.filterBy) return ''
var filter = this.filterBy.split('.')[0]
filter = filter.substr(0, 1).toUpperCase() + filter.substr(1)
return filter
},
filterValue() {
if (!this.filterBy) return ''
if (!this.filterBy.includes('.')) return ''
return this.$decode(this.filterBy.split('.')[1])
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
@ -149,6 +171,7 @@ export default {
return 6
},
shelfHeight() {
if (this.isAlternativeBookshelfView) return this.entityHeight + 80 * this.sizeMultiplier
return this.entityHeight + 40
},
totalEntityCardWidth() {
@ -157,12 +180,19 @@ export default {
},
selectedAudiobooks() {
return this.$store.state.selectedAudiobooks || []
},
sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.entityWidth / baseSize
}
},
methods: {
showBookshelfTextureModal() {
this.$store.commit('globals/setShowBookshelfTextureModal', true)
},
clearFilter() {
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
},
editEntity(entity) {
if (this.entityName === 'books' || this.entityName === 'series-books') {
var bookIds = this.entities.map((e) => e.id)

View file

@ -145,7 +145,7 @@ export default {
audioPlayerMounted() {
this.audioPlayerReady = true
if (this.stream) {
console.log('[STREAM-CONTAINER] audioPlayerMounted w/ Stream', this.stream)
console.log('[STREAM-CONTAINER] audioPlayer Mounted w/ Stream', this.stream)
this.openStream()
}
},

View file

@ -1,5 +1,5 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<div class="flex items-center h-full px-1 overflow-hidden">
<covers-book-cover :audiobook="audiobook" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="flex-grow px-2 audiobookSearchCardContent">
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
@ -10,7 +10,7 @@
<p v-if="matchKey !== 'authorFL'" class="text-xs text-gray-200 truncate">by {{ authorFL }}</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 v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
</div>
</div>
</template>
@ -70,6 +70,8 @@ export default {
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
if (this.matchKey === 'authorFL') return `by ${html}`
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
return `${html}`
}

View file

@ -5,6 +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` }">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
<span v-if="volumeNumber">#{{ volumeNumber }}&nbsp;</span>{{ title }}
</p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ authorFL }}</p>
</div>
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
<div v-show="audiobook && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
@ -79,6 +86,7 @@ export default {
},
bookCoverAspectRatio: Number,
showVolumeNumber: Boolean,
bookshelfView: Number,
bookMount: {
// Book can be passed as prop or set with setEntity()
type: Object,
@ -292,6 +300,10 @@ export default {
return this.authorFL.slice(0, 27) + '...'
}
return this.authorFL
},
isAlternativeBookshelfView() {
var constants = this.$constants || this.$nuxt.$constants
return this.bookshelfView === constants.BookshelfView.TITLES
}
},
methods: {

View file

@ -74,7 +74,6 @@ export default {
},
methods: {
setEntity(_series) {
console.log('setting entity', _series)
this.series = _series
},
setSelectionMode(val) {

View file

@ -14,7 +14,7 @@
</div>
</button>
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items">
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
@ -97,6 +97,11 @@ export default {
value: 'narrators',
sublist: true
},
{
text: 'Language',
value: 'languages',
sublist: true
},
{
text: 'Progress',
value: 'progress',
@ -155,6 +160,9 @@ export default {
narrators() {
return this.filterData.narrators || []
},
languages() {
return this.filterData.languages || []
},
progress() {
return ['Read', 'Unread', 'In Progress']
},

View file

@ -102,6 +102,9 @@ export default {
},
selectedBookIds() {
return this.$store.state.selectedAudiobooks || []
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
}
},
methods: {
@ -186,9 +189,10 @@ export default {
var books = this.showBatchUserCollectionModal ? this.selectedBookIds : [this.selectedAudiobookId]
var newCollection = {
books: books,
libraryId: this.selectedAudiobook.libraryId,
libraryId: this.currentLibraryId,
name: this.newCollectionName
}
this.$axios
.$post('/api/collections', newCollection)
.then((data) => {

View file

@ -2,9 +2,14 @@
<div class="w-full h-full relative">
<form class="w-full h-full" @submit.prevent="submitForm">
<div ref="formWrapper" class="px-4 py-6 details-form-wrapper w-full overflow-hidden overflow-y-auto">
<ui-text-input-with-label v-model="details.title" label="Title" />
<ui-text-input-with-label v-model="details.subtitle" label="Subtitle" class="mt-2" />
<div class="flex -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label v-model="details.title" label="Title" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="details.subtitle" label="Subtitle" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
@ -43,8 +48,17 @@
<ui-text-input-with-label v-model="details.publisher" label="Publisher" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="details.language" label="Language" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/3 px-1">
<ui-text-input-with-label v-model="details.isbn" label="ISBN" />
</div>
<div class="w-1/3 px-1">
<ui-text-input-with-label v-model="details.asin" label="ASIN" />
</div>
</div>
</div>
@ -90,7 +104,9 @@ export default {
volumeNumber: null,
publishYear: null,
publisher: null,
language: null,
isbn: null,
asin: null,
genres: []
},
newTags: [],
@ -231,7 +247,9 @@ export default {
this.details.volumeNumber = this.book.volumeNumber
this.details.publishYear = this.book.publishYear
this.details.publisher = this.book.publisher || null
this.details.language = this.book.language || null
this.details.isbn = this.book.isbn || null
this.details.asin = this.book.asin || null
this.newTags = this.audiobook.tags || []
},

View file

@ -65,10 +65,7 @@
<ui-checkbox v-model="selectedMatchUsage.publishYear" />
<ui-text-input-with-label v-model="selectedMatch.publishYear" :disabled="!selectedMatchUsage.publishYear" label="Publish Year" class="flex-grow ml-4" />
</div>
<div v-if="selectedMatch.isbn" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.isbn" />
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" class="flex-grow ml-4" />
</div>
<div v-if="selectedMatch.series" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.series" />
<ui-text-input-with-label v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" label="Series" class="flex-grow ml-4" />
@ -77,10 +74,14 @@
<ui-checkbox v-model="selectedMatchUsage.volumeNumber" />
<ui-text-input-with-label v-model="selectedMatch.volumeNumber" :disabled="!selectedMatchUsage.volumeNumber" label="Volume Number" class="flex-grow ml-4" />
</div>
<!-- <div v-if="selectedMatch.asin" class="flex items-center py-2">
<div v-if="selectedMatch.isbn" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.isbn" />
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" class="flex-grow ml-4" />
</div>
<div v-if="selectedMatch.asin" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.asin" />
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" class="flex-grow ml-4" />
</div> -->
</div>
<div class="flex items-center justify-end py-2">
<ui-btn color="success" type="submit">Update</ui-btn>
</div>
@ -104,20 +105,6 @@ export default {
searchTitle: null,
searchAuthor: null,
lastSearch: null,
providers: [
{
text: 'Google Books',
value: 'google'
},
{
text: 'Open Library',
value: 'openlibrary'
},
{
text: 'Audible',
value: 'audible'
}
],
provider: 'google',
searchResults: [],
hasSearched: false,
@ -129,11 +116,12 @@ export default {
author: true,
narrator: true,
description: true,
isbn: true,
publisher: true,
publishYear: true,
series: true,
volumeNumber: true
volumeNumber: true,
asin: true,
isbn: true
}
}
},
@ -156,6 +144,9 @@ export default {
},
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
providers() {
return this.$store.state.scanners.providers
}
},
methods: {
@ -205,11 +196,12 @@ export default {
author: true,
narrator: true,
description: true,
isbn: true,
publisher: true,
publishYear: true,
series: true,
volumeNumber: true
volumeNumber: true,
asin: true,
isbn: true
}
if (this.audiobook.id !== this.audiobookId) {

View file

@ -1,6 +1,6 @@
<template>
<div class="w-96 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Minutes Listening</h1>
<h1 class="text-2xl mb-4 font-book">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
<div class="relative w-96 h-72">
<div class="absolute top-0 left-0">
<template v-for="lbl in yAxisLabels">

View file

@ -1,6 +1,6 @@
<template>
<div class="flex flex-wrap justify-between mt-6">
<div class="flex p-2">
<div class="flex flex-wrap justify-center mt-6">
<div class="flex px-2">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
</svg>
@ -9,20 +9,8 @@
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Books in Library</p>
</div>
</div>
<div class="flex p-2">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z"
/>
</svg>
<div class="px-3">
<p class="text-4xl md:text-5xl font-bold">{{ userAudiobooksRead.length }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Books Read</p>
</div>
</div>
<div class="flex p-2">
<div class="flex px-4">
<span class="material-icons text-7xl">show_chart</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalAudiobookHours }}</p>
@ -30,7 +18,7 @@
</div>
</div>
<div class="flex p-2">
<div class="flex px-4">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
</svg>
@ -40,11 +28,19 @@
</div>
</div>
<div class="flex p-2">
<span class="material-icons-outlined" style="font-size: 4.1rem">watch_later</span>
<div class="flex px-4">
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Minutes Listening</p>
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Size ({{ totalSizeMod }})</p>
</div>
</div>
<div class="flex px-4">
<span class="material-icons-outlined text-6xl pt-1">audio_file</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Audio Tracks</p>
</div>
</div>
</div>
@ -53,10 +49,6 @@
<script>
export default {
props: {
listeningStats: {
type: Object,
default: () => {}
},
libraryStats: {
type: Object,
default: () => {}
@ -75,11 +67,8 @@ export default {
totalAuthors() {
return this.libraryStats ? this.libraryStats.totalAuthors : 0
},
userAudiobooks() {
return Object.values(this.user.audiobooks || {})
},
userAudiobooksRead() {
return this.userAudiobooks.filter((ab) => !!ab.isRead)
numAudioTracks() {
return this.libraryStats ? this.libraryStats.numAudioTracks : 0
},
totalAudiobookDuration() {
return this.libraryStats ? this.libraryStats.totalDuration : 0
@ -88,9 +77,15 @@ export default {
var totalHours = Math.round(this.totalAudiobookDuration / (60 * 60))
return totalHours
},
totalMinutesListening() {
if (!this.listeningStats) return 0
return Math.round(this.listeningStats.totalTime / 60)
totalSizePretty() {
var totalSize = this.libraryStats ? this.libraryStats.totalSize : 0
return this.$bytesPretty(totalSize, 1)
},
totalSizeNum() {
return this.totalSizePretty.split(' ')[0]
},
totalSizeMod() {
return this.totalSizePretty.split(' ')[1]
}
},
methods: {},

View file

@ -9,7 +9,7 @@
<draggable :list="libraryCopies" v-bind="dragOptions" class="list-group" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
<template v-for="library in libraryCopies">
<div :key="library.id" class="item">
<modals-libraries-library-item :library="library" :selected="currentLibraryId === library.id" :show-edit="true" :dragging="drag" @edit="editLibrary" @sort="draggableSort" @click="setLibrary" />
<modals-libraries-library-item :library="library" :selected="currentLibraryId === library.id" :show-edit="true" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
</div>
</template>
</draggable>

View file

@ -21,7 +21,7 @@
<td>
<div class="flex items-center">
<widgets-online-indicator :value="!!usersOnline[user.id]" />
<span class="pl-2">{{ user.username }}</span> <span v-show="$isDev" class="text-xs text-gray-400 italic pl-4">({{ user.id }})</span>
<p class="pl-2 truncate">{{ user.username }}</p>
</div>
</td>
<td class="text-sm">{{ user.type }}</td>

View file

@ -85,12 +85,24 @@ export default {
reconnectFailed() {
console.error('[SOCKET] reconnect failed')
},
init(payload) {
init(payload, count = 0) {
if (!this.$refs.streamContainer) {
if (count > 20) {
console.error('Stream container never mounted')
return
}
setTimeout(() => {
this.init(payload, ++count)
}, 100)
return
}
console.log('Init Payload', payload)
if (payload.stream) {
if (this.$refs.streamContainer) {
this.$store.commit('setStream', payload.stream)
this.$refs.streamContainer.streamOpen(payload.stream)
} else {
console.warn('Stream Container not mounted')
}
}
if (payload.user) {

View file

@ -51,7 +51,8 @@ export default {
index,
width: this.entityWidth,
height: this.entityHeight,
bookCoverAspectRatio: this.bookCoverAspectRatio
bookCoverAspectRatio: this.bookCoverAspectRatio,
bookshelfView: this.bookshelfView
}
if (this.entityName === 'series-books') props.showVolumeNumber = true

View file

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.6.48",
"version": "1.6.59",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {

View file

@ -22,7 +22,8 @@
<span class="text-white">Track From Metadata</span>
<span class="material-icons ml-1" :class="currentSort === 'metadata' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'metadata' ? 'expand_more' : 'unfold_more' }}</span>
</div>
<div class="font-mono w-20 text-center">CD From Filename</div>
<div class="font-mono w-20 text-center">Disc From Filename</div>
<div class="font-mono w-20 text-center">Disc From Metadata</div>
<div class="font-book text-center px-4 flex-grow flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilename" @mousedown.prevent>
<span class="text-white">Filename</span>
<span class="material-icons ml-1" :class="currentSort === 'filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }}</span>
@ -49,7 +50,10 @@
{{ audio.trackNumFromMeta }}
</div>
<div class="font-book truncate px-4 w-20">
{{ audio.cdNumFromFilename }}
{{ audio.discNumFromFilename }}
</div>
<div class="font-book truncate px-4 w-20">
{{ audio.discNumFromMeta }}
</div>
<div class="font-book truncate px-4 flex-grow">
{{ audio.filename }}

View file

@ -5,7 +5,7 @@
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 208px">
<div class="relative" style="height: fit-content">
<covers-book-cover :audiobook="audiobook" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
</div>
</div>
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
@ -125,7 +125,7 @@
</div>
<div class="my-4 max-w-2xl">
<p class="text-base text-gray-100">{{ description }}</p>
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
</div>
<div v-if="missingParts.length" class="bg-error border-red-800 shadow-md p-4">

View file

@ -67,10 +67,30 @@
</div>
</div>
<div class="flex mt-2 -mx-1">
<!-- <div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label v-model="audiobook.book.narrator" label="Narrator" />
</div>
</div> -->
<div class="flex mt-2 -mx-1">
<div class="w-1/3 px-1">
<ui-text-input-with-label v-model="audiobook.book.narrator" label="Narrator" />
</div>
<div class="w-1/3 px-1">
<ui-text-input-with-label v-model="audiobook.book.publisher" label="Publisher" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="audiobook.book.language" label="Language" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/3 px-1">
<ui-text-input-with-label v-model="audiobook.book.isbn" label="ISBN" />
</div>
<div class="w-1/3 px-1">
<ui-text-input-with-label v-model="audiobook.book.asin" label="ASIN" />
</div>
</div>
</div>
</div>
@ -223,7 +243,7 @@ export default {
return arr1.join(',') !== arr2.join(',')
},
compareAudiobooks(newAb, origAb) {
const bookKeysToCheck = ['title', 'subtitle', 'narrator', 'author', 'publishYear', 'series', 'volumeNumber', 'description']
const bookKeysToCheck = ['title', 'subtitle', 'narrator', 'author', 'publishYear', 'series', 'volumeNumber', 'description', 'language', 'publisher', 'isbn', 'asin']
var newBook = newAb.book
var origBook = origAb.book
var diffObj = {}

View file

@ -14,9 +14,12 @@
<script>
export default {
asyncData({ store, redirect }) {
asyncData({ store, redirect, route }) {
if (!store.getters['user/getIsRoot']) {
redirect('/?error=unauthorized')
// Non-Root user only has access to the listening stats page
if (route.name !== 'config-stats') {
redirect('/config/stats')
}
}
},
data() {
@ -38,7 +41,7 @@ export default {
currentPage() {
if (!this.$route.name) return 'Settings'
var routeName = this.$route.name.split('-')
if (routeName.length > 0) return routeName[1]
if (routeName.length > 0) return routeName.slice(1).join('-')
return 'Settings'
}
},
@ -72,7 +75,7 @@ export default {
width: 900px;
max-width: calc(100% - 176px);
}
.configContent.page-stats {
.configContent.page-library-stats {
width: 1200px;
}
@media (max-width: 1024px) {

View file

@ -7,6 +7,31 @@
<h1 class="text-xl">Settings</h1>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="storeCoversInAudiobookDir" :disabled="updatingServerSettings" @input="updateCoverStorageDestination" />
<ui-tooltip :text="coverDestinationTooltip">
<p class="pl-4 text-lg">Store covers with audiobook <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
<ui-tooltip :text="coverAspectRatioTooltip">
<p class="pl-4 text-lg">Use square book covers <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="useAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateAlternativeBookshelfView" />
<ui-tooltip :text="bookshelfViewTooltip">
<p class="pl-4 text-lg">Use alternative library bookshelf view <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
<div class="flex items-center mb-2 mt-8">
<h1 class="text-xl">Scanner Settings</h1>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" small :disabled="updatingServerSettings" @input="updateScannerParseSubtitle" />
<ui-tooltip :text="parseSubtitleTooltip">
@ -19,6 +44,10 @@
<ui-tooltip :text="scannerFindCoversTooltip">
<p class="pl-4 text-lg">Scanner find covers <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
<div class="flex-grow" />
</div>
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
</div>
<div class="flex items-center py-2">
@ -34,20 +63,6 @@
<p class="pl-4 text-lg">Scanner prefer OPF metadata <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="storeCoversInAudiobookDir" :disabled="updatingServerSettings" @input="updateCoverStorageDestination" />
<ui-tooltip :text="coverDestinationTooltip">
<p class="pl-4 text-lg">Store covers with audiobook <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
<ui-tooltip :text="coverAspectRatioTooltip">
<p class="pl-4 text-lg">Use square book covers <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
</div>
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
@ -78,9 +93,9 @@
</ui-tooltip>
</div>
</div>
<div class="hidden md:block">
<!-- <div class="hidden md:block">
<a href="https://github.com/advplyr/audiobookshelf/discussions/75#discussion-3604812" target="_blank" class="text-blue-500 hover:text-blue-300 underline">Join the discussion</a>
</div>
</div> -->
</div>
</div>
</div>
@ -94,6 +109,7 @@ export default {
storeCoversInAudiobookDir: false,
updatingServerSettings: false,
useSquareBookCovers: false,
useAlternativeBookshelfView: false,
isPurgingCache: false,
newServerSettings: {}
}
@ -134,6 +150,12 @@ export default {
coverAspectRatioTooltip() {
return 'Prefer to use square covers over standard 1.6:1 book covers'
},
bookshelfViewTooltip() {
return 'Alternative bookshelf view that shows title & author under book covers'
},
providers() {
return this.$store.state.scanners.providers
},
showExperimentalFeatures: {
get() {
return this.$store.state.showExperimentalFeatures
@ -149,6 +171,11 @@ export default {
scannerFindCovers: !!val
})
},
updateScannerCoverProvider(val) {
this.updateServerSettings({
scannerCoverProvider: val
})
},
updateCoverStorageDestination(val) {
this.newServerSettings.coverDestination = val ? this.$constants.CoverDestination.AUDIOBOOK : this.$constants.CoverDestination.METADATA
this.updateServerSettings({
@ -175,6 +202,11 @@ export default {
coverAspectRatio: val ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD
})
},
updateAlternativeBookshelfView(val) {
this.updateServerSettings({
bookshelfView: val ? this.$constants.BookshelfView.TITLES : this.$constants.BookshelfView.STANDARD
})
},
updateServerSettings(payload) {
this.updatingServerSettings = true
this.$store
@ -194,6 +226,8 @@ export default {
this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
this.useSquareBookCovers = this.newServerSettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
this.useAlternativeBookshelfView = this.newServerSettings.bookshelfView === this.$constants.BookshelfView.TITLES
},
resetAudiobooks() {
if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {

View file

@ -0,0 +1,127 @@
<template>
<div>
<p class="text-xl">Stats for library {{ currentLibraryName }}</p>
<stats-preview-icons :library-stats="libraryStats" />
<div class="flex md:flex-row flex-wrap justify-between flex-col mt-12">
<div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1>
<p v-if="!top5Genres.length">No Genres</p>
<template v-for="genre in top5Genres">
<div :key="genre.genre" class="w-full py-2">
<div class="flex items-end mb-1">
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalBooks) }}&nbsp;%</p>
<div class="flex-grow" />
<p class="text-base font-book text-white text-opacity-70">{{ genre.genre }}</p>
</div>
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalBooks) + '%' }" />
</div>
</div>
</template>
</div>
<div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Top 10 Authors</h1>
<p v-if="!top10Authors.length">No Authors</p>
<template v-for="(author, index) in top10Authors">
<div :key="author.author" class="w-full py-2">
<div class="flex items-center mb-1">
<p class="text-sm font-book text-white text-opacity-70 w-36 pr-2 truncate">{{ index + 1 }}.&nbsp;&nbsp;&nbsp;&nbsp;{{ author.author }}</p>
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
</div>
<div class="w-4 ml-3">
<p class="text-sm font-bold">{{ author.count }}</p>
</div>
</div>
</div>
</template>
</div>
<div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Longest Audiobooks (hrs)</h1>
<p v-if="!top10LongestAudiobooks.length">No Audiobooks</p>
<template v-for="(ab, index) in top10LongestAudiobooks">
<div :key="index" class="w-full py-2">
<div class="flex items-center mb-1">
<p class="text-sm font-book text-white text-opacity-70 w-44 pr-2 truncate">{{ index + 1 }}.&nbsp;&nbsp;&nbsp;&nbsp;{{ ab.title }}</p>
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.duration) / longestAudiobookDuration) + '%' }" />
</div>
<div class="w-4 ml-3">
<p class="text-sm font-bold">{{ (ab.duration / 3600).toFixed(1) }}</p>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
libraryStats: null
}
},
watch: {
currentLibraryId(newVal, oldVal) {
if (newVal) {
this.init()
}
}
},
computed: {
user() {
return this.$store.state.user.user
},
totalBooks() {
return this.libraryStats ? this.libraryStats.totalBooks : 0
},
genresWithCount() {
return this.libraryStats ? this.libraryStats.genresWithCount : []
},
top5Genres() {
return this.genresWithCount.slice(0, 5)
},
top10LongestAudiobooks() {
return this.libraryStats ? this.libraryStats.longestAudiobooks || [] : []
},
longestAudiobookDuration() {
if (!this.top10LongestAudiobooks.length) return 0
return this.top10LongestAudiobooks[0].duration
},
authorsWithCount() {
return this.libraryStats ? this.libraryStats.authorsWithCount : []
},
mostUsedAuthorCount() {
if (!this.authorsWithCount.length) return 0
return this.authorsWithCount[0].count
},
top10Authors() {
return this.authorsWithCount.slice(0, 10)
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
currentLibraryName() {
return this.$store.getters['libraries/getCurrentLibraryName']
}
},
methods: {
async init() {
this.libraryStats = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/stats`).catch((err) => {
console.error('Failed to get library stats', err)
var errorMsg = err.response ? err.response.data || 'Unknown Error' : 'Unknown Error'
this.$toast.error(`Failed to get library stats: ${errorMsg}`)
})
console.log('lib stats', this.libraryStats)
}
},
mounted() {
this.init()
}
}
</script>

View file

@ -1,40 +1,56 @@
<template>
<div>
<stats-preview-icons :listening-stats="listeningStats" :library-stats="libraryStats" />
<div class="flex justify-center">
<div class="flex p-2">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z"
/>
</svg>
<div class="px-3">
<p class="text-4xl md:text-5xl font-bold">{{ userAudiobooksRead.length }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Books Read</p>
</div>
</div>
<div class="flex md:flex-row flex-wrap justify-between flex-col mt-12">
<div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1>
<template v-for="genre in top5Genres">
<div :key="genre.genre" class="w-full py-2">
<div class="flex items-end mb-1">
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalBooks) }}&nbsp;%</p>
<div class="flex-grow" />
<p class="text-base font-book text-white text-opacity-70">{{ genre.genre }}</p>
</div>
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalBooks) + '%' }" />
</div>
</div>
</template>
<div class="flex p-2">
<span class="material-icons-outlined" style="font-size: 4.1rem">event</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Days Listened</p>
</div>
</div>
<div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Top 10 Authors</h1>
<template v-for="(author, index) in top10Authors">
<div :key="author.author" class="w-full py-2">
<div class="flex items-center mb-1">
<p class="text-sm font-book text-white text-opacity-70 w-36 pr-2 truncate">{{ index + 1 }}.&nbsp;&nbsp;&nbsp;&nbsp;{{ author.author }}</p>
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
</div>
<div class="w-4 ml-3">
<p class="text-sm font-bold">{{ author.count }}</p>
</div>
</div>
</div>
</template>
<div class="flex p-2">
<span class="material-icons-outlined" style="font-size: 4.1rem">watch_later</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Minutes Listening</p>
</div>
</div>
</div>
<div class="flex">
<stats-daily-listening-chart :listening-stats="listeningStats" />
<div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Recent Listening Sessions</h1>
<p v-if="!mostRecentListeningSessions.length">No Listening Sessions</p>
<template v-for="(book, index) in mostRecentListeningSessions">
<div :key="book.id" class="w-full py-0.5">
<div class="flex items-center mb-1">
<p class="text-sm font-book text-white text-opacity-70 w-6 truncate">{{ index + 1 }}.&nbsp;</p>
<div class="w-56">
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ book.audiobookTitle }}</p>
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(book.lastUpdate) }}</p>
</div>
<div class="flex-grow" />
<div class="w-18 text-right">
<p class="text-sm font-bold">{{ $elapsedPretty(book.timeListening) }}</p>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
@ -58,27 +74,31 @@ export default {
user() {
return this.$store.state.user.user
},
totalBooks() {
return this.libraryStats ? this.libraryStats.totalBooks : 0
},
genresWithCount() {
return this.libraryStats ? this.libraryStats.genresWithCount : []
},
top5Genres() {
return this.genresWithCount.slice(0, 5)
},
authorsWithCount() {
return this.libraryStats ? this.libraryStats.authorsWithCount : []
},
mostUsedAuthorCount() {
if (!this.authorsWithCount.length) return 0
return this.authorsWithCount[0].count
},
top10Authors() {
return this.authorsWithCount.slice(0, 10)
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
userAudiobooks() {
return Object.values(this.user.audiobooks || {})
},
userAudiobooksRead() {
return this.userAudiobooks.filter((ab) => !!ab.isRead)
},
mostRecentBooksListened() {
if (!this.listeningStats) return []
var sorted = Object.values(this.listeningStats.books || {}).sort((a, b) => b.lastUpdate - a.lastUpdate)
return sorted.slice(0, 10)
},
mostRecentListeningSessions() {
if (!this.listeningStats) return []
return this.listeningStats.recentSessions || []
},
totalMinutesListening() {
if (!this.listeningStats) return 0
return Math.round(this.listeningStats.totalTime / 60)
},
totalDaysListened() {
if (!this.listeningStats) return 0
return Object.values(this.listeningStats.days).length
}
},
methods: {
@ -88,7 +108,6 @@ export default {
var errorMsg = err.response ? err.response.data || 'Unknown Error' : 'Unknown Error'
this.$toast.error(`Failed to get library stats: ${errorMsg}`)
})
console.log('lib stats', this.libraryStats)
this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => {
console.error('Failed to load listening sesions', err)
return []

View file

@ -15,10 +15,16 @@ const BookCoverAspectRatio = {
SQUARE: 1
}
const BookshelfView = {
STANDARD: 0,
TITLES: 1
}
const Constants = {
DownloadStatus,
CoverDestination,
BookCoverAspectRatio
BookCoverAspectRatio,
BookshelfView
}
const KeyNames = {

View file

@ -13,6 +13,11 @@ export const getters = {
getCurrentLibrary: state => {
return state.libraries.find(lib => lib.id === state.currentLibraryId)
},
getCurrentLibraryName: (state, getters) => {
var currentLibrary = getters.getCurrentLibrary
if (!currentLibrary) return ''
return currentLibrary.name
},
getSortedLibraries: state => () => {
return state.libraries.map(lib => ({ ...lib })).sort((a, b) => a.displayOrder - b.displayOrder)
}
@ -182,7 +187,8 @@ export const mutations = {
genres: [],
tags: [],
series: [],
narrators: []
narrators: [],
languages: []
}
*/
@ -213,5 +219,8 @@ export const mutations = {
if (genre && !state.filterData.genres.includes(genre)) state.filterData.genres.push(genre)
})
}
if (audiobook.book.language && !state.filterData.languages.includes(audiobook.book.language)) {
state.filterData.languages.push(audiobook.book.language)
}
}
}

View file

@ -1,5 +1,19 @@
export const state = () => ({
libraryScans: []
libraryScans: [],
providers: [
{
text: 'Google Books',
value: 'google'
},
{
text: 'Open Library',
value: 'openlibrary'
},
{
text: 'Audible',
value: 'audible'
}
]
})
export const getters = {