diff --git a/client/components/AbsSelect.vue b/client/components/AbsSelect.vue
deleted file mode 100644
index 5c0e1cd50..000000000
--- a/client/components/AbsSelect.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
- -
- {{ opt.label }}
-
- - No options
-
-
-
-
-
-
-
-
diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue
index 98d1904e7..f74134041 100644
--- a/client/components/app/Appbar.vue
+++ b/client/components/app/Appbar.vue
@@ -42,31 +42,15 @@
-
-
-
- inbox
-
-
-
-
-
- send
-
-
-
-
{{ username }}
-
-
{{ $getString('MessageItemsSelected', [numMediaItemsSelected]) }}
@@ -74,20 +58,17 @@
play_arrow
{{ $strings.ButtonPlay }}
-
-
-
@@ -179,22 +160,26 @@ export default {
},
contextMenuItems() {
if (!this.userIsAdminOrUp) return []
+
const options = [
{
text: this.$strings.ButtonQuickMatch,
action: 'quick-match'
}
]
+
if (!this.isPodcastLibrary && this.selectedMediaItemsArePlayable) {
options.push({
text: this.$strings.ButtonQuickEmbedMetadata,
action: 'quick-embed'
})
}
+
options.push({
text: this.$strings.ButtonReScan,
action: 'rescan'
})
+
// The limit of 50 is introduced because of the URL length. Each id has 36 chars, so 36 * 40 = 1440
// + 40 , separators = 1480 chars + base path 280 chars = 1760 chars. This keeps the URL under 2000 chars even with longer domains
if (this.selectedMediaItems.length <= 40) {
@@ -203,6 +188,7 @@ export default {
action: 'download'
})
}
+
return options
}
},
@@ -273,6 +259,7 @@ export default {
},
async playSelectedItems() {
this.$store.commit('setProcessingBatch', true)
+
const libraryItemIds = this.selectedMediaItems.map((i) => i.id)
const libraryItems = await this.$axios
.$post(`/api/items/batch/get`, { libraryItemIds })
@@ -321,7 +308,6 @@ export default {
toggleBatchRead() {
this.$store.commit('setProcessingBatch', true)
const newIsFinished = !this.selectedIsFinished
-
const updateProgressPayloads = this.selectedMediaItems.map((item) => {
return {
libraryItemId: item.id,
@@ -353,7 +339,9 @@ export default {
callback: (confirmed, hardDelete) => {
if (confirmed) {
localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1)
+
this.$store.commit('setProcessingBatch', true)
+
this.$axios
.$post(`/api/items/batch/delete?hard=${hardDelete ? 1 : 0}`, {
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue
index 47c6b7ae6..1d8f0f20b 100644
--- a/client/pages/item/_id/index.vue
+++ b/client/pages/item/_id/index.vue
@@ -21,7 +21,6 @@
-
@@ -42,8 +41,7 @@
{{ $getString('LabelByAuthor', [podcastAuthor]) }}
- {{ $getString('LabelByAuthor', ['']) }}
- {{ author.name }},
+ {{ $getString('LabelByAuthor', ['']) }}{{ author.name }},
by Unknown
@@ -56,6 +54,7 @@
{{ $getString('MessageEpisodesQueuedForDownload', [episodeDownloadsQueued.length]) }}
+
close
@@ -71,11 +70,9 @@
{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%
-
{{ $strings.LabelFinished }} {{ fmtDate(userProgressFinishedAt) }}
-
- {{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}
-
-
{{ $strings.LabelStarted }} {{ fmtDate(userProgressStartedAt) }}
+
{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt, dateFormat) }}
+
{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}
+
{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}
@@ -99,12 +96,6 @@
{{ $strings.ButtonRead }}
-
-
- sell
- Recommend
-
-
@@ -133,70 +124,25 @@
-
+
+
-
-
+
+
+
+
-
-
-
-
-
-
-
Recommend this book
-
-
-
-
-
-
-
-
-
-
If left empty, this will be a public recommendation.
-
-
-
-
-
-
{{ recForm.note.length }}/1000
-
-
-
-
-
- Cancel
-
- hourglass_empty
- Submit
-
-
-
-
@@ -207,7 +153,8 @@ export default {
return redirect(`/login?redirect=${route.path}`)
}
- const item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=downloads,rssfeed,share`).catch((error) => {
+ // Include episode downloads for podcasts
+ var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=downloads,rssfeed,share`).catch((error) => {
console.error('Failed', error)
return false
})
@@ -235,18 +182,7 @@ export default {
episodeDownloadsQueued: [],
showBookmarksModal: false,
isDescriptionClamped: false,
- showFullDescription: false,
-
- // Recommend modal state
- showRecModal: false,
- postingRec: false,
- recTags: [],
- recForm: {
- tagId: '',
- recipientUserId: '',
- note: '',
- visibility: 'public'
- }
+ showFullDescription: false
}
},
computed: {
@@ -351,7 +287,10 @@ export default {
return this.series.map((se) => {
let text = se.name
if (se.sequence) text += ` #${se.sequence}`
- return { ...se, text }
+ return {
+ ...se,
+ text
+ }
})
},
duration() {
@@ -414,7 +353,9 @@ export default {
return this.$store.getters['user/getUserCanDownload']
},
showRssFeedBtn() {
- if (!this.rssFeed && !this.podcastEpisodes.length && !this.tracks.length) return false
+ if (!this.rssFeed && !this.podcastEpisodes.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
},
showQueueBtn() {
@@ -426,86 +367,86 @@ export default {
},
contextMenuItems() {
const items = []
- if (this.showCollectionsButton) items.push({ text: this.$strings.LabelCollections, action: 'collections' })
- if (!this.isPodcast && this.tracks.length) items.push({ text: this.$strings.LabelYourPlaylists, action: 'playlists' })
- if (this.bookmarks.length) items.push({ text: this.$strings.LabelYourBookmarks, action: 'bookmarks' })
- if (this.showRssFeedBtn) items.push({ text: this.$strings.LabelOpenRSSFeed, action: 'rss-feeds' })
- if (this.userCanDownload) items.push({ text: this.$strings.LabelDownload, action: 'download' })
+
+ if (this.showCollectionsButton) {
+ items.push({
+ text: this.$strings.LabelCollections,
+ action: 'collections'
+ })
+ }
+
+ if (!this.isPodcast && this.tracks.length) {
+ items.push({
+ text: this.$strings.LabelYourPlaylists,
+ action: 'playlists'
+ })
+ }
+
+ if (this.bookmarks.length) {
+ items.push({
+ text: this.$strings.LabelYourBookmarks,
+ action: 'bookmarks'
+ })
+ }
+
+ if (this.showRssFeedBtn) {
+ items.push({
+ text: this.$strings.LabelOpenRSSFeed,
+ action: 'rss-feeds'
+ })
+ }
+
+ if (this.userCanDownload) {
+ items.push({
+ text: this.$strings.LabelDownload,
+ action: 'download'
+ })
+ }
+
if (this.ebookFile && this.$store.state.libraries.ereaderDevices?.length) {
items.push({
text: this.$strings.LabelSendEbookToDevice,
- subitems: this.$store.state.libraries.ereaderDevices.map((d) => ({ text: d.name, action: 'sendToDevice', data: d.name }))
+ subitems: this.$store.state.libraries.ereaderDevices.map((d) => {
+ return {
+ text: d.name,
+ action: 'sendToDevice',
+ data: d.name
+ }
+ })
})
}
- if (this.userIsAdminOrUp && !this.isPodcast && this.tracks.length) items.push({ text: this.$strings.LabelShare, action: 'share' })
- if (this.userCanDelete) items.push({ text: this.$strings.ButtonDelete, action: 'delete' })
+
+ if (this.userIsAdminOrUp && !this.isPodcast && this.tracks.length) {
+ items.push({
+ text: this.$strings.LabelShare,
+ action: 'share'
+ })
+ }
+
+ if (this.userCanDelete) {
+ items.push({
+ text: this.$strings.ButtonDelete,
+ action: 'delete'
+ })
+ }
+
return items
}
},
methods: {
- // ---- Safe date formatter ----
- fmtDate(value) {
- if (!value) return ''
- if (typeof value === 'number') {
- const ms = value < 1e12 ? value * 1000 : value
- return this.$formatDate(ms, this.dateFormat)
- }
- const ts = Date.parse(value)
- if (!Number.isNaN(ts)) return this.$formatDate(ts, this.dateFormat)
- return ''
- },
-
- // --- Recommendations helpers ---
- async openRecommendModal() {
- this.showRecModal = true
- if (!this.recTags.length) {
- try {
- this.recTags = await this.$axios.$get('/api/recommendations/tags')
- } catch (e) {
- console.error('Fetch tags failed', e)
- this.$toast?.error?.('Failed to load tags')
- }
- }
- },
- closeRecommendModal() {
- if (this.postingRec) return
- this.showRecModal = false
- this.recForm = { tagId: '', recipientUserId: '', note: '', visibility: 'public' }
- },
- async submitRecommendation() {
- if (!this.recForm.tagId) return
- this.postingRec = true
- try {
- const payload = {
- bookId: this.libraryItemId,
- tagId: this.recForm.tagId,
- note: this.recForm.note || undefined,
- visibility: this.recForm.visibility,
- recipientUserId: this.recForm.recipientUserId || undefined
- }
- await this.$axios.$post('/api/recommendations', payload)
- this.$toast?.success?.('Recommendation saved')
- this.closeRecommendModal()
- } catch (e) {
- const msg = e?.response?.data?.message || 'Failed to save recommendation'
- this.$toast?.error?.(msg)
- console.error('Create recommendation failed', e)
- } finally {
- this.postingRec = false
- }
- },
-
- // --- existing methods below ---
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.playItem(bookmark.time)
+ if (confirmed) {
+ this.playItem(bookmark.time)
+ }
},
type: 'yesNo'
}
@@ -534,7 +475,7 @@ export default {
return this.$toast.error(this.$strings.ToastNoRSSFeed)
}
this.fetchingRSSFeed = true
- const payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => {
+ var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => {
console.error('Failed to get feed', error)
this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
return null
@@ -542,6 +483,7 @@ export default {
this.fetchingRSSFeed = false
if (!payload) return
+ console.log('Podcast feed', payload)
const podcastfeed = payload.podcast
if (!podcastfeed.episodes || !podcastfeed.episodes.length) {
this.$toast.info(this.$strings.ToastPodcastNoEpisodesInFeed)
@@ -562,8 +504,10 @@ export default {
if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) {
const payload = {
message: this.$getString('MessageConfirmMarkItemFinished', [this.title]),
- callback: (c) => {
- if (c) this.toggleFinished(true)
+ callback: (confirmed) => {
+ if (confirmed) {
+ this.toggleFinished(true)
+ }
},
type: 'yesNo'
}
@@ -571,7 +515,9 @@ export default {
return
}
- const updatePayload = { isFinished: !this.userIsFinished }
+ var updatePayload = {
+ isFinished: !this.userIsFinished
+ }
this.isProcessingReadUpdate = true
this.$axios
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
@@ -588,12 +534,15 @@ export default {
let episodeId = null
const queueItems = []
if (this.isPodcast) {
+ // Uses the sorting and filtering from the episode table component
const episodesInListeningOrder = this.$refs.episodesTable?.episodesList || []
+ // Find the first unplayed episode from the table
let episodeIndex = episodesInListeningOrder.findIndex((ep) => {
const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
return !podcastProgress || !podcastProgress.isFinished
})
+ // If all episodes are played, use the first episode
if (episodeIndex < 0) episodeIndex = 0
episodeId = episodesInListeningOrder[episodeIndex].id
@@ -608,14 +557,14 @@ export default {
episodeId: episode.id,
title: episode.title,
subtitle: this.title,
- caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.fmtDate(episode.publishedAt)]) : this.$strings.LabelUnknownPublishDate,
+ caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.audioFile.duration || null,
coverPath: this.libraryItem.media.coverPath || null
})
}
}
} else {
- queueItems.push({
+ const queueItem = {
libraryItemId: this.libraryItemId,
libraryId: this.libraryId,
episodeId: null,
@@ -624,7 +573,8 @@ export default {
caption: '',
duration: this.duration || null,
coverPath: this.media.coverPath || null
- })
+ }
+ queueItems.push(queueItem)
}
this.$eventBus.$emit('play-item', {
@@ -644,16 +594,20 @@ export default {
},
libraryItemUpdated(libraryItem) {
if (libraryItem.id === this.libraryItemId) {
+ console.log('Item was updated', libraryItem)
this.libraryItem = libraryItem
this.$nextTick(this.checkDescriptionClamped)
}
},
clearProgressClick() {
if (!this.userMediaProgress) return
+
const payload = {
message: this.$strings.MessageConfirmResetProgress,
callback: (confirmed) => {
- if (confirmed) this.clearProgress()
+ if (confirmed) {
+ this.clearProgress()
+ }
},
type: 'yesNo'
}
@@ -726,8 +680,11 @@ export default {
},
queueBtnClick() {
if (this.isQueued) {
+ // Remove from queue
this.$store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId })
} else {
+ // Add to queue
+
const queueItem = {
libraryItemId: this.libraryItemId,
libraryId: this.libraryId,
@@ -754,6 +711,7 @@ export default {
callback: (confirmed, hardDelete) => {
if (confirmed) {
localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1)
+
this.$axios
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
.then(() => {
@@ -775,7 +733,10 @@ export default {
message: this.$getString('MessageConfirmSendEbookToDevice', [this.ebookFile.ebookFormat, this.title, deviceName]),
callback: (confirmed) => {
if (confirmed) {
- const payload = { libraryItemId: this.libraryItemId, deviceName }
+ const payload = {
+ libraryItemId: this.libraryItemId,
+ deviceName
+ }
this.processing = true
this.$axios
.$post(`/api/emails/send-ebook-to-device`, payload)
diff --git a/client/pages/me/recommendations/inbox.vue b/client/pages/me/recommendations/inbox.vue
deleted file mode 100644
index 0c19f4894..000000000
--- a/client/pages/me/recommendations/inbox.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
Recommendations · Inbox
-
-
Loading…
-
You have no incoming recommendations yet.
-
-
-
-
-
-
- from {{ r.recommender?.username || 'Someone' }}
- • {{ fmt(r.createdAt) }}
-
-
-
-
- Go to
-
-
-
-
-
- {{ r.tag?.label || 'Recommended' }}
-
-
- {{ r.bookTitle || `Book #${r.bookId}` }}
- {{ r.note }}
-
-
-
-
-
-
-
-
diff --git a/client/pages/me/recommendations/sent.vue b/client/pages/me/recommendations/sent.vue
deleted file mode 100644
index 2bf5c9cf2..000000000
--- a/client/pages/me/recommendations/sent.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
Recommendations · Sent
-
-
Loading…
-
You haven’t sent any recommendations yet.
-
-
-
-
-
- to {{ r.recipient?.username || 'Public' }}
- • {{ fmt(r.createdAt) }}
- {{ r.visibility }}
-
-
-
-
-
-
- {{ r.tag?.label || 'Recommended' }}
-
-
- {{ r.bookTitle || `Book #${r.bookId}` }}
- {{ r.note }}
-
-
-
-
-
-
-
-