Merge branch 'master' into social

This commit is contained in:
Ben 2022-08-20 11:52:41 -04:00 committed by GitHub
commit 92c393d2b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
351 changed files with 53592 additions and 1765 deletions

View file

@ -17,6 +17,7 @@
<div class="flex items-center">
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p>
<div class="flex-grow" />
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn>
<ui-btn color="success" small @click="saveChapters">Save</ui-btn>
<div class="w-40" />
@ -32,7 +33,8 @@
<div :key="chapter.id" class="flex py-1">
<div class="w-12">#{{ chapter.id + 1 }}</div>
<div class="w-32 px-1">
<ui-text-input v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
</div>
<div class="flex-grow px-1">
<ui-text-input v-model="chapter.title" class="text-xs" />
@ -136,7 +138,7 @@
<script>
export default {
async asyncData({ store, params, app, redirect, route }) {
async asyncData({ store, params, app, redirect, from }) {
if (!store.getters['user/getUserCanUpdate']) {
return redirect('/?error=unauthorized')
}
@ -152,8 +154,12 @@ export default {
console.error('Invalid media type')
return redirect('/')
}
var previousRoute = from ? from.fullPath : null
if (from && from.path === '/login') previousRoute = null
return {
libraryItem
libraryItem,
previousRoute
}
},
data() {
@ -168,7 +174,8 @@ export default {
asinInput: null,
findingChapters: false,
showFindChaptersModal: false,
chapterData: null
chapterData: null,
showSecondInputs: false
}
},
computed: {
@ -339,7 +346,6 @@ export default {
this.saving = true
console.log('udpated chapters', this.newChapters)
const payload = {
chapters: this.newChapters
}
@ -349,7 +355,11 @@ export default {
this.saving = false
if (data.updated) {
this.$toast.success('Chapters updated')
this.$router.push(`/item/${this.libraryItem.id}`)
if (this.previousRoute) {
this.$router.push(this.previousRoute)
} else {
this.$router.push(`/item/${this.libraryItem.id}`)
}
} else {
this.$toast.info('No changes needed updating')
}

View file

@ -144,12 +144,6 @@ export default {
isPodcastLibrary() {
return this.mediaType === 'podcast'
},
coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio')
},
bookCoverAspectRatio() {
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},

View file

@ -8,8 +8,8 @@
</div>
</div>
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
<div class="flex sm:items-end flex-col sm:flex-row">
<h1 class="text-2xl md:text-3xl font-sans">
<div class="flex items-end flex-row flex-wrap md:flex-nowrap">
<h1 class="text-2xl md:text-3xl font-sans w-full md:w-fit mb-4 md:mb-0">
{{ collectionName }}
</h1>
<div class="flex-grow" />
@ -65,7 +65,7 @@ export default {
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem

View file

@ -71,6 +71,11 @@ export default {
.configContent.page-library-stats {
width: 1200px;
}
@media (max-width: 1550px) {
.configContent.page-library-stats {
margin-left: 176px;
}
}
@media (max-width: 1240px) {
.configContent {
margin-left: 176px;
@ -82,5 +87,8 @@ export default {
width: 100%;
max-width: 100%;
}
.configContent.page-library-stats {
margin-left: 0px;
}
}
</style>

View file

@ -8,12 +8,20 @@
<p class="text-base mb-4 text-gray-300">Backups include users, user progress, book details, server settings and covers stored in <span class="font-mono text-gray-100">/metadata/items</span>. <br />Backups <strong>do not</strong> include any files stored in your library folders.</p>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="dailyBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
<ui-tooltip :text="dailyBackupsTooltip">
<p class="pl-4 text-lg">Run daily backups <span class="material-icons icon-text">info_outlined</span></p>
<ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
<ui-tooltip :text="backupsTooltip">
<p class="pl-4 text-lg">Enable automatic backups <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
<div v-if="enableBackups" class="mb-6">
<div class="flex items-center pl-6">
<span class="material-icons-outlined text-black-50">schedule</span>
<p class="text-gray-100 px-2">{{ scheduleDescription }}</p>
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer" @click="showCronBuilder = !showCronBuilder">edit</span>
</div>
</div>
<div class="flex items-center py-2">
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
@ -30,6 +38,8 @@
<tables-backups-table />
</div>
<modals-backup-schedule-modal v-model="showCronBuilder" :cron-expression.sync="cronExpression" />
</div>
</template>
@ -38,10 +48,12 @@ export default {
data() {
return {
updatingServerSettings: false,
dailyBackups: true,
enableBackups: true,
backupsToKeep: 2,
maxBackupSize: 1,
newServerSettings: {}
cronExpression: '',
newServerSettings: {},
showCronBuilder: false
}
},
watch: {
@ -53,14 +65,19 @@ export default {
}
},
computed: {
dailyBackupsTooltip() {
return 'Runs at 1:30am every day (your server time). Saved in /metadata/backups.'
backupsTooltip() {
return 'Backups saved in /metadata/backups'
},
maxBackupSizeTooltip() {
return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
},
serverSettings() {
return this.$store.state.serverSettings
},
scheduleDescription() {
if (!this.cronExpression) return ''
const parsed = this.$parseCronExpression(this.cronExpression)
return parsed ? parsed.description : 'Custom cron expression ' + this.cronExpression
}
},
methods: {
@ -74,7 +91,7 @@ export default {
return
}
var updatePayload = {
backupSchedule: this.dailyBackups ? '30 1 * * *' : false,
backupSchedule: this.enableBackups ? this.cronExpression : false,
backupsToKeep: Number(this.backupsToKeep),
maxBackupSize: Number(this.maxBackupSize)
}
@ -97,8 +114,9 @@ export default {
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
this.dailyBackups = !!this.newServerSettings.backupSchedule
this.enableBackups = !!this.newServerSettings.backupSchedule
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
this.cronExpression = this.newServerSettings.backupSchedule || '30 1 * * *'
}
},
mounted() {

View file

@ -62,7 +62,7 @@
<h2 class="font-semibold">Display</h2>
</div>
<div class="flex items-center py-2">
<!-- <div class="flex items-center py-2">
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
<ui-tooltip :text="tooltips.coverAspectRatio">
<p class="pl-4">
@ -70,6 +70,15 @@
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div> -->
<div class="flex items-center py-2">
<ui-toggle-switch v-model="homeUseAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateHomeAlternativeBookshelfView" />
<ui-tooltip :text="tooltips.bookshelfView">
<p class="pl-4">
Alternative bookshelf view for home page
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
@ -167,6 +176,16 @@
</ui-tooltip>
</div>
<!-- <div class="flex items-center py-2">
<ui-text-input type="number" v-model="newServerSettings.scannerMaxThreads" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateScannerMaxThreads" />
<ui-tooltip :text="tooltips.scannerMaxThreads">
<p class="pl-4">
Max # of threads to use
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div> -->
<div class="pt-4">
<h2 class="font-semibold">Experimental Features</h2>
</div>
@ -194,6 +213,16 @@
</p>
</ui-tooltip>
</div>
<!-- <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerUseSingleThreadedProber" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerUseSingleThreadedProber', val)" />
<ui-tooltip :text="tooltips.scannerUseSingleThreadedProber">
<p class="pl-4">
Scanner use old single threaded audio prober
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div> -->
</div>
</div>
</div>
@ -261,6 +290,7 @@ export default {
isResettingLibraryItems: false,
updatingServerSettings: false,
useSquareBookCovers: false,
homeUseAlternativeBookshelfView: false,
useAlternativeBookshelfView: false,
isPurgingCache: false,
newServerSettings: {},
@ -280,6 +310,8 @@ export default {
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)',
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically',
shareStats: 'Allow users to share latest listening activity with other users on an opt-in basis'
scannerUseSingleThreadedProber: 'The old scanner used a single thread. Leaving it in to use as a comparison for now.',
scannerMaxThreads: 'Number of concurrent media files to scan at a time. Value of 1 will be a slower scan but less CPU usage. <br><br>Value of 0 defaults to # of CPU cores for this server times 2 (i.e. 4-core CPU will be 8)'
},
showConfirmPurgeCache: false
}
@ -311,6 +343,26 @@ export default {
}
},
methods: {
updateScannerMaxThreads(val) {
if (!val || isNaN(val)) {
this.$toast.error('Invalid max threads must be a number')
this.newServerSettings.scannerMaxThreads = 0
return
}
if (Number(val) < 0) {
this.$toast.error('Max threads must be >= 0')
this.newServerSettings.scannerMaxThreads = 0
return
}
if (Math.round(Number(val)) !== Number(val)) {
this.$toast.error('Max threads must be an integer')
this.newServerSettings.scannerMaxThreads = 0
return
}
this.updateServerSettings({
scannerMaxThreads: Number(val)
})
},
updateSortingPrefixes(val) {
if (!val || !val.length) {
this.$toast.error('Must have at least 1 prefix')
@ -331,6 +383,11 @@ export default {
coverAspectRatio: val ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD
})
},
updateHomeAlternativeBookshelfView(val) {
this.updateServerSettings({
homeBookshelfView: val ? this.$constants.BookshelfView.TITLES : this.$constants.BookshelfView.STANDARD
})
},
updateAlternativeBookshelfView(val) {
this.updateServerSettings({
bookshelfView: val ? this.$constants.BookshelfView.TITLES : this.$constants.BookshelfView.STANDARD
@ -362,6 +419,7 @@ export default {
this.useSquareBookCovers = this.newServerSettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
this.homeUseAlternativeBookshelfView = this.newServerSettings.homeBookshelfView === this.$constants.BookshelfView.TITLES
this.useAlternativeBookshelfView = this.newServerSettings.bookshelfView === this.$constants.BookshelfView.TITLES
},
resetLibraryItems() {

View file

@ -3,7 +3,7 @@
<h1 class="text-xl">Stats for library {{ currentLibraryName }}</h1>
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
<div class="flex md:flex-row flex-wrap justify-between flex-col mt-8">
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
<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>

View file

@ -161,6 +161,7 @@ export default {
},
beforeDestroy() {
if (!this.$root.socket) return
this.$root.socket.emit('remove_log_listener')
this.$root.socket.off('daily_logs', this.dailyLogsLoaded)
this.$root.socket.off('log', this.logEvtReceived)
}

View file

@ -39,7 +39,7 @@
<td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
</td>
<td class="text-center">
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td>
<td class="text-center hidden sm:table-cell">
@ -57,8 +57,8 @@
</div>
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
</div>
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
</div>
</template>
@ -89,7 +89,8 @@ export default {
total: 0,
currentPage: 0,
userFilter: null,
selectedUser: ''
selectedUser: '',
processingGoToTimestamp: false
}
},
computed: {
@ -110,6 +111,44 @@ export default {
}
},
methods: {
removedSession() {
this.loadSessions(this.currentPage)
},
async clickCurrentTime(session) {
if (this.processingGoToTimestamp) return
this.processingGoToTimestamp = true
const libraryItem = await this.$axios.$get(`/api/items/${session.libraryItemId}`).catch((error) => {
console.error('Failed to get library item', error)
return null
})
if (!libraryItem) {
this.$toast.error('Failed to get library item')
this.processingGoToTimestamp = false
return
}
if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode')
this.processingGoToTimestamp = false
return
}
const payload = {
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`,
callback: (confirmed) => {
if (confirmed) {
this.$eventBus.$emit('play-item', {
libraryItemId: libraryItem.id,
episodeId: session.episodeId || null,
startTime: session.currentTime
})
}
this.processingGoToTimestamp = false
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
updateUserFilter() {
this.loadSessions(0)
},

View file

@ -13,11 +13,12 @@
<widgets-online-indicator :value="!!userOnline" />
<h1 class="text-xl pl-2">{{ username }}</h1>
</div>
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)">
<p v-if="userToken" class="py-2 text-xs">
<strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span
><span class="material-icons pl-2 text-base">content_copy</span>
</p>
<div v-if="userToken" class="flex text-xs mt-4">
<ui-text-input-with-label label="API Token" :value="userToken" readonly />
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
<span class="material-icons pl-2 text-base">content_copy</span>
</div>
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2">
@ -104,11 +105,8 @@ export default {
userToken() {
return this.user.token
},
coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio')
},
bookCoverAspectRatio() {
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
username() {
return this.user.username
@ -138,12 +136,15 @@ export default {
this.$copyToClipboard(str, this)
},
async init() {
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`).then((data) => {
return data.sessions || []
}).catch((err) => {
console.error('Failed to load listening sesions', err)
return []
})
this.listeningSessions = await this.$axios
.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`)
.then((data) => {
return data.sessions || []
})
.catch((err) => {
console.error('Failed to load listening sesions', err)
return []
})
this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => {
console.error('Failed to load listening sesions', err)
return []

View file

@ -42,7 +42,7 @@
<td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
</td>
<td class="text-center">
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td>
<td class="text-center hidden sm:table-cell">
@ -62,7 +62,7 @@
</div>
</div>
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
</div>
</template>
@ -85,7 +85,8 @@ export default {
listeningSessions: [],
numPages: 0,
total: 0,
currentPage: 0
currentPage: 0,
processingGoToTimestamp: false
}
},
computed: {
@ -97,6 +98,44 @@ export default {
}
},
methods: {
removedSession() {
this.loadSessions(this.currentPage)
},
async clickCurrentTime(session) {
if (this.processingGoToTimestamp) return
this.processingGoToTimestamp = true
const libraryItem = await this.$axios.$get(`/api/items/${session.libraryItemId}`).catch((error) => {
console.error('Failed to get library item', error)
return null
})
if (!libraryItem) {
this.$toast.error('Failed to get library item')
this.processingGoToTimestamp = false
return
}
if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode')
this.processingGoToTimestamp = false
return
}
const payload = {
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`,
callback: (confirmed) => {
if (confirmed) {
this.$eventBus.$emit('play-item', {
libraryItemId: libraryItem.id,
episodeId: session.episodeId || null,
startTime: session.currentTime
})
}
this.processingGoToTimestamp = false
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
prevPage() {
this.loadSessions(this.currentPage - 1)
},

View file

@ -7,11 +7,11 @@
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<!-- Item Progress Bar -->
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
<!-- Item Cover Overlay -->
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
<div v-show="showPlayButton && !streaming" class="h-full flex items-center justify-center pointer-events-none">
<div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="startStream">
<span class="material-icons text-4xl">play_circle_filled</span>
</div>
@ -24,12 +24,13 @@
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
<div class="flex justify-center">
<div class="mb-4">
<div class="flex sm:items-end flex-col sm:flex-row">
<h1 class="text-2xl md:text-3xl font-sans">
{{ title }}
</h1>
<p v-if="bookSubtitle" class="sm:ml-4 text-gray-400 text-xl md:text-2xl">{{ bookSubtitle }}</p>
</div>
<h1 class="text-2xl md:text-3xl font-semibold">
{{ title }}
</h1>
<p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p>
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ _series.text }}</nuxt-link>
<template v-if="!isVideo">
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
@ -39,8 +40,6 @@
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
</template>
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ _series.text }}</nuxt-link>
<div v-if="narrator" class="flex py-0.5 mt-4">
<div class="w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Narrated By</span>
@ -129,12 +128,12 @@
<!-- Icon buttons -->
<div class="flex items-center justify-center md:justify-start pt-4">
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ streaming ? 'Playing' : 'Play' }}
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ isStreaming ? 'Playing' : 'Play' }}
</ui-btn>
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span>
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">error</span>
{{ isMissing ? 'Missing' : 'Incomplete' }}
</ui-btn>
@ -160,7 +159,11 @@
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
</ui-tooltip>
<!-- Experimental RSS feed open -->
<ui-tooltip v-if="bookmarks.length" text="Your Bookmarks" direction="top">
<ui-icon-btn :icon="bookmarks.length ? 'bookmarks' : 'bookmark_border'" class="mx-0.5" @click="clickBookmarksBtn" />
</ui-tooltip>
<!-- RSS feed -->
<ui-tooltip v-if="showRssFeedBtn" text="Open RSS Feed" direction="top">
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
</ui-tooltip>
@ -189,6 +192,7 @@
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
<modals-rssfeed-view-modal v-model="showRssFeedModal" :library-item="libraryItem" :feed-url="rssFeedUrl" />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
</div>
</template>
@ -222,7 +226,8 @@ export default {
podcastFeedEpisodes: [],
episodesDownloading: [],
episodeDownloadsQueued: [],
showRssFeedModal: false
showRssFeedModal: false,
showBookmarksModal: false
}
},
computed: {
@ -241,11 +246,8 @@ export default {
isFile() {
return this.libraryItem.isFile
},
coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio')
},
bookCoverAspectRatio() {
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
bookCoverWidth() {
return 208
@ -296,6 +298,10 @@ export default {
chapters() {
return this.media.chapters || []
},
bookmarks() {
if (this.isPodcast) return []
return this.$store.getters['user/getUserBookmarksForItem'](this.libraryItemId)
},
tracks() {
return this.media.tracks || []
},
@ -389,7 +395,7 @@ export default {
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
streaming() {
isStreaming() {
return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId
},
userCanUpdate() {
@ -409,6 +415,31 @@ export default {
}
},
methods: {
clickBookmarksBtn() {
this.showBookmarksModal = true
},
selectBookmark(bookmark) {
if (!bookmark) return
if (this.isStreaming) {
this.$eventBus.$emit('playback-seek', bookmark.time)
} else if (this.streamLibraryItem) {
this.showBookmarksModal = false
console.log('Already streaming library item so ask about it')
const payload = {
message: `Start playback for "${this.title}" at ${this.$secondsToTimestamp(bookmark.time)}?`,
callback: (confirmed) => {
if (confirmed) {
this.startStream(bookmark.time)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
} else {
this.startStream(bookmark.time)
}
this.showBookmarksModal = false
},
clearDownloadQueue() {
if (confirm('Are you sure you want to clear episode download queue?')) {
this.$axios
@ -453,7 +484,21 @@ export default {
openEbook() {
this.$store.commit('showEReader', this.libraryItem)
},
toggleFinished() {
toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) {
const payload = {
message: `Are you sure you want to mark "${this.title}" as finished?`,
callback: (confirmed) => {
if (confirmed) {
this.toggleFinished(true)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
return
}
var updatePayload = {
isFinished: !this.userIsFinished
}
@ -470,7 +515,7 @@ export default {
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
},
startStream() {
startStream(startTime = null) {
var episodeId = null
if (this.isPodcast) {
var episode = this.podcastEpisodes.find((ep) => {
@ -483,7 +528,8 @@ export default {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItem.id,
episodeId
episodeId,
startTime
})
},
editClick() {

View file

@ -124,9 +124,10 @@ export default {
location.reload()
},
setUser({ user, userDefaultLibraryId, serverSettings, Source }) {
setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('setSource', Source)
this.$store.commit('feeds/setFeeds', feeds)
if (serverSettings.chromecastEnabled) {
console.log('Chromecast enabled import script')