UI updates

This commit is contained in:
advplyr 2022-04-09 20:29:59 -05:00
parent 12a153d423
commit abf140bd21
10 changed files with 142 additions and 51 deletions

View file

@ -6,13 +6,13 @@
</div> </div>
<!-- Alternative bookshelf title/author/sort --> <!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <!-- <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' }"> <p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }} {{ displayTitle }}
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</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> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div> </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> <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>
@ -35,7 +35,7 @@
</div> </div>
<!-- No progress shown for collapsed series in library --> <!-- No progress shown for collapsed series in library -->
<div v-if="!booksInSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div> <div v-if="!collapsedSeries && !isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }"> <div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span> <span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
@ -49,8 +49,13 @@
</ui-tooltip> </ui-tooltip>
<!-- Volume number --> <!-- Volume number -->
<div v-if="volumeNumber && showVolumeNumber && !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` }"> <div v-if="seriesSequence && showSequence && !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> <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
</div>
<!-- Podcast Num Episodes -->
<div v-if="numEpisodes && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
</div> </div>
</div> </div>
</template> </template>
@ -70,7 +75,7 @@ export default {
default: 192 default: 192
}, },
bookCoverAspectRatio: Number, bookCoverAspectRatio: Number,
showVolumeNumber: Boolean, showSequence: Boolean,
bookshelfView: Number, bookshelfView: Number,
bookMount: { bookMount: {
// Book can be passed as prop or set with setEntity() // Book can be passed as prop or set with setEntity()
@ -122,6 +127,12 @@ export default {
mediaMetadata() { mediaMetadata() {
return this.media.metadata || {} return this.media.metadata || {}
}, },
mediaType() {
return this._libraryItem.mediaType
},
isPodcast() {
return this.mediaType === 'podcast'
},
placeholderUrl() { placeholderUrl() {
return '/book_placeholder.jpg' return '/book_placeholder.jpg'
}, },
@ -135,9 +146,6 @@ export default {
libraryItemId() { libraryItemId() {
return this._libraryItem.id return this._libraryItem.id
}, },
series() {
return this.mediaMetadata.series
},
libraryId() { libraryId() {
return this._libraryItem.libraryId return this._libraryItem.libraryId
}, },
@ -147,6 +155,9 @@ export default {
numTracks() { numTracks() {
return this.media.numTracks return this.media.numTracks
}, },
numEpisodes() {
return this.media.numEpisodes
},
processingBatch() { processingBatch() {
return this.store.state.processingBatch return this.store.state.processingBatch
}, },
@ -174,19 +185,26 @@ export default {
return this.mediaMetadata.authors || [] return this.mediaMetadata.authors || []
}, },
author() { author() {
return this.authors.map((au) => au.name).join(', ') if (this.isPodcast) return this.mediaMetadata.author
return this.mediaMetadata.authorName
}, },
authorLF() { authorLF() {
return this.authors return this.mediaMetadata.authorNameLF
.map((au) => {
var parts = au.name.split(' ')
if (parts.length === 1) return parts[0]
return `${parts[1]}, ${parts[0]}`
})
.join(', ')
}, },
volumeNumber() { series() {
return this.mediaMetadata.volumeNumber || null // Only included when filtering by series or collapse series
return this.mediaMetadata.series
},
seriesSequence() {
return this.series ? this.series.sequence : null
},
collapsedSeries() {
// Only added to item object when collapseSeries is enabled
return this._libraryItem.collapsedSeries
},
booksInSeries() {
// Only added to item object when collapseSeries is enabled
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
}, },
displayTitle() { displayTitle() {
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) { if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
@ -308,16 +326,6 @@ export default {
return this.author.slice(0, 27) + '...' return this.author.slice(0, 27) + '...'
} }
return this.author return this.author
},
isAlternativeBookshelfView() {
return false
// 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: { methods: {
@ -340,7 +348,7 @@ export default {
} else { } else {
var router = this.$router || this.$nuxt.$router var router = this.$router || this.$nuxt.$router
if (router) { if (router) {
if (this.booksInSeries) router.push(`/library/${this.libraryId}/series/${this.$encode(this.series)}`) if (this.collapsedSeries) router.push(`/library/${this.libraryId}/series/${this.collapsedSeries.id}`)
else router.push(`/item/${this.libraryItemId}`) else router.push(`/item/${this.libraryItemId}`)
} }
} }

View file

@ -11,8 +11,8 @@
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" /> <img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
</div> </div>
<!-- No progress shown for collapsed series in library --> <!-- No progress shown for collapsed series or podcasts in library -->
<div class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div> <div v-if="!isPodcast && !collapsedSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 80 * userProgressPercent + 'px' }"></div>
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }"> <div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span> <span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
@ -20,7 +20,7 @@
</div> </div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }"> <p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
{{ displayTitle }} {{ displayTitle }}<span v-if="seriesSequence">&nbsp;#{{ seriesSequence }}</span>
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
@ -44,7 +44,7 @@ export default {
default: 192 default: 192
}, },
bookCoverAspectRatio: Number, bookCoverAspectRatio: Number,
showVolumeNumber: Boolean, showSequence: Boolean,
bookshelfView: Number, bookshelfView: Number,
bookMount: { bookMount: {
// Book can be passed as prop or set with setEntity() // Book can be passed as prop or set with setEntity()
@ -96,6 +96,12 @@ export default {
mediaMetadata() { mediaMetadata() {
return this.media.metadata || {} return this.media.metadata || {}
}, },
mediaType() {
return this._libraryItem.mediaType
},
isPodcast() {
return this.mediaType === 'podcast'
},
placeholderUrl() { placeholderUrl() {
return '/book_placeholder.jpg' return '/book_placeholder.jpg'
}, },
@ -149,8 +155,20 @@ export default {
authorLF() { authorLF() {
return this.mediaMetadata.authorNameLF || '' return this.mediaMetadata.authorNameLF || ''
}, },
volumeNumber() { series() {
return this.mediaMetadata.volumeNumber || null // Only included when filtering by series or collapse series
return this.mediaMetadata.series
},
seriesSequence() {
return this.series ? this.series.sequence : null
},
collapsedSeries() {
// Only added to item object when collapseSeries is enabled
return this._libraryItem.collapsedSeries
},
booksInSeries() {
// Only added to item object when collapseSeries is enabled
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
}, },
displayTitle() { displayTitle() {
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) { if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
@ -303,7 +321,7 @@ export default {
} else { } else {
var router = this.$router || this.$nuxt.$router var router = this.$router || this.$nuxt.$router
if (router) { if (router) {
if (this.booksInSeries) router.push(`/library/${this.libraryId}/series/${this.$encode(this.series)}`) if (this.collapsedSeries) router.push(`/library/${this.libraryId}/series/${this.collapsedSeries.id}`)
else router.push(`/item/${this.libraryItemId}`) else router.push(`/item/${this.libraryItemId}`)
} }
} }

View file

@ -1,17 +1,8 @@
<template> <template>
<div class="w-full h-9 bg-bg relative"> <div class="w-full h-9 bg-bg relative">
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary text-gray-200"> <div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary text-gray-200">
<nuxt-link to="/bookshelf" class="w-1/4 h-full flex items-center justify-center" :class="routeName === 'bookshelf' ? 'bg-primary' : 'text-gray-400'"> <nuxt-link v-for="item in items" :key="item.to" :to="item.to" class="h-full flex items-center justify-center" :style="{ width: isPodcast ? '50%' : '25%' }" :class="routeName === item.routeName ? 'bg-primary' : 'text-gray-400'">
<p>Home</p> <p>{{ item.text }}</p>
</nuxt-link>
<nuxt-link to="/bookshelf/library" class="w-1/4 h-full flex items-center justify-center" :class="routeName === 'bookshelf-library' ? 'bg-primary' : 'text-gray-400'">
<p>Library</p>
</nuxt-link>
<nuxt-link to="/bookshelf/series" class="w-1/4 h-full flex items-center justify-center" :class="routeName === 'bookshelf-series' ? 'bg-primary' : 'text-gray-400'">
<p>Series</p>
</nuxt-link>
<nuxt-link to="/bookshelf/collections" class="w-1/4 h-full flex items-center justify-center" :class="routeName === 'bookshelf-collections' ? 'bg-primary' : 'text-gray-400'">
<p>Collections</p>
</nuxt-link> </nuxt-link>
</div> </div>
</div> </div>
@ -23,8 +14,52 @@ export default {
return {} return {}
}, },
computed: { computed: {
items() {
if (this.isPodcast) {
return [
{
to: '/bookshelf',
routeName: 'bookshelf',
text: 'Home'
},
{
to: '/bookshelf/library',
routeName: 'bookshelf-library',
text: 'Library'
}
]
}
return [
{
to: '/bookshelf',
routeName: 'bookshelf',
text: 'Home'
},
{
to: '/bookshelf/library',
routeName: 'bookshelf-library',
text: 'Library'
},
{
to: '/bookshelf/series',
routeName: 'bookshelf-series',
text: 'Series'
},
{
to: '/bookshelf/collections',
routeName: 'bookshelf-collections',
text: 'Collections'
}
]
},
routeName() { routeName() {
return this.$route.name return this.$route.name
},
isPodcast() {
return this.libraryMediaType == 'podcast'
},
libraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
} }
}, },
methods: {}, methods: {},

View file

@ -8,8 +8,8 @@
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p> <p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p> <p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
<div class="flex-grow" /> <div class="flex-grow" />
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="bookshelfListView = !bookshelfListView">{{ bookshelfListView ? 'view_list' : 'grid_view' }}</span>
<template v-if="page === 'library'"> <template v-if="page === 'library'">
<span class="material-icons px-2" @click="bookshelfListView = !bookshelfListView">{{ bookshelfListView ? 'view_list' : 'grid_view' }}</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" />
@ -51,6 +51,9 @@ export default {
var routeName = this.$route.name || '' var routeName = this.$route.name || ''
return routeName.split('-')[1] return routeName.split('-')[1]
}, },
seriesBookPage() {
return this.$route.name == 'bookshelf-series-id'
},
routeQuery() { routeQuery() {
return this.$route.query || {} return this.$route.query || {}
}, },

View file

@ -274,6 +274,12 @@ export default {
} else { } else {
console.log('[default] syncLocalMediaProgress No local media progress to sync') console.log('[default] syncLocalMediaProgress No local media progress to sync')
} }
},
userUpdated(user) {
console.log('User updated', user)
if (this.user && this.user.id == user.id) {
this.$store.commit('user/setUser', user)
}
} }
}, },
async mounted() { async mounted() {
@ -286,6 +292,7 @@ export default {
this.$socket.on('connection-update', this.socketConnectionUpdate) this.$socket.on('connection-update', this.socketConnectionUpdate)
this.$socket.on('initialized', this.socketInit) this.$socket.on('initialized', this.socketInit)
this.$socket.on('user_updated', this.userUpdated)
if (this.$store.state.isFirstLoad) { if (this.$store.state.isFirstLoad) {
this.$store.commit('setIsFirstLoad', false) this.$store.commit('setIsFirstLoad', false)
@ -310,6 +317,7 @@ export default {
beforeDestroy() { beforeDestroy() {
this.$socket.off('connection-update', this.socketConnectionUpdate) this.$socket.off('connection-update', this.socketConnectionUpdate)
this.$socket.off('initialized', this.socketInit) this.$socket.off('initialized', this.socketInit)
this.$socket.off('user_updated', this.userUpdated)
} }
} }
</script> </script>

View file

@ -47,7 +47,7 @@ export default {
height: this.entityHeight, height: this.entityHeight,
bookCoverAspectRatio: this.bookCoverAspectRatio bookCoverAspectRatio: this.bookCoverAspectRatio
} }
if (this.entityName === 'series-books') props.showVolumeNumber = true if (this.entityName === 'series-books') props.showSequence = true
var _this = this var _this = this
var instance = new ComponentClass({ var instance = new ComponentClass({

View file

@ -123,7 +123,13 @@ export default {
} }
return cat return cat
}) })
this.shelves = this.shelves.concat(categories) // Put continue listening shelf first
var continueListeningShelf = categories.find((c) => c.id == 'continue-listening')
if (continueListeningShelf) {
this.shelves = [continueListeningShelf, ...this.shelves]
console.log(this.shelves)
}
this.shelves = this.shelves.concat(categories.filter((c) => c.id != 'continue-listening'))
} }
this.loading = false this.loading = false
}, },

