Merge branch 'master' into oauth2-support

This commit is contained in:
advplyr 2023-11-05 10:58:17 -06:00
commit 30d4e709f0
14 changed files with 621 additions and 283 deletions

View file

@ -86,7 +86,8 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
}
override fun onSeekTo(pos: Long) {
playerNotificationService.seekPlayer(pos)
val currentTrackStartOffset = playerNotificationService.getCurrentTrackStartOffsetMs()
playerNotificationService.seekPlayer(currentTrackStartOffset + pos)
}
private fun onChangeSpeed() {

View file

@ -652,16 +652,20 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
}
fun getCurrentTime() : Long {
fun getCurrentTrackStartOffsetMs() : Long {
return if (currentPlayer.mediaItemCount > 1) {
val windowIndex = currentPlayer.currentMediaItemIndex
val currentTrackStartOffset = currentPlaybackSession?.getTrackStartOffsetMs(windowIndex) ?: 0L
currentPlayer.currentPosition + currentTrackStartOffset
currentTrackStartOffset
} else {
currentPlayer.currentPosition
0
}
}
fun getCurrentTime() : Long {
return currentPlayer.currentPosition + getCurrentTrackStartOffsetMs()
}
fun getCurrentTimeSeconds() : Double {
return getCurrentTime() / 1000.0
}

View file

@ -367,8 +367,14 @@ export default {
this.handleScroll(scrollTop)
},
buildSearchParams() {
if (this.page === 'search' || this.page === 'series' || this.page === 'collections') {
if (this.page === 'search' || this.page === 'collections') {
return ''
} else if (this.page === 'series') {
// Sort by name ascending
let searchParams = new URLSearchParams()
searchParams.set('sort', 'name')
searchParams.set('desc', 0)
return searchParams.toString()
}
let searchParams = new URLSearchParams()

View file

@ -688,12 +688,13 @@ export default {
this.setUserAndConnection(payload)
}
},
async setUserAndConnection({ user, userDefaultLibraryId, serverSettings }) {
async setUserAndConnection({ user, userDefaultLibraryId, serverSettings, ereaderDevices }) {
if (!user) return
console.log('Successfully logged in', JSON.stringify(user))
this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
// Set library - Use last library if set and available fallback to default user library
var lastLibraryId = await this.$localStore.getLastLibraryId()

View file

@ -105,30 +105,28 @@ export default {
this.$nativeHttp
.delete(`/api/me/item/${this.libraryItemId}/bookmark/${bm.time}`)
.then(() => {
this.$toast.success('Bookmark removed')
this.$store.commit('user/deleteBookmark', { libraryItemId: this.libraryItemId, time: bm.time })
})
.catch((error) => {
this.$toast.error(`Failed to remove bookmark`)
console.error(error)
})
this.show = false
},
async clickBookmark(bm) {
await this.$hapticsImpact()
this.$emit('select', bm)
},
submitUpdateBookmark(updatedBookmark) {
var bookmark = { ...updatedBookmark }
this.$nativeHttp
.patch(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark)
.then(() => {
this.$toast.success('Bookmark updated')
.patch(`/api/me/item/${this.libraryItemId}/bookmark`, updatedBookmark)
.then((bookmark) => {
this.$store.commit('user/updateBookmark', bookmark)
this.showBookmarkTitleInput = false
})
.catch((error) => {
this.$toast.error(`Failed to update bookmark`)
console.error(error)
})
this.show = false
},
submitCreateBookmark() {
if (!this.newBookmarkTitle) {

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="width" height="100%">
<template #outer>
<div v-if="title" class="absolute top-8 left-4 z-40" style="max-width: 80%">
<div v-if="title" class="absolute top-10 left-4 z-40 pt-1 pb-1.5" style="max-width: 80%">
<p class="text-white text-xl truncate">{{ title }}</p>
</div>
</template>

View file

@ -0,0 +1,473 @@
<template>
<div>
<modals-dialog v-model="show" :items="moreMenuItems" @action="moreMenuAction" />
<modals-item-details-modal v-model="showDetailsModal" :library-item="libraryItem" />
<modals-dialog v-model="showSendEbookDevicesModal" title="Select a device" :items="ereaderDeviceItems" @action="sendEbookToDeviceAction" />
</div>
</template>
<script>
import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem } from '@/plugins/capacitor'
export default {
props: {
value: Boolean,
processing: Boolean,
libraryItem: {
type: Object,
default: () => {}
},
episode: {
type: Object,
default: () => {}
},
rssFeed: {
type: Object,
default: () => null
},
hideRssFeedOption: Boolean
},
data() {
return {
showDetailsModal: false,
showSendEbookDevicesModal: false
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
moreMenuItems() {
const items = []
// TODO: Implement on iOS
if (this.$platform !== 'ios' && !this.isPodcast) {
items.push({
text: 'History',
value: 'history',
icon: 'history'
})
}
if (!this.isPodcast || this.episode) {
if (!this.userIsFinished) {
items.push({
text: 'Mark as Finished',
value: 'markFinished',
icon: 'beenhere'
})
}
if (this.progressPercent > 0) {
items.push({
text: 'Discard Progress',
value: 'discardProgress',
icon: 'backspace'
})
}
}
if ((!this.isPodcast && this.serverLibraryItemId) || (this.episode && this.serverEpisodeId)) {
items.push({
text: 'Add to Playlist',
value: 'playlist',
icon: 'playlist_add'
})
if (this.ereaderDeviceItems.length) {
items.push({
text: 'Send ebook to device',
value: 'sendEbook',
icon: 'send'
})
}
}
if (this.showRSSFeedOption) {
items.push({
text: this.rssFeed ? 'RSS Feed' : 'Open RSS Feed',
value: 'rssFeed',
icon: 'rss_feed'
})
}
if (this.localLibraryItemId) {
items.push({
text: 'Manage Local Files',
value: 'manageLocal',
icon: 'folder'
})
if (!this.isPodcast) {
items.push({
text: 'Delete Local Item',
value: 'deleteLocal',
icon: 'delete'
})
} else if (this.localEpisodeId) {
items.push({
text: 'Delete Local Episode',
value: 'deleteLocalEpisode',
icon: 'delete'
})
}
}
if (!this.episode) {
items.push({
text: 'More Info',
value: 'details',
icon: 'info'
})
}
return items
},
ereaderDeviceItems() {
if (!this.ebookFile || !this.$store.state.libraries.ereaderDevices?.length) return []
return this.$store.state.libraries.ereaderDevices.map((d) => {
return {
text: d.name,
value: d.name
}
})
},
isConnectedToServer() {
if (!this.isLocal) return true
if (!this.libraryItem?.serverAddress) return false
return this.$store.getters['user/getServerAddress'] === this.libraryItem.serverAddress
},
isLocal() {
return !!this.libraryItem?.isLocal
},
localLibraryItem() {
if (this.isLocal) return this.libraryItem
return this.libraryItem?.localLibraryItem || null
},
localLibraryItemId() {
return this.localLibraryItem?.id || null
},
serverLibraryItemId() {
if (!this.isLocal) return this.libraryItem?.id
if (this.isConnectedToServer) {
return this.libraryItem.libraryItemId
}
return null
},
localEpisode() {
if (this.isLocal) return this.episode
return this.episode.localEpisode
},
localEpisodeId() {
return this.localEpisode?.id || null
},
serverEpisodeId() {
if (!this.isLocal) return this.episode?.id
if (this.isConnectedToServer) {
return this.episode.serverEpisodeId
}
return null
},
mediaType() {
return this.libraryItem?.mediaType
},
isPodcast() {
return this.mediaType == 'podcast'
},
media() {
return this.libraryItem?.media || {}
},
mediaMetadata() {
return this.media.metadata || {}
},
title() {
return this.mediaMetadata.title
},
tracks() {
return this.media.tracks || []
},
episodes() {
return this.media.episodes || []
},
ebookFile() {
return this.media.ebookFile
},
localItemProgress() {
if (this.isPodcast) {
if (!this.localEpisodeId) return null
return this.$store.getters['globals/getLocalMediaProgressById'](this.localLibraryItemId, this.localEpisodeId)
}
return this.$store.getters['globals/getLocalMediaProgressById'](this.localLibraryItemId)
},
serverItemProgress() {
if (this.isPodcast) {
if (!this.serverEpisodeId) return null
return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId, this.serverEpisodeId)
}
return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId)
},
userItemProgress() {
if (this.isLocal) return this.localItemProgress
return this.serverItemProgress
},
userIsFinished() {
return !!this.userItemProgress?.isFinished
},
useEBookProgress() {
if (!this.userItemProgress || this.userItemProgress.progress) return false
return this.userItemProgress.ebookProgress > 0
},
progressPercent() {
if (this.useEBookProgress) return Math.max(Math.min(1, this.userItemProgress.ebookProgress), 0)
return Math.max(Math.min(1, this.userItemProgress?.progress || 0), 0)
},
showRSSFeedOption() {
if (this.hideRssFeedOption) return false
if (!this.serverLibraryItemId) return false
if (!this.rssFeed && !this.episodes.length && !this.tracks.length) return false // Cannot open RSS feed with no episodes/tracks
// If rss feed is open then show feed url to users otherwise just show to admins
return this.userIsAdminOrUp || this.rssFeed
},
mediaId() {
if (this.isPodcast) return null
return this.serverLibraryItemId || this.localLibraryItemId
}
},
methods: {
moreMenuAction(action) {
this.show = false
if (action === 'manageLocal') {
this.$nextTick(() => {
this.$router.push(`/localMedia/item/${this.localLibraryItemId}`)
})
} else if (action === 'details') {
this.showDetailsModal = true
} else if (action === 'playlist') {
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.episode }])
this.$store.commit('globals/setShowPlaylistsAddCreateModal', true)
} else if (action === 'markFinished') {
if (this.episode) this.toggleEpisodeFinished()
else this.toggleFinished()
} else if (action === 'history') {
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
} else if (action === 'discardProgress') {
this.clearProgressClick()
} else if (action === 'deleteLocal') {
this.deleteLocalItem()
} else if (action === 'deleteLocalEpisode') {
this.deleteLocalEpisode()
} else if (action === 'rssFeed') {
this.clickRSSFeed()
} else if (action === 'sendEbook') {
this.showSendEbookDevicesModal = true
}
},
async toggleFinished() {
await this.$hapticsImpact()
// Show confirm if item has progress since it will reset
if (this.userItemProgress && this.userItemProgress.progress > 0 && !this.userIsFinished) {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to mark this item as Finished?'
})
if (!value) return
}
this.$emit('update:processing', true)
if (this.isLocal) {
const isFinished = !this.userIsFinished
const payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId: this.localLibraryItemId, isFinished })
console.log('toggleFinished payload', JSON.stringify(payload))
if (payload?.error) {
this.$toast.error(payload?.error || 'Unknown error')
} else {
const localMediaProgress = payload.localMediaProgress
console.log('toggleFinished localMediaProgress', JSON.stringify(localMediaProgress))
if (localMediaProgress) {
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
}
}
} else {
const updatePayload = {
isFinished: !this.userIsFinished
}
await this.$nativeHttp.patch(`/api/me/progress/${this.serverLibraryItemId}`, updatePayload).catch((error) => {
console.error('Failed', error)
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
}
this.$emit('update:processing', false)
},
async toggleEpisodeFinished() {
await this.$hapticsImpact()
this.$emit('update:processing', true)
if (this.isLocal || this.localEpisode) {
const isFinished = !this.userIsFinished
const localLibraryItemId = this.localLibraryItemId
const localEpisodeId = this.localEpisodeId
const payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId, localEpisodeId, isFinished })
console.log('toggleFinished payload', JSON.stringify(payload))
if (payload?.error) {
this.$toast.error(payload?.error || 'Unknown error')
} else {
const localMediaProgress = payload.localMediaProgress
console.log('toggleFinished localMediaProgress', JSON.stringify(localMediaProgress))
if (localMediaProgress) {
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
}
}
} else {
const updatePayload = {
isFinished: !this.userIsFinished
}
await this.$nativeHttp.patch(`/api/me/progress/${this.serverLibraryItemId}/${this.serverEpisodeId}`, updatePayload).catch((error) => {
console.error('Failed', error)
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
}
this.$emit('update:processing', false)
},
async clearProgressClick() {
await this.$hapticsImpact()
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to reset your progress?'
})
if (value) {
this.$emit('update:processing', true)
const serverMediaProgressId = this.serverItemProgress?.id
if (this.localItemProgress) {
await this.$db.removeLocalMediaProgress(this.localItemProgress.id)
this.$store.commit('globals/removeLocalMediaProgressForItem', this.localItemProgress.id)
}
if (serverMediaProgressId) {
await this.$nativeHttp
.delete(`/api/me/progress/${serverMediaProgressId}`)
.then(() => {
console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`)
this.$store.commit('user/removeMediaProgress', serverMediaProgressId)
})
.catch((error) => {
console.error('Progress reset failed', error)
})
}
this.$emit('update:processing', false)
}
},
async deleteLocalEpisode() {
await this.$hapticsImpact()
const localEpisodeAudioTrack = this.localEpisode.audioTrack
const localFile = this.localLibraryItem.localFiles.find((lf) => lf.id === localEpisodeAudioTrack.localFileId)
if (!localFile) {
this.$toast.error('Audio track does not have matching local file..')
return
}
let confirmMessage = `Remove local episode "${localFile.basePath}" from your device?`
if (this.serverLibraryItemId) {
confirmMessage += ' The file on the server will be unaffected.'
}
const { value } = await Dialog.confirm({
title: 'Confirm',
message: confirmMessage
})
if (value) {
const res = await AbsFileSystem.deleteTrackFromItem({ id: this.localLibraryItemId, trackLocalFileId: localFile.id, trackContentUrl: localEpisodeAudioTrack.contentUrl })
if (res?.id) {
this.$toast.success('Deleted episode successfully')
if (this.isLocal) {
// If this is local episode then redirect to server episode when available
if (this.serverEpisodeId) {
this.$router.replace(`/item/${this.serverLibraryItemId}/${this.serverEpisodeId}`)
} else {
this.$router.replace(`/item/${this.localLibraryItemId}`)
}
} else {
// Update local library item and local episode
this.libraryItem.localLibraryItem = res
this.$delete(this.episode, 'localEpisode')
}
} else this.$toast.error('Failed to delete')
}
},
async deleteLocalItem() {
await this.$hapticsImpact()
let confirmMessage = 'Remove local files of this item from your device?'
if (this.serverLibraryItemId) {
confirmMessage += ' The files on the server and your progress will be unaffected.'
}
const { value } = await Dialog.confirm({
title: 'Confirm',
message: confirmMessage
})
if (value) {
const res = await AbsFileSystem.deleteItem(this.localLibraryItem)
if (res?.success) {
this.$toast.success('Deleted successfully')
if (this.isLocal) {
// If local then redirect to server version when available
if (this.serverLibraryItemId) {
this.$router.replace(`/item/${this.serverLibraryItemId}`)
} else {
this.$router.replace('/bookshelf')
}
} else {
// Remove localLibraryItem
this.$delete(this.libraryItem, 'localLibraryItem')
}
} else this.$toast.error('Failed to delete')
}
},
clickRSSFeed() {
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
id: this.serverLibraryItemId,
name: this.title,
type: 'item',
feed: this.rssFeed,
hasEpisodesWithoutPubDate: this.episodes.some((ep) => !ep.pubDate)
})
},
sendEbookToDeviceAction(deviceName) {
this.showSendEbookDevicesModal = false
const payload = {
libraryItemId: this.serverLibraryItemId,
deviceName
}
this.$emit('update:processing', true)
this.$nativeHttp
.post(`/api/emails/send-ebook-to-device`, payload)
.then(() => {
this.$toast.success('Ebook sent successfully')
})
.catch((error) => {
console.error('Failed to send ebook to device', error)
this.$toast.error('Failed to send ebook to device')
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
mounted() {}
}
</script>

View file

@ -1,6 +1,7 @@
<template>
<div class="w-full px-2 py-2 overflow-hidden relative">
<nuxt-link v-if="libraryItem" :to="`/item/${libraryItem.id}`" class="flex items-center w-full">
<div class="w-full px-1.5 pb-1.5">
<div class="w-full h-full p-2 rounded-lg relative bg-bg overflow-hidden">
<nuxt-link v-if="libraryItem" :to="itemUrl" class="flex items-center w-full">
<div class="h-full relative" :style="{ width: '50px' }">
<covers-book-cover :library-item="libraryItem" :width="50" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
@ -16,7 +17,14 @@
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
</button>
</div>
<div class="w-8 min-w-8 flex justify-center">
<button class="w-8 h-8 rounded-full flex items-center justify-center" @click.stop.prevent="showMore">
<span class="material-icons">more_vert</span>
</button>
</div>
</nuxt-link>
<div class="absolute bottom-0 left-0 h-0.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: progressPercent * 100 + '%' }"></div>
</div>
</div>
</template>
@ -30,12 +38,13 @@ export default {
}
},
data() {
return {
isProcessingReadUpdate: false,
processingRemove: false
}
return {}
},
computed: {
itemUrl() {
if (this.episodeId) return `/item/${this.libraryItem.id}/${this.episodeId}`
return `/item/${this.libraryItem.id}`
},
libraryItem() {
return this.item.libraryItem || {}
},
@ -57,6 +66,12 @@ export default {
mediaMetadata() {
return this.media.metadata || {}
},
mediaType() {
return this.libraryItem.mediaType
},
isPodcast() {
return this.mediaType === 'podcast'
},
tracks() {
if (this.episode) return []
return this.media.tracks || []
@ -92,12 +107,6 @@ export default {
coverWidth() {
return 50
},
isMissing() {
return this.libraryItem.isMissing
},
isInvalid() {
return this.libraryItem.isInvalid
},
showPlayBtn() {
return !this.isMissing && !this.isInvalid && (this.tracks.length || this.episode)
},
@ -107,9 +116,31 @@ export default {
},
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
},
userItemProgress() {
return this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, this.episodeId)
},
userIsFinished() {
return !!this.userItemProgress?.isFinished
},
progressPercent() {
return Math.max(Math.min(1, this.userItemProgress?.progress || 0), 0)
}
},
methods: {
showMore() {
const playlistItem = {
libraryItem: this.libraryItem,
episode: this.episode
}
if (this.localLibraryItem) {
playlistItem.libraryItem.localLibraryItem = this.localLibraryItem
}
if (this.localEpisode && playlistItem.episode) {
playlistItem.episode.localEpisode = this.localEpisode
}
this.$emit('showMore', playlistItem)
},
async playClick() {
await this.$hapticsImpact()
if (this.streamIsPlaying) {
@ -135,7 +166,7 @@ export default {
<style>
.item-table-content {
width: calc(100% - 82px);
max-width: calc(100% - 82px);
width: calc(100% - 114px);
max-width: calc(100% - 114px);
}
</style>

View file

@ -1,7 +1,7 @@
<template>
<div class="w-full bg-primary bg-opacity-40">
<div class="w-full h-14 flex items-center px-4 bg-primary">
<p class="pr-4">Playlist Items</p>
<div class="w-full bg-primary/50 rounded-lg">
<div class="w-full h-14 flex items-center px-3">
<p class="pr-2">Playlist Items</p>
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
<span class="text-xs md:text-sm font-mono leading-none">{{ items.length }}</span>
@ -11,7 +11,7 @@
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
</div>
<template v-for="item in items">
<tables-playlist-item-table-row :key="item.id" :item="item" :playlist-id="playlistId" />
<tables-playlist-item-table-row :key="item.id" :item="item" :playlist-id="playlistId" @showMore="showMore" />
</template>
</div>
</template>
@ -41,7 +41,11 @@ export default {
return this.$elapsedPrettyExtended(this.totalDuration)
}
},
methods: {},
methods: {
showMore(playlistItem) {
this.$emit('showMore', playlistItem)
}
},
mounted() {}
}
</script>

View file

@ -152,8 +152,9 @@ export default {
return
}
const { user, userDefaultLibraryId, serverSettings } = authRes
const { user, userDefaultLibraryId, serverSettings, ereaderDevices } = authRes
this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
// Set library - Use last library if set and available fallback to default user library
const lastLibraryId = await this.$localStore.getLastLibraryId()

View file

@ -50,7 +50,7 @@
</ui-btn>
</div>
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 text-center" :class="resettingProgress ? 'opacity-25' : ''">
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 text-center">
<p>Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="!useEBookProgress && !userIsFinished" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p v-else-if="userIsFinished" class="text-gray-400 text-xs">Finished {{ $formatDate(userProgressFinishedAt) }}</p>
@ -126,14 +126,16 @@
<tables-ebook-files-table v-if="ebookFiles.length" :library-item="libraryItem" />
<!-- modals -->
<modals-item-more-menu-modal v-model="showMoreMenu" :library-item="libraryItem" :rss-feed="rssFeed" :processing.sync="processing" />
<modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" />
<modals-dialog v-model="showMoreMenu" :items="moreMenuItems" @action="moreMenuAction" />
<modals-item-details-modal v-model="showDetailsModal" :library-item="libraryItem" />
<modals-fullscreen-cover v-model="showFullscreenCover" :library-item="libraryItem" />
</div>
<div v-show="processing" class="fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black/50 z-50">
<ui-loading-indicator />
</div>
</div>
</template>
@ -175,11 +177,9 @@ export default {
},
data() {
return {
resettingProgress: false,
isProcessingReadUpdate: false,
processing: false,
showSelectLocalFolder: false,
showMoreMenu: false,
showDetailsModal: false,
showFullscreenCover: false,
coverRgb: 'rgb(55, 56, 56)',
coverBgIsLight: false,
@ -311,7 +311,7 @@ export default {
return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId)
},
userIsFinished() {
return this.userItemProgress ? !!this.userItemProgress.isFinished : false
return !!this.userItemProgress?.isFinished
},
userTimeRemaining() {
if (!this.userItemProgress) return 0
@ -324,10 +324,10 @@ export default {
},
progressPercent() {
if (this.useEBookProgress) return Math.max(Math.min(1, this.userItemProgress.ebookProgress), 0)
return this.userItemProgress ? Math.max(Math.min(1, this.userItemProgress.progress), 0) : 0
return Math.max(Math.min(1, this.userItemProgress?.progress || 0), 0)
},
userProgressFinishedAt() {
return this.userItemProgress ? this.userItemProgress.finishedAt : 0
return this.userItemProgress?.finishedAt || 0
},
isStreaming() {
return this.isPlaying && !this.$store.getters['getIsCurrentSessionLocal']
@ -387,83 +387,6 @@ export default {
isCasting() {
return this.$store.state.isCasting
},
showRSSFeedOption() {
if (!this.serverLibraryItemId) return false
if (!this.rssFeed && !this.episodes.length && !this.tracks.length) return false // Cannot open RSS feed with no episodes/tracks
// If rss feed is open then show feed url to users otherwise just show to admins
return this.userIsAdminOrUp || this.rssFeed
},
moreMenuItems() {
const items = []
if (!this.isPodcast) {
// TODO: Implement on iOS
if (!this.isIos) {
items.push({
text: 'History',
value: 'history',
icon: 'history'
})
}
if (!this.userIsFinished) {
items.push({
text: 'Mark as Finished',
value: 'markFinished',
icon: 'beenhere'
})
}
if (this.progressPercent > 0) {
items.push({
text: 'Discard Progress',
value: 'discardProgress',
icon: 'backspace'
})
}
}
if (!this.isPodcast && this.serverLibraryItemId) {
items.push({
text: 'Add to Playlist',
value: 'playlist',
icon: 'playlist_add'
})
}
if (this.showRSSFeedOption) {
items.push({
text: this.rssFeed ? 'RSS Feed' : 'Open RSS Feed',
value: 'rssFeed',
icon: 'rss_feed'
})
}
if (this.localLibraryItemId) {
items.push({
text: 'Manage Local Files',
value: 'manageLocal',
icon: 'folder'
})
if (!this.isPodcast) {
items.push({
text: 'Delete Local Item',
value: 'deleteLocal',
icon: 'delete'
})
}
}
items.push({
text: 'More Info',
value: 'details',
icon: 'info'
})
return items
},
coverWidth() {
let width = this.windowWidth - 94
if (width > 325) return 325
@ -471,42 +394,9 @@ export default {
if (width * this.bookCoverAspectRatio > 325) width = 325 / this.bookCoverAspectRatio
return width
},
mediaId() {
if (this.isPodcast) return null
return this.serverLibraryItemId || this.localLibraryItemId
}
},
methods: {
async deleteLocalItem() {
await this.$hapticsImpact()
let confirmMessage = 'Remove local files of this item from your device?'
if (this.serverLibraryItemId) {
confirmMessage += ' The files on the server and your progress will be unaffected.'
}
const { value } = await Dialog.confirm({
title: 'Confirm',
message: confirmMessage
})
if (value) {
const res = await AbsFileSystem.deleteItem(this.localLibraryItem)
if (res?.success) {
this.$toast.success('Deleted successfully')
if (this.isLocal) {
// If local then redirect to server version when available
if (this.serverLibraryItemId) {
this.$router.replace(`/item/${this.serverLibraryItemId}`)
} else {
this.$router.replace('/bookshelf')
}
} else {
// Remove localLibraryItem
this.$delete(this.libraryItem, 'localLibraryItem')
}
} else this.$toast.error('Failed to delete')
}
},
async coverImageLoaded(fullCoverUrl) {
if (!fullCoverUrl) return
@ -521,39 +411,6 @@ export default {
console.log(e)
})
},
moreMenuAction(action) {
this.showMoreMenu = false
if (action === 'manageLocal') {
this.$nextTick(() => {
this.$router.push(`/localMedia/item/${this.localLibraryItemId}`)
})
} else if (action === 'details') {
this.showDetailsModal = true
} else if (action === 'playlist') {
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: null }])
this.$store.commit('globals/setShowPlaylistsAddCreateModal', true)
} else if (action === 'markFinished') {
if (this.isProcessingReadUpdate) return
this.toggleFinished()
} else if (action === 'history') {
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
} else if (action === 'discardProgress') {
this.clearProgressClick()
} else if (action === 'deleteLocal') {
this.deleteLocalItem()
} else if (action === 'rssFeed') {
this.clickRSSFeed()
}
},
clickRSSFeed() {
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
id: this.serverLibraryItemId,
name: this.title,
type: 'item',
feed: this.rssFeed,
hasEpisodesWithoutPubDate: this.episodes.some((ep) => !ep.pubDate)
})
},
moreButtonPress() {
this.showMoreMenu = true
},
@ -635,37 +492,6 @@ export default {
this.$eventBus.$emit('play-item', { libraryItemId, serverLibraryItemId: this.serverLibraryItemId, startTime })
}
},
async clearProgressClick() {
await this.$hapticsImpact()
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to reset your progress?'
})
if (value) {
this.resettingProgress = true
const serverMediaProgressId = this.serverItemProgress?.id
if (this.localLibraryItemId) {
await this.$db.removeLocalMediaProgress(this.localLibraryItemId)
this.$store.commit('globals/removeLocalMediaProgressForItem', this.localLibraryItemId)
}
if (this.serverLibraryItemId && serverMediaProgressId) {
await this.$nativeHttp
.delete(`/api/me/progress/${serverMediaProgressId}`)
.then(() => {
console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`)
this.$store.commit('user/removeMediaProgress', serverMediaProgressId)
})
.catch((error) => {
console.error('Progress reset failed', error)
})
}
this.resettingProgress = false
}
},
itemUpdated(libraryItem) {
if (libraryItem.id === this.serverLibraryItemId) {
console.log('Item Updated')
@ -767,48 +593,6 @@ export default {
this.$set(this.libraryItem, 'localLibraryItem', item)
}
},
async toggleFinished() {
await this.$hapticsImpact()
// Show confirm if item has progress since it will reset
if (this.userItemProgress && this.userItemProgress.progress > 0 && !this.userIsFinished) {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to mark this item as Finished?'
})
if (!value) return
}
this.isProcessingReadUpdate = true
if (this.isLocal) {
const isFinished = !this.userIsFinished
const payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId: this.localLibraryItemId, isFinished })
console.log('toggleFinished payload', JSON.stringify(payload))
if (payload?.error) {
this.$toast.error(payload?.error || 'Unknown error')
} else {
const localMediaProgress = payload.localMediaProgress
console.log('toggleFinished localMediaProgress', JSON.stringify(localMediaProgress))
if (localMediaProgress) {
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
}
}
this.isProcessingReadUpdate = false
} else {
const updatePayload = {
isFinished: !this.userIsFinished
}
this.$nativeHttp
.patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
.catch((error) => {
console.error('Failed', error)
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
.finally(() => {
this.isProcessingReadUpdate = false
})
}
},
libraryChanged(libraryId) {
if (this.libraryItem.libraryId !== libraryId) {
this.$router.replace('/bookshelf')

View file

@ -1,11 +1,11 @@
<template>
<div class="w-full h-full">
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
<div class="w-full h-full overflow-y-auto py-6 md:p-8">
<div class="w-full flex justify-center">
<covers-playlist-cover :items="playlistItems" :width="180" :height="180" />
</div>
<div class="flex-grow py-6">
<div class="flex items-center px-2">
<div class="flex-grow px-1 py-6">
<div class="flex items-center px-3">
<h1 class="text-xl font-sans">
{{ playlistName }}
</h1>
@ -16,13 +16,18 @@
</ui-btn>
</div>
<div class="my-8 max-w-2xl px-2">
<div class="my-8 max-w-2xl px-3">
<p class="text-base text-gray-100">{{ description }}</p>
</div>
<tables-playlist-items-table :items="playlistItems" :playlist-id="playlist.id" />
<tables-playlist-items-table :items="playlistItems" :playlist-id="playlist.id" @showMore="showMore" />
</div>
</div>
<modals-item-more-menu-modal v-model="showMoreMenu" :library-item="selectedLibraryItem" :episode="selectedEpisode" hide-rss-feed-option :processing.sync="processing" />
<div v-show="processing" class="fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black/50 z-50">
<ui-loading-indicator />
</div>
</div>
</template>
@ -67,7 +72,12 @@ export default {
}
},
data() {
return {}
return {
showMoreMenu: false,
processing: false,
selectedLibraryItem: null,
selectedEpisode: null
}
},
computed: {
bookCoverAspectRatio() {
@ -101,6 +111,11 @@ export default {
}
},
methods: {
showMore(playlistItem) {
this.selectedLibraryItem = playlistItem.libraryItem
this.selectedEpisode = playlistItem.episode
this.showMoreMenu = true
},
clickPlay() {
const nextItem = this.playableItems.find((i) => {
const prog = this.$store.getters['user/getUserMediaProgress'](i.libraryItemId, i.episodeId)

View file

@ -7,7 +7,8 @@ export const state = () => ({
showModal: false,
issues: 0,
filterData: null,
numUserPlaylists: 0
numUserPlaylists: 0,
ereaderDevices: []
})
export const getters = {
@ -177,5 +178,8 @@ export const mutations = {
if (genre && !state.filterData.genres.includes(genre)) state.filterData.genres.push(genre)
})
}
},
setEReaderDevices(state, ereaderDevices) {
state.ereaderDevices = ereaderDevices
}
}

View file

@ -145,5 +145,21 @@ export const mutations = {
setSettings(state, settings) {
if (!settings) return
state.settings = settings
},
updateBookmark(state, bookmark) {
if (!state.user?.bookmarks) return
state.user.bookmarks = state.user.bookmarks.map((bm) => {
if (bm.libraryItemId === bookmark.libraryItemId && bm.time === bookmark.time) {
return bookmark
}
return bm
})
},
deleteBookmark(state, { libraryItemId, time }) {
if (!state.user?.bookmarks) return
state.user.bookmarks = state.user.bookmarks.filter(bm => {
if (bm.libraryItemId === libraryItemId && bm.time === time) return false
return true
})
}
}