diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index 879d50a3..92599c7a 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -186,7 +186,7 @@ export default { methods: { requestBatchQuickEmbed() { const payload = { - message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

Would you like to continue?', + message: this.$strings.MessageConfirmQuickEmbed, callback: (confirmed) => { if (confirmed) { this.$axios @@ -219,7 +219,7 @@ export default { }, async batchRescan() { const payload = { - message: `Are you sure you want to re-scan ${this.selectedMediaItems.length} items?`, + message: this.$getString('MessageConfirmReScanLibraryItems', [this.selectedMediaItems.length]), callback: (confirmed) => { if (confirmed) { this.$axios @@ -316,8 +316,8 @@ export default { }, batchDeleteClick() { const payload = { - message: `This will delete ${this.numMediaItemsSelected} library items from the database and your file system. Are you sure?`, - checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + message: this.$getString('MessageConfirmDeleteLibraryItems', [this.numMediaItemsSelected]), + checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox, yesButtonText: this.$strings.ButtonDelete, yesButtonColor: 'error', checkboxDefaultValue: true, diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 677aba70..c2db0725 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -14,10 +14,10 @@
-
-

v{{ $config.version }}

+
+ -

{{ Source }}

+

{{ Source }}

Latest: {{ latestVersion }}
diff --git a/client/components/app/SettingsContent.vue b/client/components/app/SettingsContent.vue index 233839e7..c78873e3 100644 --- a/client/components/app/SettingsContent.vue +++ b/client/components/app/SettingsContent.vue @@ -3,9 +3,7 @@

{{ headerText }}

-
- -
+

@@ -19,14 +17,9 @@ export default { props: { headerText: String, description: String, - note: String, - showAddButton: Boolean + note: String }, - methods: { - clicked() { - this.$emit('clicked') - } - } + methods: {} } diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index 995f4c23..deb96a6c 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -3,117 +3,119 @@

- - - - +
+ + + + -

{{ $strings.ButtonHome }}

+

{{ $strings.ButtonHome }}

-
- +
+ - - format_list_bulleted + + format_list_bulleted -

{{ $strings.ButtonLatest }}

+

{{ $strings.ButtonLatest }}

-
- +
+ - - - - + + + + -

{{ $strings.ButtonLibrary }}

+

{{ $strings.ButtonLibrary }}

-
- +
+ - - - - + + + + -

{{ $strings.ButtonSeries }}

+

{{ $strings.ButtonSeries }}

-
- +
+ - - collections_bookmark + + collections_bookmark -

{{ $strings.ButtonCollections }}

+

{{ $strings.ButtonCollections }}

-
- +
+ - - queue_music + + queue_music -

{{ $strings.ButtonPlaylists }}

+

{{ $strings.ButtonPlaylists }}

-
- +
+ - - - - + + + + -

{{ $strings.ButtonAuthors }}

+

{{ $strings.ButtonAuthors }}

-
- +
+ - - record_voice_over + + record_voice_over -

{{ $strings.LabelNarrators }}

+

{{ $strings.LabelNarrators }}

-
- +
+ - - + + -

{{ $strings.ButtonSearch }}

+

{{ $strings.ButtonSearch }}

-
- +
+ - - album + + album -

Albums

+

Albums

-
- +
+ - - file_download + + file_download -

{{ $strings.ButtonDownloadQueue }}

+

{{ $strings.ButtonDownloadQueue }}

-
- +
+ - - warning + + warning -

{{ $strings.ButtonIssues }}

+

{{ $strings.ButtonIssues }}

-
-
-

{{ numIssues }}

-
- +
+
+

{{ numIssues }}

+
+ +
-
+

v{{ $config.version }}

Update

{{ Source }}

@@ -235,3 +237,12 @@ export default { mounted() {} } + + \ No newline at end of file diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index d40ce0da..e9b6969d 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -1,9 +1,9 @@ @@ -53,9 +58,9 @@ export default { authorCopy: { name: '', asin: '', - description: '', - imagePath: '' + description: '' }, + imageUrl: '', processing: false } }, @@ -100,10 +105,10 @@ export default { }, methods: { init() { + this.imageUrl = '' this.authorCopy.name = this.author.name this.authorCopy.asin = this.author.asin this.authorCopy.description = this.author.description - this.authorCopy.imagePath = this.author.imagePath }, removeClick() { const payload = { @@ -131,7 +136,7 @@ export default { this.$store.commit('globals/setConfirmPrompt', payload) }, async submitForm() { - var keysToCheck = ['name', 'asin', 'description', 'imagePath'] + var keysToCheck = ['name', 'asin', 'description'] var updatePayload = {} keysToCheck.forEach((key) => { if (this.authorCopy[key] !== this.author[key]) { @@ -160,21 +165,46 @@ export default { } this.processing = false }, - async removeCover() { - var updatePayload = { - imagePath: null - } + removeCover() { this.processing = true - var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => { - console.error('Failed', error) - this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed) - return null - }) - if (result && result.updated) { - this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess) - this.$store.commit('globals/showEditAuthorModal', result.author) + this.$axios + .$delete(`/api/authors/${this.authorId}/image`) + .then((data) => { + this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess) + this.$store.commit('globals/showEditAuthorModal', data.author) + }) + .catch((error) => { + console.error('Failed', error) + this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed) + }) + .finally(() => { + this.processing = false + }) + }, + submitUploadCover() { + if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) { + this.$toast.error('Invalid image url') + return } - this.processing = false + + this.processing = true + const updatePayload = { + url: this.imageUrl + } + this.$axios + .$post(`/api/authors/${this.authorId}/image`, updatePayload) + .then((data) => { + this.imageUrl = '' + this.$toast.success('Author image updated') + this.$store.commit('globals/showEditAuthorModal', data.author) + }) + .catch((error) => { + console.error('Failed', error) + this.$toast.error(error.response.data || 'Failed to remove author image') + }) + .finally(() => { + this.processing = false + }) }, async searchAuthor() { if (!this.authorCopy.name && !this.authorCopy.asin) { diff --git a/client/components/modals/emails/EReaderDeviceModal.vue b/client/components/modals/emails/EReaderDeviceModal.vue index 4b6e87cf..79d80f7c 100644 --- a/client/components/modals/emails/EReaderDeviceModal.vue +++ b/client/components/modals/emails/EReaderDeviceModal.vue @@ -8,7 +8,7 @@
-
+
@@ -16,6 +16,14 @@
+
+
+ +
+
+ +
+
@@ -45,8 +53,11 @@ export default { processing: false, newDevice: { name: '', - email: '' - } + email: '', + availabilityOption: 'adminAndUp', + users: [] + }, + users: [] } }, watch: { @@ -68,10 +79,55 @@ export default { } }, title() { - return this.ereaderDevice ? 'Create Device' : 'Update Device' + return !this.ereaderDevice ? 'Create Device' : 'Update Device' + }, + userAvailabilityOptions() { + return [ + { + text: this.$strings.LabelAdminUsersOnly, + value: 'adminOrUp' + }, + { + text: this.$strings.LabelAllUsersExcludingGuests, + value: 'userOrUp' + }, + { + text: this.$strings.LabelAllUsersIncludingGuests, + value: 'guestOrUp' + }, + { + text: this.$strings.LabelSelectUsers, + value: 'specificUsers' + } + ] + }, + userOptions() { + return this.users.map((u) => ({ text: u.username, value: u.id })) } }, methods: { + availabilityOptionChanged(option) { + if (option === 'specificUsers' && !this.users.length) { + this.loadUsers() + } + }, + async loadUsers() { + this.processing = true + this.users = await this.$axios + .$get('/api/users') + .then((res) => { + return res.users.sort((a, b) => { + return a.createdAt - b.createdAt + }) + }) + .catch((error) => { + console.error('Failed', error) + return [] + }) + .finally(() => { + this.processing = false + }) + }, submitForm() { this.$refs.ereaderNameInput.blur() this.$refs.ereaderEmailInput.blur() @@ -81,19 +137,27 @@ export default { return } + if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) { + this.$toast.error('Must select at least one user') + return + } + if (this.newDevice.availabilityOption !== 'specificUsers') { + this.newDevice.users = [] + } + this.newDevice.name = this.newDevice.name.trim() this.newDevice.email = this.newDevice.email.trim() if (!this.ereaderDevice) { if (this.existingDevices.some((d) => d.name === this.newDevice.name)) { - this.$toast.error('EReader device with that name already exists') + this.$toast.error('Ereader device with that name already exists') return } this.submitCreate() } else { if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) { - this.$toast.error('EReader device with that name already exists') + this.$toast.error('Ereader device with that name already exists') return } @@ -160,9 +224,17 @@ export default { if (this.ereaderDevice) { this.newDevice.name = this.ereaderDevice.name this.newDevice.email = this.ereaderDevice.email + this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp' + this.newDevice.users = this.ereaderDevice.users || [] + + if (this.newDevice.availabilityOption === 'specificUsers' && !this.users.length) { + this.loadUsers() + } } else { this.newDevice.name = '' this.newDevice.email = '' + this.newDevice.availabilityOption = 'adminOrUp' + this.newDevice.users = [] } } }, diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue index 09329482..056fdcb3 100644 --- a/client/components/modals/item/tabs/Cover.vue +++ b/client/components/modals/item/tabs/Cover.vue @@ -7,7 +7,7 @@
-
+
delete @@ -16,15 +16,16 @@
-
+
upload
+ - - {{ $strings.ButtonSave }} + + {{ $strings.ButtonSubmit }}
@@ -64,7 +65,7 @@

{{ $strings.MessageNoCoversFound }}

@@ -165,6 +166,9 @@ export default { userCanUpload() { return this.$store.getters['user/getUserCanUpload'] }, + userCanDelete() { + return this.$store.getters['user/getUserCanDelete'] + }, userToken() { return this.$store.getters['user/getToken'] }, @@ -222,71 +226,53 @@ export default { this.coversFound = [] this.hasSearched = false } - this.imageUrl = this.media.coverPath || '' + this.imageUrl = '' this.searchTitle = this.mediaMetadata.title || '' this.searchAuthor = this.mediaMetadata.authorName || '' if (this.isPodcast) this.provider = 'itunes' else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google' }, removeCover() { - if (!this.media.coverPath) { - this.imageUrl = '' + if (!this.coverPath) { return } - this.updateCover('') + this.isProcessing = true + this.$axios + .$delete(`/api/items/${this.libraryItemId}/cover`) + .then(() => {}) + .catch((error) => { + console.error('Failed to remove cover', error) + if (error.response?.data) { + this.$toast.error(error.response.data) + } + }) + .finally(() => { + this.isProcessing = false + }) }, submitForm() { this.updateCover(this.imageUrl) }, async updateCover(cover) { - if (cover === this.coverPath) { - console.warn('Cover has not changed..', cover) + if (!cover.startsWith('http:') && !cover.startsWith('https:')) { + this.$toast.error('Invalid URL') return } this.isProcessing = true - var success = false - - if (!cover) { - // Remove cover - success = await this.$axios - .$delete(`/api/items/${this.libraryItemId}/cover`) - .then(() => true) - .catch((error) => { - console.error('Failed to remove cover', error) - if (error.response && error.response.data) { - this.$toast.error(error.response.data) - } - return false - }) - } else if (cover.startsWith('http:') || cover.startsWith('https:')) { - // Download cover from url and use - success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }).catch((error) => { - console.error('Failed to download cover from url', error) - if (error.response && error.response.data) { - this.$toast.error(error.response.data) - } - return false + this.$axios + .$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }) + .then(() => { + this.imageUrl = '' + this.$toast.success('Update Successful') }) - } else { - // Update local cover url - const updatePayload = { - cover - } - success = await this.$axios.$patch(`/api/items/${this.libraryItemId}/cover`, updatePayload).catch((error) => { - console.error('Failed to update', error) - if (error.response && error.response.data) { - this.$toast.error(error.response.data) - } - return false + .catch((error) => { + console.error('Failed to update cover', error) + this.$toast.error(error.response?.data || 'Failed to update cover') + }) + .finally(() => { + this.isProcessing = false }) - } - if (success) { - this.$toast.success('Update Successful') - } else if (this.media.coverPath) { - this.imageUrl = this.media.coverPath - } - this.isProcessing = false }, getSearchQuery() { var searchQuery = `provider=${this.provider}&title=${this.searchTitle}` @@ -319,7 +305,19 @@ export default { this.hasSearched = true }, setCover(coverFile) { - this.updateCover(coverFile.metadata.path) + this.isProcessing = true + this.$axios + .$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path }) + .then(() => { + this.$toast.success('Update Successful') + }) + .catch((error) => { + console.error('Failed to set local cover', error) + this.$toast.error(error.response?.data || 'Failed to set cover') + }) + .finally(() => { + this.isProcessing = false + }) } } } diff --git a/client/components/modals/item/tabs/Details.vue b/client/components/modals/item/tabs/Details.vue index 14fe68a7..62f08c92 100644 --- a/client/components/modals/item/tabs/Details.vue +++ b/client/components/modals/item/tabs/Details.vue @@ -11,8 +11,8 @@ {{ $strings.ButtonQuickMatch }} - - {{ $strings.ButtonReScan }} + + {{ $strings.ButtonReScan }}
@@ -80,9 +80,9 @@ export default { libraryProvider() { return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google' }, - libraryScan() { + isLibraryScanning() { if (!this.libraryId) return null - return this.$store.getters['scanners/getLibraryScan'](this.libraryId) + return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.libraryId) } }, methods: { diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 0c5d67eb..1c682919 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -22,7 +22,7 @@
@@ -205,7 +205,7 @@ export default { processing: Boolean, libraryItem: { type: Object, - default: () => { } + default: () => {} } }, data() { @@ -290,13 +290,17 @@ export default { return this.$strings.LabelSearchTitle }, media() { - return this.libraryItem ? this.libraryItem.media || {} : {} + return this.libraryItem?.media || {} }, mediaMetadata() { return this.media.metadata || {} }, + currentBookDuration() { + if (this.isPodcast) return 0 + return this.media.duration || 0 + }, mediaType() { - return this.libraryItem ? this.libraryItem.mediaType : null + return this.libraryItem?.mediaType || null }, isPodcast() { return this.mediaType == 'podcast' diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 633b7646..5bcdabed 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -1,5 +1,5 @@