View file

@ -240,14 +240,17 @@ export default {
if (value) { if (value) {
this.resettingProgress = true this.resettingProgress = true
if (this.isLocal) { if (this.isLocal) {
// TODO: If connected to server also sync with server
await this.$db.removeLocalMediaProgress(this.libraryItemId) await this.$db.removeLocalMediaProgress(this.libraryItemId)
this.$store.commit('globals/removeLocalMediaProgress', this.libraryItemId) this.$store.commit('globals/removeLocalMediaProgress', this.libraryItemId)
} else { } else {
var progressId = this.userItemProgress.id
await this.$axios await this.$axios
.$delete(`/api/me/progress/${this.libraryItemId}`) .$delete(`/api/me/progress/${this.libraryItemId}`)
.then(() => { .then(() => {
console.log('Progress reset complete') console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`) this.$toast.success(`Your progress was reset`)
this.$store.commit('user/removeMediaProgress', progressId)
}) })
.catch((error) => { .catch((error) => {
console.error('Progress reset failed', error) console.error('Progress reset failed', error)

View file

@ -46,6 +46,7 @@ class ServerSocket extends EventEmitter {
this.socket.on('connect', this.onConnect.bind(this)) this.socket.on('connect', this.onConnect.bind(this))
this.socket.on('disconnect', this.onDisconnect.bind(this)) this.socket.on('disconnect', this.onDisconnect.bind(this))
this.socket.on('init', this.onInit.bind(this)) this.socket.on('init', this.onInit.bind(this))
this.socket.on('user_updated', this.onUserUpdated.bind(this))
this.socket.onAny((evt, args) => { this.socket.onAny((evt, args) => {
console.log(`[SOCKET] ${this.socket.id}: ${evt} ${JSON.stringify(args)}`) console.log(`[SOCKET] ${this.socket.id}: ${evt} ${JSON.stringify(args)}`)
@ -78,6 +79,11 @@ class ServerSocket extends EventEmitter {
} }
this.emit('initialized', true) this.emit('initialized', true)
} }
onUserUpdated(data) {
console.log('[SOCKET] User updated', data)
this.emit('user_updated', data)
}
} }
export default ({ app, store }, inject) => { export default ({ app, store }, inject) => {

View file

@ -70,6 +70,10 @@ export const mutations = {
setUser(state, user) { setUser(state, user) {
state.user = user state.user = user
}, },
removeMediaProgress(state, id) {
if (!state.user) return
state.user.mediaProgress = state.user.mediaProgress.filter(mp => mp.id != id)
},
setServerConnectionConfig(state, serverConnectionConfig) { setServerConnectionConfig(state, serverConnectionConfig) {
state.serverConnectionConfig = serverConnectionConfig state.serverConnectionConfig = serverConnectionConfig
}, },