mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-20 18:54:38 +02:00
Add: bookshelf list view, add: sort by most recent
This commit is contained in:
parent
6dbbfdbc04
commit
56a70aefaf
16 changed files with 377 additions and 50 deletions
|
@ -13,8 +13,8 @@ android {
|
||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 21
|
versionCode 22
|
||||||
versionName "0.9.5-beta"
|
versionName "0.9.6-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
font-family: 'Material Icons';
|
font-family: 'Material Icons';
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
font-size: 1.5rem;
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -21,6 +21,12 @@
|
||||||
-webkit-font-feature-settings: 'liga';
|
-webkit-font-feature-settings: 'liga';
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
.material-icons.text-icon {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
.material-icons.text-base {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Gentium Book Basic';
|
font-family: 'Gentium Book Basic';
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<template v-for="(shelf, index) in groupedBooks">
|
<template v-for="(shelf, index) in groupedBooks">
|
||||||
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
||||||
<template v-for="audiobook in shelf">
|
<template v-for="audiobook in shelf">
|
||||||
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" :user-progress="userAudiobooks[audiobook.id]" :local-user-progress="localUserAudiobooks[audiobook.id]" />
|
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" />
|
||||||
</template>
|
</template>
|
||||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,19 +41,13 @@ export default {
|
||||||
return this.$store.getters['user/getFilterOrderKey']
|
return this.$store.getters['user/getFilterOrderKey']
|
||||||
},
|
},
|
||||||
hasFilters() {
|
hasFilters() {
|
||||||
return this.$store.getters['user/getUserSetting']('filterBy') !== 'all'
|
return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all'
|
||||||
},
|
|
||||||
userAudiobooks() {
|
|
||||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
|
||||||
},
|
|
||||||
localUserAudiobooks() {
|
|
||||||
return this.$store.state.user.localUserAudiobooks || {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clearFilter() {
|
clearFilter() {
|
||||||
this.$store.dispatch('user/updateUserSettings', {
|
this.$store.dispatch('user/updateUserSettings', {
|
||||||
filterBy: 'all'
|
mobileFilterBy: 'all'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
calcShelves() {
|
calcShelves() {
|
||||||
|
|
138
components/app/BookshelfList.vue
Normal file
138
components/app/BookshelfList.vue
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
<template>
|
||||||
|
<div id="bookshelf" ref="wrapper" class="w-full overflow-y-auto">
|
||||||
|
<template v-for="(ab, index) in audiobooks">
|
||||||
|
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 px-2 flex relative">
|
||||||
|
<app-bookshelf-list-row :audiobook="ab" :card-width="cardWidth" :page-width="pageWidth" />
|
||||||
|
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-show="!audiobooks.length" class="w-full py-16 text-center text-xl">
|
||||||
|
<div class="py-4">No Audiobooks</div>
|
||||||
|
<ui-btn v-if="hasFilters" @click="clearFilter">Clear Filter</ui-btn>
|
||||||
|
</div>
|
||||||
|
<div v-show="isLoading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-70 z-20">
|
||||||
|
<div class="py-4">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currFilterOrderKey: null,
|
||||||
|
pageWidth: 0,
|
||||||
|
audiobooks: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoading() {
|
||||||
|
return this.$store.state.audiobooks.isLoading
|
||||||
|
},
|
||||||
|
cardWidth() {
|
||||||
|
return 75
|
||||||
|
},
|
||||||
|
cardHeight() {
|
||||||
|
return this.cardWidth * 2
|
||||||
|
},
|
||||||
|
contentRowWidth() {
|
||||||
|
return this.pageWidth - 16 - this.cardWidth
|
||||||
|
},
|
||||||
|
filterOrderKey() {
|
||||||
|
return this.$store.getters['user/getFilterOrderKey']
|
||||||
|
},
|
||||||
|
hasFilters() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearFilter() {
|
||||||
|
this.$store.dispatch('user/updateUserSettings', {
|
||||||
|
mobileFilterBy: 'all'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
calcShelves() {
|
||||||
|
this.audiobooks = this.$store.getters['audiobooks/getFilteredAndSorted']()
|
||||||
|
},
|
||||||
|
audiobooksUpdated() {
|
||||||
|
this.calcShelves()
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
if (this.$refs.wrapper) {
|
||||||
|
this.pageWidth = this.$refs.wrapper.clientWidth
|
||||||
|
this.calcShelves()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
settingsUpdated() {
|
||||||
|
if (this.currFilterOrderKey !== this.filterOrderKey) {
|
||||||
|
this.calcShelves()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadAudiobooks() {
|
||||||
|
var currentLibrary = await this.$localStore.getCurrentLibrary()
|
||||||
|
if (currentLibrary) {
|
||||||
|
this.$store.commit('libraries/setCurrentLibrary', currentLibrary.id)
|
||||||
|
}
|
||||||
|
this.$store.dispatch('audiobooks/load')
|
||||||
|
},
|
||||||
|
socketConnected(isConnected) {
|
||||||
|
if (isConnected) {
|
||||||
|
console.log('Connected - Load from server')
|
||||||
|
this.loadAudiobooks()
|
||||||
|
} else {
|
||||||
|
console.log('Disconnected - Reset to local storage')
|
||||||
|
this.$store.commit('audiobooks/reset')
|
||||||
|
this.$store.dispatch('audiobooks/useDownloaded')
|
||||||
|
// this.calcShelves()
|
||||||
|
// this.$store.dispatch('downloads/loadFromStorage')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
||||||
|
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
|
|
||||||
|
if (!this.$server) {
|
||||||
|
console.error('Bookshelf mounted no server')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$server.on('connected', this.socketConnected)
|
||||||
|
if (this.$server.connected) {
|
||||||
|
this.loadAudiobooks()
|
||||||
|
} else {
|
||||||
|
console.log('Bookshelf - Server not connected using downloaded')
|
||||||
|
}
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
||||||
|
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
||||||
|
window.removeEventListener('resize', this.resize)
|
||||||
|
|
||||||
|
if (!this.$server) {
|
||||||
|
console.error('Bookshelf beforeDestroy no server')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$server.off('connected', this.socketConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#bookshelf {
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
}
|
||||||
|
.bookshelfRow {
|
||||||
|
background-image: url(/wood_panels.jpg);
|
||||||
|
}
|
||||||
|
.bookshelfDivider {
|
||||||
|
background: rgb(149, 119, 90);
|
||||||
|
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
||||||
|
box-shadow: 2px 14px 8px #111111aa;
|
||||||
|
}
|
||||||
|
</style>
|
124
components/app/BookshelfListRow.vue
Normal file
124
components/app/BookshelfListRow.vue
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full flex">
|
||||||
|
<cards-book-card :audiobook="audiobook" :width="cardWidth" class="self-end" />
|
||||||
|
<div class="relative px-2" :style="{ width: contentRowWidth + 'px' }">
|
||||||
|
<div class="flex">
|
||||||
|
<nuxt-link :to="`/audiobook/${audiobook.id}`">
|
||||||
|
<p class="leading-6" style="font-size: 1.1rem">{{ audiobook.book.title }}</p>
|
||||||
|
</nuxt-link>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- <button class="mx-1" @click="editAudiobook(ab)">
|
||||||
|
<span class="material-icons text-icon pb-px">edit</span>
|
||||||
|
</button> -->
|
||||||
|
<button v-if="!isPlaying" class="mx-1 rounded-full w-6 h-6" @click="playAudiobook">
|
||||||
|
<span class="material-icons">play_arrow</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="audiobook.book.subtitle" class="text-gray-200 leading-6 truncate" style="font-size: 0.9rem">{{ audiobook.book.subtitle }}</p>
|
||||||
|
<p class="text-sm text-gray-200">by {{ audiobook.book.author }}</p>
|
||||||
|
<div class="flex items-center py-1">
|
||||||
|
<p class="text-xs text-gray-300">{{ $elapsedPretty(audiobook.duration) }}</p>
|
||||||
|
<span class="px-3 text-xs text-gray-300">•</span>
|
||||||
|
<p class="text-xs text-gray-300 font-mono">{{ $bytesPretty(audiobook.size, 0) }}</p>
|
||||||
|
<span class="px-3 text-xs text-gray-300">•</span>
|
||||||
|
<p class="text-xs text-gray-300">{{ numTracks }} tracks</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<div v-if="userProgressPercent && !userIsRead" class="w-min my-1">
|
||||||
|
<div class="bg-primary bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">Progress: {{ Math.floor(userProgressPercent * 100) }}%</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isDownloadPlayable" class="w-min my-1 mx-1">
|
||||||
|
<div class="bg-success bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">Downloaded</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isDownloading" class="w-min my-1 mx-1">
|
||||||
|
<div class="bg-warning bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">Downloading...</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isPlaying" class="w-min my-1 mx-1">
|
||||||
|
<div class="bg-info bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">{{ isStreaming ? 'Streaming' : 'Playing' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
audiobook: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
cardWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 75
|
||||||
|
},
|
||||||
|
pageWidth: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
audiobookId() {
|
||||||
|
return this.audiobook.id
|
||||||
|
},
|
||||||
|
mostRecentUserProgress() {
|
||||||
|
return this.$store.getters['user/getMostRecentAudiobookProgress'](this.audiobookId)
|
||||||
|
},
|
||||||
|
userProgressPercent() {
|
||||||
|
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
|
||||||
|
},
|
||||||
|
userIsRead() {
|
||||||
|
return this.mostRecentUserProgress ? !!this.mostRecentUserProgress.isRead : false
|
||||||
|
},
|
||||||
|
contentRowWidth() {
|
||||||
|
return this.pageWidth - 16 - this.cardWidth
|
||||||
|
},
|
||||||
|
isDownloading() {
|
||||||
|
return this.downloadObj ? this.downloadObj.isDownloading : false
|
||||||
|
},
|
||||||
|
isDownloadPreparing() {
|
||||||
|
return this.downloadObj ? this.downloadObj.isPreparing : false
|
||||||
|
},
|
||||||
|
isDownloadPlayable() {
|
||||||
|
return this.downloadObj && !this.isDownloading && !this.isDownloadPreparing
|
||||||
|
},
|
||||||
|
downloadedCover() {
|
||||||
|
return this.downloadObj ? this.downloadObj.cover : null
|
||||||
|
},
|
||||||
|
downloadObj() {
|
||||||
|
return this.$store.getters['downloads/getDownload'](this.audiobookId)
|
||||||
|
},
|
||||||
|
isStreaming() {
|
||||||
|
return this.$store.getters['isAudiobookStreaming'](this.audiobookId)
|
||||||
|
},
|
||||||
|
isPlaying() {
|
||||||
|
return this.$store.getters['isAudiobookPlaying'](this.audiobookId)
|
||||||
|
},
|
||||||
|
numTracks() {
|
||||||
|
if (this.audiobook.tracks) return this.audiobook.tracks.length
|
||||||
|
return this.audiobook.numTracks || 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
playAudiobook() {
|
||||||
|
if (this.isPlaying) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$store.commit('setPlayOnLoad', true)
|
||||||
|
if (!this.isDownloadPlayable) {
|
||||||
|
// Stream
|
||||||
|
console.log('[PLAYCLICK] Set Playing STREAM ' + this.audiobook.book.title)
|
||||||
|
this.$store.commit('setStreamAudiobook', this.audiobook)
|
||||||
|
this.$server.socket.emit('open_stream', this.audiobook.id)
|
||||||
|
} else {
|
||||||
|
// Local
|
||||||
|
console.log('[PLAYCLICK] Set Playing Local Download ' + this.audiobook.book.title)
|
||||||
|
this.$store.commit('setPlayingDownload', this.downloadObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full p-4 pointer-events-none fixed bottom-0 left-0 right-0 z-20">
|
<div class="w-full p-4 pointer-events-none fixed bottom-0 left-0 right-0 z-20">
|
||||||
<div v-if="audiobook" class="w-full bg-primary absolute bottom-0 left-0 right-0 z-50 p-2 pointer-events-auto" @click.stop @mousedown.stop @mouseup.stop>
|
<div v-if="audiobook" id="streamContainer" class="w-full bg-primary absolute bottom-0 left-0 right-0 z-50 p-2 pointer-events-auto" @click.stop @mousedown.stop @mouseup.stop>
|
||||||
<div class="pl-16 pr-2 flex items-center pb-2">
|
<div class="pl-16 pr-2 flex items-center pb-2">
|
||||||
<div>
|
<div>
|
||||||
<p class="px-2">{{ title }}</p>
|
<p class="px-2">{{ title }}</p>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
<span class="material-icons" @click="cancelStream">close</span>
|
<span class="material-icons" @click="cancelStream">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-2 -top-10">
|
<div class="absolute left-2 -top-10 bookCoverWrapper">
|
||||||
<cards-book-cover :audiobook="audiobook" :download-cover="downloadedCover" :width="64" />
|
<cards-book-cover :audiobook="audiobook" :download-cover="downloadedCover" :width="64" />
|
||||||
</div>
|
</div>
|
||||||
<audio-player-mini ref="audioPlayerMini" :loading="isLoading" @updateTime="updateTime" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @hook:mounted="audioPlayerMounted" />
|
<audio-player-mini ref="audioPlayerMini" :loading="isLoading" @updateTime="updateTime" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @hook:mounted="audioPlayerMounted" />
|
||||||
|
@ -380,3 +380,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bookCoverWrapper {
|
||||||
|
box-shadow: 3px -2px 5px #00000066;
|
||||||
|
}
|
||||||
|
#streamContainer {
|
||||||
|
box-shadow: 0px -8px 8px #11111177;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,9 +17,9 @@
|
||||||
<span class="material-icons text-success" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">download_done</span>
|
<span class="material-icons text-success" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">download_done</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div class="absolute bottom-0 left-0 h-1.5 shadow-sm" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||||
|
|
||||||
<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">
|
<div v-if="showError" :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem', bottom: sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300 absolute left-0">
|
||||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,14 +35,6 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
},
|
},
|
||||||
userProgress: {
|
|
||||||
type: Object,
|
|
||||||
default: () => null
|
|
||||||
},
|
|
||||||
localUserProgress: {
|
|
||||||
type: Object,
|
|
||||||
default: () => null
|
|
||||||
},
|
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 140
|
default: 140
|
||||||
|
@ -87,16 +79,17 @@ export default {
|
||||||
return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL
|
return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL
|
||||||
},
|
},
|
||||||
orderBy() {
|
orderBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
return this.$store.getters['user/getUserSetting']('mobileOrderBy')
|
||||||
},
|
},
|
||||||
mostRecentUserProgress() {
|
mostRecentUserProgress() {
|
||||||
if (!this.localUserProgress) return this.userProgress
|
return this.$store.getters['user/getMostRecentAudiobookProgress'](this.audiobookId)
|
||||||
if (!this.userProgress) return this.localUserProgress
|
|
||||||
return this.localUserProgress.lastUpdate > this.userProgress.lastUpdate ? this.localUserProgress : this.userProgress
|
|
||||||
},
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
|
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
|
||||||
},
|
},
|
||||||
|
userIsRead() {
|
||||||
|
return this.mostRecentUserProgress ? !!this.mostRecentUserProgress.isRead : false
|
||||||
|
},
|
||||||
showError() {
|
showError() {
|
||||||
return this.hasMissingParts || this.hasInvalidParts
|
return this.hasMissingParts || this.hasInvalidParts
|
||||||
},
|
},
|
||||||
|
@ -110,7 +103,6 @@ export default {
|
||||||
return this.download ? this.download.cover : null
|
return this.download ? this.download.cover : null
|
||||||
},
|
},
|
||||||
download() {
|
download() {
|
||||||
return null
|
|
||||||
return this.$store.getters['downloads/getDownloadIfReady'](this.audiobookId)
|
return this.$store.getters['downloads/getDownloadIfReady'](this.audiobookId)
|
||||||
},
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
|
|
|
@ -54,6 +54,10 @@ export default {
|
||||||
{
|
{
|
||||||
text: 'Size',
|
text: 'Size',
|
||||||
value: 'size'
|
value: 'size'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Last Read',
|
||||||
|
value: 'recent'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -89,6 +93,7 @@ export default {
|
||||||
if (this.selected === val) {
|
if (this.selected === val) {
|
||||||
this.selectedDesc = !this.selectedDesc
|
this.selectedDesc = !this.selectedDesc
|
||||||
} else {
|
} else {
|
||||||
|
if (val === 'recent' || val === 'addedAt') this.selectedDesc = true // Progress defaults to descending
|
||||||
this.selected = val
|
this.selected = val
|
||||||
}
|
}
|
||||||
this.show = false
|
this.show = false
|
||||||
|
|
|
@ -415,6 +415,6 @@ export default {
|
||||||
height: calc(100vh - 64px);
|
height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
#content.playerOpen {
|
#content.playerOpen {
|
||||||
height: calc(100vh - 240px);
|
height: calc(100vh - 236px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -26,7 +26,7 @@ export default {
|
||||||
],
|
],
|
||||||
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=Ubuntu+Mono&family=Open+Sans:wght@400;600' },
|
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Source+Sans+Pro:wght@300;400;600' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.9.5-beta",
|
"version": "v0.9.6-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<p class="font-book">{{ numAudiobooks }} Audiobooks</p>
|
<p class="font-book">{{ numAudiobooks }} Audiobooks</p>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
<span class="material-icons px-2" @click="changeView">{{ viewIcon }}</span>
|
||||||
<div class="relative flex items-center px-2">
|
<div class="relative flex items-center px-2">
|
||||||
<span class="material-icons" @click="showFilterModal = true">filter_alt</span>
|
<span class="material-icons" @click="showFilterModal = true">filter_alt</span>
|
||||||
<div v-show="hasFilters" class="absolute top-0 right-2 w-2 h-2 rounded-full bg-success border border-green-300 shadow-sm z-10 pointer-events-none" />
|
<div v-show="hasFilters" class="absolute top-0 right-2 w-2 h-2 rounded-full bg-success border border-green-300 shadow-sm z-10 pointer-events-none" />
|
||||||
|
@ -13,10 +14,13 @@
|
||||||
<span class="material-icons px-2" @click="showSortModal = true">sort</span>
|
<span class="material-icons px-2" @click="showSortModal = true">sort</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-bookshelf />
|
<template v-if="bookshelfReady">
|
||||||
|
<app-bookshelf v-if="!isListView" />
|
||||||
|
<app-bookshelf-list v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
<modals-order-modal v-model="showSortModal" :order-by.sync="settings.orderBy" :descending.sync="settings.orderDesc" @change="updateOrder" />
|
<modals-order-modal v-model="showSortModal" :order-by.sync="settings.mobileOrderBy" :descending.sync="settings.mobileOrderDesc" @change="updateOrder" />
|
||||||
<modals-filter-modal v-model="showFilterModal" :filter-by.sync="settings.filterBy" @change="updateFilter" />
|
<modals-filter-modal v-model="showFilterModal" :filter-by.sync="settings.mobileFilterBy" @change="updateFilter" />
|
||||||
<modals-search-modal v-model="showSearchModal" />
|
<modals-search-modal v-model="showSearchModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -28,7 +32,9 @@ export default {
|
||||||
showSortModal: false,
|
showSortModal: false,
|
||||||
showFilterModal: false,
|
showFilterModal: false,
|
||||||
showSearchModal: false,
|
showSearchModal: false,
|
||||||
settings: {}
|
settings: {},
|
||||||
|
isListView: false,
|
||||||
|
bookshelfReady: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -37,9 +43,18 @@ export default {
|
||||||
},
|
},
|
||||||
numAudiobooks() {
|
numAudiobooks() {
|
||||||
return this.$store.getters['audiobooks/getFiltered']().length
|
return this.$store.getters['audiobooks/getFiltered']().length
|
||||||
|
},
|
||||||
|
viewIcon() {
|
||||||
|
return this.isListView ? 'grid_view' : 'view_stream'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
changeView() {
|
||||||
|
this.isListView = !this.isListView
|
||||||
|
|
||||||
|
var bookshelfView = this.isListView ? 'list' : 'grid'
|
||||||
|
this.$localStore.setBookshelfView(bookshelfView)
|
||||||
|
},
|
||||||
updateOrder() {
|
updateOrder() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
|
@ -50,8 +65,13 @@ export default {
|
||||||
this.$store.commit('user/setSettings', this.settings) // Immediate update
|
this.$store.commit('user/setSettings', this.settings) // Immediate update
|
||||||
this.$store.dispatch('user/updateUserSettings', this.settings)
|
this.$store.dispatch('user/updateUserSettings', this.settings)
|
||||||
},
|
},
|
||||||
init() {
|
async init() {
|
||||||
this.settings = { ...this.$store.state.user.settings }
|
this.settings = { ...this.$store.state.user.settings }
|
||||||
|
|
||||||
|
var bookshelfView = await this.$localStore.getBookshelfView()
|
||||||
|
this.isListView = bookshelfView === 'list'
|
||||||
|
this.bookshelfReady = true
|
||||||
|
console.log('Bookshelf view', bookshelfView)
|
||||||
},
|
},
|
||||||
settingsUpdated(settings) {
|
settingsUpdated(settings) {
|
||||||
for (const key in settings) {
|
for (const key in settings) {
|
||||||
|
|
|
@ -442,7 +442,6 @@ class LocalStorage {
|
||||||
this.userAudiobooks = val ? JSON.parse(val) : {}
|
this.userAudiobooks = val ? JSON.parse(val) : {}
|
||||||
this.userAudiobooksLoaded = true
|
this.userAudiobooksLoaded = true
|
||||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
||||||
console.log('[LocalStorage] Loaded Local USER Audiobooks ' + JSON.stringify(this.userAudiobooks))
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalStorage] Failed to load user audiobooks', error)
|
console.error('[LocalStorage] Failed to load user audiobooks', error)
|
||||||
}
|
}
|
||||||
|
@ -600,6 +599,24 @@ class LocalStorage {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setBookshelfView(view) {
|
||||||
|
try {
|
||||||
|
await Storage.set({ key: 'bookshelfView', value: view })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[LocalStorage] Failed to set bookshelf view', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBookshelfView() {
|
||||||
|
try {
|
||||||
|
var view = await Storage.get({ key: 'bookshelfView' }) || {}
|
||||||
|
return view.value || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[LocalStorage] Failed to get bookshelf view', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ app, store }, inject) => {
|
export default ({ app, store }, inject) => {
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const getters = {
|
||||||
getFiltered: (state, getters, rootState, rootGetters) => () => {
|
getFiltered: (state, getters, rootState, rootGetters) => () => {
|
||||||
var filtered = state.audiobooks
|
var filtered = state.audiobooks
|
||||||
var settings = rootState.user.settings || {}
|
var settings = rootState.user.settings || {}
|
||||||
var filterBy = settings.filterBy || ''
|
var filterBy = settings.mobileFilterBy || ''
|
||||||
|
|
||||||
var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress']
|
var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress']
|
||||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||||
|
@ -45,18 +45,27 @@ export const getters = {
|
||||||
}
|
}
|
||||||
return filtered
|
return filtered
|
||||||
},
|
},
|
||||||
getFilteredAndSorted: (state, getters, rootState) => () => {
|
getFilteredAndSorted: (state, getters, rootState, rootGetters) => () => {
|
||||||
var settings = rootState.user.settings
|
var settings = rootState.user.settings
|
||||||
var direction = settings.orderDesc ? 'desc' : 'asc'
|
var direction = settings.mobileOrderDesc ? 'desc' : 'asc'
|
||||||
|
|
||||||
var filtered = getters.getFiltered()
|
var filtered = getters.getFiltered()
|
||||||
var orderByNumber = settings.orderBy === 'book.volumeNumber'
|
|
||||||
return sort(filtered)[direction]((ab) => {
|
if (settings.mobileOrderBy === 'recent') {
|
||||||
// Supports dot notation strings i.e. "book.title"
|
return sort(filtered)[direction]((ab) => {
|
||||||
var value = settings.orderBy.split('.').reduce((a, b) => a[b], ab)
|
var abprogress = rootGetters['user/getMostRecentAudiobookProgress'](ab.id)
|
||||||
if (orderByNumber && !isNaN(value)) return Number(value)
|
if (!abprogress) return 0
|
||||||
return value
|
return abprogress.lastUpdate
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
var orderByNumber = settings.mobileOrderBy === 'book.volumeNumber'
|
||||||
|
return sort(filtered)[direction]((ab) => {
|
||||||
|
// Supports dot notation strings i.e. "book.title"
|
||||||
|
var value = settings.mobileOrderBy.split('.').reduce((a, b) => a[b], ab)
|
||||||
|
if (orderByNumber && !isNaN(value)) return Number(value)
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getUniqueAuthors: (state) => {
|
getUniqueAuthors: (state) => {
|
||||||
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
||||||
|
|
|
@ -2,6 +2,9 @@ export const state = () => ({
|
||||||
user: null,
|
user: null,
|
||||||
localUserAudiobooks: {},
|
localUserAudiobooks: {},
|
||||||
settings: {
|
settings: {
|
||||||
|
mobileOrderBy: 'recent',
|
||||||
|
mobileOrderDesc: true,
|
||||||
|
mobileFilterBy: 'all',
|
||||||
orderBy: 'book.title',
|
orderBy: 'book.title',
|
||||||
orderDesc: false,
|
orderDesc: false,
|
||||||
filterBy: 'all',
|
filterBy: 'all',
|
||||||
|
@ -19,6 +22,16 @@ export const getters = {
|
||||||
getUserAudiobook: (state) => (audiobookId) => {
|
getUserAudiobook: (state) => (audiobookId) => {
|
||||||
return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
|
return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
|
||||||
},
|
},
|
||||||
|
getLocalUserAudiobook: (state) => (audiobookId) => {
|
||||||
|
return state.user && state.user.localUserAudiobooks ? state.user.localUserAudiobooks[audiobookId] || null : null
|
||||||
|
},
|
||||||
|
getMostRecentAudiobookProgress: (state, getters) => (audiobookId) => {
|
||||||
|
var userAb = getters.getUserAudiobook(audiobookId)
|
||||||
|
var localUserAb = getters.getLocalUserAudiobook(audiobookId)
|
||||||
|
if (!localUserAb) return userAb
|
||||||
|
if (!userAb) return localUserAb
|
||||||
|
return localUserAb.lastUpdate > userAb.lastUpdate ? localUserAb : userAb
|
||||||
|
},
|
||||||
getUserSetting: (state) => (key) => {
|
getUserSetting: (state) => (key) => {
|
||||||
return state.settings ? state.settings[key] || null : null
|
return state.settings ? state.settings[key] || null : null
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,7 +30,7 @@ module.exports = {
|
||||||
none: 'none'
|
none: 'none'
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Open Sans', ...defaultTheme.fontFamily.sans],
|
sans: ['Source Sans Pro', ...defaultTheme.fontFamily.sans],
|
||||||
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono],
|
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono],
|
||||||
book: ['Gentium Book Basic', 'serif']
|
book: ['Gentium Book Basic', 'serif']
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue