mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Translation strings added (#3304)
* Update: `pages/items/_id` toast messages * Update: account modal strings * Update: audio file data modal strings * Update: sleep timer set string * Update: loading indicator string * Update: lazy book card strings * Reorder keys * Fix: syntax error in LazyBookCard * Fix: json ordering * Fix: fix double message definition * Update: login form toast strings * Update: batch delete toast * Update: collection add toast messages * Replace: toasts in BookShelfToolbar * Update: playlist edit toasts * Update: Details tab * Add: title required string * Update: ereader toasts * Update: author toasts, title and name required toasts * Clean up "no updates" strings * Change: slug strings * Update: cover modal toasts * Change: cancel encode toasts * Change: failed to share toasts * Simplify: "renameFail" and "removeFail" toasts * Fix: ordering * Change: chapters remove toast * Update: notification strings * Revert: loading indicator (error in browser) * Update: collectionBooksTable toast * Update: "failed to get" strings * Update: backup strings * Update: custom provider strings * Update: sessions strings * Update: email strings * Update sort display translation strings, update podcast episode queue strings to use translation * Fix loading indicator please wait translation * Consolidate translations and reduce number of toasts --------- Co-authored-by: advplyr <advplyr@protonmail.com>
This commit is contained in:
		
							parent
							
								
									acc4bdbc5e
								
							
						
					
					
						commit
						f0e70ed27b
					
				@ -332,13 +332,13 @@ export default {
 | 
			
		||||
                libraryItemIds: this.selectedMediaItems.map((i) => i.id)
 | 
			
		||||
              })
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                this.$toast.success('Batch delete success')
 | 
			
		||||
                this.$toast.success(this.$strings.ToastBatchDeleteSuccess)
 | 
			
		||||
                this.$store.commit('globals/resetSelectedMediaItems', [])
 | 
			
		||||
                this.$eventBus.$emit('bookshelf_clear_selection')
 | 
			
		||||
              })
 | 
			
		||||
              .catch((error) => {
 | 
			
		||||
                console.error('Batch delete failed', error)
 | 
			
		||||
                this.$toast.error('Batch delete failed')
 | 
			
		||||
                this.$toast.error(this.$strings.ToastBatchDeleteFailed)
 | 
			
		||||
              })
 | 
			
		||||
              .finally(() => {
 | 
			
		||||
                this.$store.commit('setProcessingBatch', false)
 | 
			
		||||
 | 
			
		||||
@ -473,11 +473,11 @@ export default {
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.$toast.success('Series re-added to continue listening')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastItemUpdateSuccess)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to re-add series to continue listening', error)
 | 
			
		||||
          this.$toast.error('Failed to re-add series to continue listening')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastItemUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processingSeries = false
 | 
			
		||||
@ -504,7 +504,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        if (!response) {
 | 
			
		||||
          console.error(`Author ${author.name} not found`)
 | 
			
		||||
          this.$toast.error(`Author ${author.name} not found`)
 | 
			
		||||
          this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
 | 
			
		||||
        } else if (response.updated) {
 | 
			
		||||
          if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
 | 
			
		||||
          else console.log(`Author ${response.author.name} was updated (no image found)`)
 | 
			
		||||
@ -522,13 +522,13 @@ export default {
 | 
			
		||||
        this.$axios
 | 
			
		||||
          .$delete(`/api/libraries/${this.currentLibraryId}/issues`)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            this.$toast.success('Removed library items with issues')
 | 
			
		||||
            this.$toast.success(this.$strings.ToastRemoveItemsWithIssuesSuccess)
 | 
			
		||||
            this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
 | 
			
		||||
            this.$store.dispatch('libraries/fetch', this.currentLibraryId)
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Failed to remove library items with issues', error)
 | 
			
		||||
            this.$toast.error('Failed to remove library items with issues')
 | 
			
		||||
            this.$toast.error(this.$strings.ToastRemoveItemsWithIssuesFailed)
 | 
			
		||||
          })
 | 
			
		||||
          .finally(() => {
 | 
			
		||||
            this.processingIssues = false
 | 
			
		||||
 | 
			
		||||
@ -346,14 +346,14 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    displaySortLine() {
 | 
			
		||||
      if (this.collapsedSeries) return null
 | 
			
		||||
      if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
 | 
			
		||||
      if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
 | 
			
		||||
      if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
 | 
			
		||||
      if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
 | 
			
		||||
      if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
 | 
			
		||||
      if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
 | 
			
		||||
      if (this.orderBy === 'mtimeMs') return this.$getString('LabelFileModifiedDate', [this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)])
 | 
			
		||||
      if (this.orderBy === 'birthtimeMs') return this.$getString('LabelFileBornDate', [this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)])
 | 
			
		||||
      if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt, this.dateFormat)])
 | 
			
		||||
      if (this.orderBy === 'media.duration') return this.$strings.LabelDuration + ': ' + this.$elapsedPrettyExtended(this.media.duration, false)
 | 
			
		||||
      if (this.orderBy === 'size') return this.$strings.LabelSize + ': ' + this.$bytesPretty(this._libraryItem.size)
 | 
			
		||||
      if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} ` + this.$strings.LabelEpisodes
 | 
			
		||||
      if (this.orderBy === 'media.metadata.publishedYear') {
 | 
			
		||||
        if (this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
 | 
			
		||||
        if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
 | 
			
		||||
        return '\u00A0'
 | 
			
		||||
      }
 | 
			
		||||
      return null
 | 
			
		||||
@ -710,7 +710,7 @@ export default {
 | 
			
		||||
    toggleFinished(confirmed = false) {
 | 
			
		||||
      if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
 | 
			
		||||
        const payload = {
 | 
			
		||||
          message: `Are you sure you want to mark "${this.displayTitle}" as finished?`,
 | 
			
		||||
          message: this.$getString('MessageConfirmMarkItemFinished', [this.displayTitle]),
 | 
			
		||||
          callback: (confirmed) => {
 | 
			
		||||
            if (confirmed) {
 | 
			
		||||
              this.toggleFinished(true)
 | 
			
		||||
@ -755,18 +755,18 @@ export default {
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          var result = data.result
 | 
			
		||||
          if (!result) {
 | 
			
		||||
            this.$toast.error(`Re-Scan Failed for "${this.title}"`)
 | 
			
		||||
            this.$toast.error(this.$getString('ToastRescanFailed', [this.displayTitle]))
 | 
			
		||||
          } else if (result === 'UPDATED') {
 | 
			
		||||
            this.$toast.success(`Re-Scan complete item was updated`)
 | 
			
		||||
            this.$toast.success(this.$strings.ToastRescanUpdated)
 | 
			
		||||
          } else if (result === 'UPTODATE') {
 | 
			
		||||
            this.$toast.success(`Re-Scan complete item was up to date`)
 | 
			
		||||
            this.$toast.success(this.$strings.ToastRescanUpToDate)
 | 
			
		||||
          } else if (result === 'REMOVED') {
 | 
			
		||||
            this.$toast.error(`Re-Scan complete item was removed`)
 | 
			
		||||
            this.$toast.error(this.$strings.ToastRescanRemoved)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to scan library item', error)
 | 
			
		||||
          this.$toast.error('Failed to scan library item')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastScanFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
@ -823,7 +823,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to remove series from home', error)
 | 
			
		||||
          this.$toast.error('Failed to update user')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastFailedToUpdateUser)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
@ -841,7 +841,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to hide item from home', error)
 | 
			
		||||
          this.$toast.error('Failed to update user')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastFailedToUpdateUser)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
@ -856,7 +856,7 @@ export default {
 | 
			
		||||
          episodeId: this.recentEpisode.id,
 | 
			
		||||
          title: this.recentEpisode.title,
 | 
			
		||||
          subtitle: this.mediaMetadata.title,
 | 
			
		||||
          caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
 | 
			
		||||
          caption: this.recentEpisode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
 | 
			
		||||
          duration: this.recentEpisode.audioFile.duration || null,
 | 
			
		||||
          coverPath: this.media.coverPath || null
 | 
			
		||||
        }
 | 
			
		||||
@ -906,11 +906,11 @@ export default {
 | 
			
		||||
            axios
 | 
			
		||||
              .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                this.$toast.success('Item deleted')
 | 
			
		||||
                this.$toast.success(this.$strings.ToastItemDeletedSuccess)
 | 
			
		||||
              })
 | 
			
		||||
              .catch((error) => {
 | 
			
		||||
                console.error('Failed to delete item', error)
 | 
			
		||||
                this.$toast.error('Failed to delete item')
 | 
			
		||||
                this.$toast.error(this.$strings.ToastItemDeletedFailed)
 | 
			
		||||
              })
 | 
			
		||||
              .finally(() => {
 | 
			
		||||
                this.processing = false
 | 
			
		||||
@ -1016,7 +1016,7 @@ export default {
 | 
			
		||||
                  episodeId: episode.id,
 | 
			
		||||
                  title: episode.title,
 | 
			
		||||
                  subtitle: this.mediaMetadata.title,
 | 
			
		||||
                  caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
 | 
			
		||||
                  caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
 | 
			
		||||
                  duration: episode.audioFile.duration || null,
 | 
			
		||||
                  coverPath: this.media.coverPath || null
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,7 @@ export default {
 | 
			
		||||
    displaySortLine() {
 | 
			
		||||
      switch (this.orderBy) {
 | 
			
		||||
        case 'addedAt':
 | 
			
		||||
          return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}`
 | 
			
		||||
          return this.$getString('LabelAddedDate', [this.$formatDate(this.addedAt, this.dateFormat)])
 | 
			
		||||
        case 'totalDuration':
 | 
			
		||||
          return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
 | 
			
		||||
        case 'lastBookUpdated':
 | 
			
		||||
 | 
			
		||||
@ -4,11 +4,11 @@
 | 
			
		||||
      <p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
 | 
			
		||||
      <div class="flex-grow" />
 | 
			
		||||
 | 
			
		||||
      <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">Fire onTest Event</ui-btn>
 | 
			
		||||
      <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">Fire & Fail</ui-btn>
 | 
			
		||||
      <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">{{ this.$strings.ButtonFireOnTest }}</ui-btn>
 | 
			
		||||
      <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">{{ this.$strings.ButtonFireAndFail }}</ui-btn>
 | 
			
		||||
      <!-- <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="rapidFireTestEvents">Rapid Fire</ui-btn> -->
 | 
			
		||||
      <ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">Test</ui-btn>
 | 
			
		||||
      <ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">Enable</ui-btn>
 | 
			
		||||
      <ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">{{ this.$strings.ButtonTest }}</ui-btn>
 | 
			
		||||
      <ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">{{ this.$strings.ButtonEnable }}</ui-btn>
 | 
			
		||||
 | 
			
		||||
      <ui-icon-btn :size="7" icon-font-size="1.1rem" icon="edit" class="mr-2" @click="editNotification" />
 | 
			
		||||
      <ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
 | 
			
		||||
@ -65,12 +65,12 @@ export default {
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.$toast.success('Triggered onTest Event')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed', error)
 | 
			
		||||
          const errorMsg = error.response ? error.response.data : null
 | 
			
		||||
          this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger onTest event')
 | 
			
		||||
          this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.testing = false
 | 
			
		||||
@ -91,7 +91,7 @@ export default {
 | 
			
		||||
    // End testing functions
 | 
			
		||||
    sendTestClick() {
 | 
			
		||||
      const payload = {
 | 
			
		||||
        message: `Trigger this notification with test data?`,
 | 
			
		||||
        message: this.$strings.MessageConfirmNotificationTestTrigger,
 | 
			
		||||
        callback: (confirmed) => {
 | 
			
		||||
          if (confirmed) {
 | 
			
		||||
            this.sendTest()
 | 
			
		||||
@ -106,12 +106,12 @@ export default {
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .$get(`/api/notifications/${this.notification.id}/test`)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.$toast.success('Triggered test notification')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed', error)
 | 
			
		||||
          const errorMsg = error.response ? error.response.data : null
 | 
			
		||||
          this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger test notification')
 | 
			
		||||
          this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.sendingTest = false
 | 
			
		||||
@ -127,11 +127,10 @@ export default {
 | 
			
		||||
        .$patch(`/api/notifications/${this.notification.id}`, payload)
 | 
			
		||||
        .then((updatedSettings) => {
 | 
			
		||||
          this.$emit('update', updatedSettings)
 | 
			
		||||
          this.$toast.success('Notification enabled')
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to update notification', error)
 | 
			
		||||
          this.$toast.error('Failed to update notification')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.enabling = false
 | 
			
		||||
@ -139,7 +138,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    deleteNotificationClick() {
 | 
			
		||||
      const payload = {
 | 
			
		||||
        message: `Are you sure you want to delete this notification?`,
 | 
			
		||||
        message: this.$strings.MessageConfirmDeleteNotification,
 | 
			
		||||
        callback: (confirmed) => {
 | 
			
		||||
          if (confirmed) {
 | 
			
		||||
            this.deleteNotification()
 | 
			
		||||
@ -155,11 +154,10 @@ export default {
 | 
			
		||||
        .$delete(`/api/notifications/${this.notification.id}`)
 | 
			
		||||
        .then((updatedSettings) => {
 | 
			
		||||
          this.$emit('update', updatedSettings)
 | 
			
		||||
          this.$toast.success('Deleted notification')
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed', error)
 | 
			
		||||
          this.$toast.error('Failed to delete notification')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastNotificationDeleteFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.deleting = false
 | 
			
		||||
 | 
			
		||||
@ -111,7 +111,7 @@
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="flex pt-4 px-2">
 | 
			
		||||
            <ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">Unlink OpenID</ui-btn>
 | 
			
		||||
            <ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">{{ $strings.ButtonUnlinkOpenId }}</ui-btn>
 | 
			
		||||
            <ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
 | 
			
		||||
            <div class="flex-grow" />
 | 
			
		||||
            <ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
 | 
			
		||||
@ -212,19 +212,19 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    unlinkOpenID() {
 | 
			
		||||
      const payload = {
 | 
			
		||||
        message: 'Are you sure you want to unlink this user from OpenID?',
 | 
			
		||||
        message: this.$strings.MessageConfirmUnlinkOpenId,
 | 
			
		||||
        callback: (confirmed) => {
 | 
			
		||||
          if (confirmed) {
 | 
			
		||||
            this.unlinkingFromOpenID = true
 | 
			
		||||
            this.$axios
 | 
			
		||||
              .$patch(`/api/users/${this.account.id}/openid-unlink`)
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                this.$toast.success('User unlinked from OpenID')
 | 
			
		||||
                this.$toast.success(this.$strings.ToastUnlinkOpenIdSuccess)
 | 
			
		||||
                this.show = false
 | 
			
		||||
              })
 | 
			
		||||
              .catch((error) => {
 | 
			
		||||
                console.error('Failed to unlink user from OpenID', error)
 | 
			
		||||
                this.$toast.error('Failed to unlink user from OpenID')
 | 
			
		||||
                this.$toast.error(this.$strings.ToastUnlinkOpenIdFailed)
 | 
			
		||||
              })
 | 
			
		||||
              .finally(() => {
 | 
			
		||||
                this.unlinkingFromOpenID = false
 | 
			
		||||
@ -265,15 +265,15 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    submitForm() {
 | 
			
		||||
      if (!this.newUser.username) {
 | 
			
		||||
        this.$toast.error('Enter a username')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastNewUserUsernameError)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) {
 | 
			
		||||
        this.$toast.error('Must select at least one library')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastNewUserLibraryError)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) {
 | 
			
		||||
        this.$toast.error('Must select at least one tag')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastNewUserTagError)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -313,12 +313,12 @@ export default {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
          console.error('Failed to update account', error)
 | 
			
		||||
          var errMsg = error.response ? error.response.data || '' : ''
 | 
			
		||||
          this.$toast.error(errMsg || 'Failed to update account')
 | 
			
		||||
          this.$toast.error(errMsg || this.$strings.ToastFailedToUpdateAccount)
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
    submitCreateAccount() {
 | 
			
		||||
      if (!this.newUser.password) {
 | 
			
		||||
        this.$toast.error('Must have a password, only root user can have an empty password')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastNewUserPasswordError)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -329,9 +329,9 @@ export default {
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
          if (data.error) {
 | 
			
		||||
            this.$toast.error(`Failed to create account: ${data.error}`)
 | 
			
		||||
            this.$toast.error(this.$strings.ToastNewUserCreatedFailed + ': ' + data.error)
 | 
			
		||||
          } else {
 | 
			
		||||
            this.$toast.success('New account created')
 | 
			
		||||
            this.$toast.success(this.$strings.ToastNewUserCreatedSuccess)
 | 
			
		||||
            this.show = false
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
  <modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
 | 
			
		||||
    <template #outer>
 | 
			
		||||
      <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
 | 
			
		||||
        <p class="text-3xl text-white truncate">Add custom metadata provider</p>
 | 
			
		||||
        <p class="text-3xl text-white truncate">{{ $strings.HeaderAddCustomMetadataProvider }}</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <form @submit.prevent="submitForm">
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
            <ui-text-input-with-label v-model="newUrl" label="URL" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="w-full mb-2 p-1">
 | 
			
		||||
            <ui-text-input-with-label v-model="newAuthHeaderValue" :label="'Authorization Header Value'" type="password" />
 | 
			
		||||
            <ui-text-input-with-label v-model="newAuthHeaderValue" :label="$strings.LabelProviderAuthorizationValue" type="password" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="flex px-1 pt-4">
 | 
			
		||||
            <div class="flex-grow" />
 | 
			
		||||
@ -67,7 +67,7 @@ export default {
 | 
			
		||||
  methods: {
 | 
			
		||||
    submitForm() {
 | 
			
		||||
      if (!this.newName || !this.newUrl) {
 | 
			
		||||
        this.$toast.error('Must add name and url')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -81,13 +81,13 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.$emit('added', data.provider)
 | 
			
		||||
          this.$toast.success('New provider added')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastProviderCreatedSuccess)
 | 
			
		||||
          this.show = false
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          const errorMsg = error.response?.data || 'Unknown error'
 | 
			
		||||
          console.error('Failed to add provider', error)
 | 
			
		||||
          this.$toast.error('Failed to add provider: ' + errorMsg)
 | 
			
		||||
          this.$toast.error(this.$strings.ToastProviderCreatedFailed + ': ' + errorMsg)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
      <div class="flex items-center justify-between">
 | 
			
		||||
        <p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
 | 
			
		||||
        <ui-btn v-if="ffprobeData" small class="ml-2" @click="ffprobeData = null">{{ $strings.ButtonReset }}</ui-btn>
 | 
			
		||||
        <ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">Probe Audio File</ui-btn>
 | 
			
		||||
        <ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">{{ $strings.ButtonProbeAudioFile }}</ui-btn>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="w-full h-px bg-white bg-opacity-10 my-4" />
 | 
			
		||||
@ -159,7 +159,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to get ffprobe data', error)
 | 
			
		||||
          this.$toast.error('FFProbe failed')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastFailedToLoadData)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.probingFile = false
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
      <widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
 | 
			
		||||
 | 
			
		||||
      <div class="flex items-center justify-end">
 | 
			
		||||
        <ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdateNecessary }}</ui-btn>
 | 
			
		||||
        <ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </modals-modal>
 | 
			
		||||
 | 
			
		||||
@ -94,7 +94,7 @@ export default {
 | 
			
		||||
          this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
 | 
			
		||||
          this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
          console.error(error)
 | 
			
		||||
        })
 | 
			
		||||
      this.show = false
 | 
			
		||||
 | 
			
		||||
@ -100,7 +100,7 @@
 | 
			
		||||
 | 
			
		||||
      <div class="flex items-center">
 | 
			
		||||
        <ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
 | 
			
		||||
        <ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn>
 | 
			
		||||
        <ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">{{ $strings.ButtonCloseSession }}</ui-btn>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </modals-modal>
 | 
			
		||||
@ -206,14 +206,13 @@ export default {
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .$post(`/api/session/${this._session.id}/close`)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.$toast.success('Session closed')
 | 
			
		||||
          this.show = false
 | 
			
		||||
          this.$emit('closedSession')
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to close session', error)
 | 
			
		||||
          const errMsg = error.response?.data || ''
 | 
			
		||||
          this.$toast.error(errMsg || 'Failed to close open session')
 | 
			
		||||
          this.$toast.error(errMsg || this.$strings.ToastSessionCloseFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
 | 
			
		||||
@ -165,7 +165,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    openShare() {
 | 
			
		||||
      if (!this.newShareSlug) {
 | 
			
		||||
        this.$toast.error('Slug is required')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastSlugRequired)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const payload = {
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
        </template>
 | 
			
		||||
        <form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
 | 
			
		||||
          <ui-text-input v-model="customTime" type="number" step="any" min="0.1" :placeholder="$strings.LabelTimeInMinutes" class="w-48" />
 | 
			
		||||
          <ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
 | 
			
		||||
          <ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-18 flex items-center justify-center ml-1">{{ $strings.ButtonSubmit }}</ui-btn>
 | 
			
		||||
        </form>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-if="timerSet" class="w-full p-4">
 | 
			
		||||
 | 
			
		||||
@ -78,14 +78,13 @@ export default {
 | 
			
		||||
          if (data.error) {
 | 
			
		||||
            this.$toast.error(data.error)
 | 
			
		||||
          } else {
 | 
			
		||||
            this.$toast.success('Cover Uploaded')
 | 
			
		||||
            this.resetCoverPreview()
 | 
			
		||||
          }
 | 
			
		||||
          this.processingUpload = false
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed', error)
 | 
			
		||||
          var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error'
 | 
			
		||||
          var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
 | 
			
		||||
          this.$toast.error(errorMsg)
 | 
			
		||||
          this.processingUpload = false
 | 
			
		||||
        })
 | 
			
		||||
@ -95,7 +94,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
      var success = await this.$axios.$post(`/api/${this.entity}/${this.entityId}/cover`, { url: this.imageUrl }).catch((error) => {
 | 
			
		||||
        console.error('Failed to download cover from url', error)
 | 
			
		||||
        var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error'
 | 
			
		||||
        var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
 | 
			
		||||
        this.$toast.error(errorMsg)
 | 
			
		||||
        return false
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
@ -116,12 +116,12 @@ export default {
 | 
			
		||||
            this.$axios
 | 
			
		||||
              .$delete(`/api/authors/${this.authorId}`)
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                this.$toast.success('Author removed')
 | 
			
		||||
                this.$toast.success(this.$strings.ToastAuthorRemoveSuccess)
 | 
			
		||||
                this.show = false
 | 
			
		||||
              })
 | 
			
		||||
              .catch((error) => {
 | 
			
		||||
                console.error('Failed to remove author', error)
 | 
			
		||||
                this.$toast.error('Failed to remove author')
 | 
			
		||||
                this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
              })
 | 
			
		||||
              .finally(() => {
 | 
			
		||||
                this.processing = false
 | 
			
		||||
@ -141,7 +141,7 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      if (!Object.keys(updatePayload).length) {
 | 
			
		||||
        this.$toast.info(this.$strings.MessageNoUpdateNecessary)
 | 
			
		||||
        this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.processing = true
 | 
			
		||||
@ -158,7 +158,7 @@ export default {
 | 
			
		||||
        } else if (result.merged) {
 | 
			
		||||
          this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
 | 
			
		||||
          this.show = false
 | 
			
		||||
        } else this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
 | 
			
		||||
        } else this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
      }
 | 
			
		||||
      this.processing = false
 | 
			
		||||
    },
 | 
			
		||||
@ -174,7 +174,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed', error)
 | 
			
		||||
          this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
 | 
			
		||||
          this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
@ -182,7 +182,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    submitUploadCover() {
 | 
			
		||||
      if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
 | 
			
		||||
        this.$toast.error('Invalid image url')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastInvalidImageUrl)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -194,14 +194,14 @@ export default {
 | 
			
		||||
        .$post(`/api/authors/${this.authorId}/image`, updatePayload)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.imageUrl = ''
 | 
			
		||||
          this.$toast.success('Author image updated')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
 | 
			
		||||
 | 
			
		||||
          this.authorCopy.updatedAt = data.author.updatedAt
 | 
			
		||||
          this.authorCopy.imagePath = data.author.imagePath
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed', error)
 | 
			
		||||
          this.$toast.error(error.response.data || 'Failed to remove author image')
 | 
			
		||||
          this.$toast.error(error.response.data || this.$strings.ToastRemoveFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
@ -209,7 +209,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    async searchAuthor() {
 | 
			
		||||
      if (!this.authorCopy.name && !this.authorCopy.asin) {
 | 
			
		||||
        this.$toast.error('Must enter an author name')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastNameRequired)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.processing = true
 | 
			
		||||
@ -228,17 +228,19 @@ export default {
 | 
			
		||||
        return null
 | 
			
		||||
      })
 | 
			
		||||
      if (!response) {
 | 
			
		||||
        this.$toast.error('Author not found')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastAuthorSearchNotFound)
 | 
			
		||||
      } else if (response.updated) {
 | 
			
		||||
        if (response.author.imagePath) {
 | 
			
		||||
          this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
 | 
			
		||||
        } else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
 | 
			
		||||
        } else {
 | 
			
		||||
          this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.authorCopy = {
 | 
			
		||||
          ...response.author
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this.$toast.info('No updates were made for Author')
 | 
			
		||||
        this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
      }
 | 
			
		||||
      this.processing = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -143,7 +143,7 @@ export default {
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Failed to remove books from collection', error)
 | 
			
		||||
            this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
 | 
			
		||||
            this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
            this.processing = false
 | 
			
		||||
          })
 | 
			
		||||
      } else {
 | 
			
		||||
@ -157,7 +157,7 @@ export default {
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Failed to remove book from collection', error)
 | 
			
		||||
            this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
 | 
			
		||||
            this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
            this.processing = false
 | 
			
		||||
          })
 | 
			
		||||
      }
 | 
			
		||||
@ -172,12 +172,12 @@ export default {
 | 
			
		||||
          .$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
 | 
			
		||||
          .then((updatedCollection) => {
 | 
			
		||||
            console.log(`Books added to collection`, updatedCollection)
 | 
			
		||||
            this.$toast.success('Books added to collection')
 | 
			
		||||
            this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
 | 
			
		||||
            this.processing = false
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Failed to add books to collection', error)
 | 
			
		||||
            this.$toast.error('Failed to add books to collection')
 | 
			
		||||
            this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
 | 
			
		||||
            this.processing = false
 | 
			
		||||
          })
 | 
			
		||||
      } else {
 | 
			
		||||
@ -187,12 +187,12 @@ export default {
 | 
			
		||||
          .$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
 | 
			
		||||
          .then((updatedCollection) => {
 | 
			
		||||
            console.log(`Book added to collection`, updatedCollection)
 | 
			
		||||
            this.$toast.success('Book added to collection')
 | 
			
		||||
            this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
 | 
			
		||||
            this.processing = false
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Failed to add book to collection', error)
 | 
			
		||||
            this.$toast.error('Failed to add book to collection')
 | 
			
		||||
            this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
 | 
			
		||||
            this.processing = false
 | 
			
		||||
          })
 | 
			
		||||
      }
 | 
			
		||||
@ -221,7 +221,7 @@ export default {
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to create collection', error)
 | 
			
		||||
          var errMsg = error.response ? error.response.data || '' : ''
 | 
			
		||||
          this.$toast.error(`Failed to create collection: ${errMsg}`)
 | 
			
		||||
          this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg)
 | 
			
		||||
          this.processing = false
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -106,7 +106,7 @@ export default {
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Failed to remove collection', error)
 | 
			
		||||
            this.processing = false
 | 
			
		||||
            this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
 | 
			
		||||
            this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
          })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
@ -115,7 +115,7 @@ export default {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (!this.newCollectionName) {
 | 
			
		||||
        return this.$toast.error('Collection must have a name')
 | 
			
		||||
        return this.$toast.error(this.$strings.ToastNameRequired)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.processing = true
 | 
			
		||||
 | 
			
		||||
@ -125,12 +125,12 @@ export default {
 | 
			
		||||
      this.$refs.ereaderEmailInput.blur()
 | 
			
		||||
 | 
			
		||||
      if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
 | 
			
		||||
        this.$toast.error('Name and email required')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastNameEmailRequired)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) {
 | 
			
		||||
        this.$toast.error('Must select at least one user')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastSelectAtLeastOneUser)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (this.newDevice.availabilityOption !== 'specificUsers') {
 | 
			
		||||
@ -142,14 +142,14 @@ export default {
 | 
			
		||||
 | 
			
		||||
      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(this.$strings.ToastDeviceNameAlreadyExists)
 | 
			
		||||
          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(this.$strings.ToastDeviceNameAlreadyExists)
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -174,12 +174,11 @@ export default {
 | 
			
		||||
        .$post(`/api/emails/ereader-devices`, payload)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.$emit('update', data.ereaderDevices)
 | 
			
		||||
          this.$toast.success('Device updated')
 | 
			
		||||
          this.show = false
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to update device', error)
 | 
			
		||||
          this.$toast.error('Failed to update device')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastDeviceUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
@ -201,12 +200,11 @@ export default {
 | 
			
		||||
        .$post('/api/emails/ereader-devices', payload)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.$emit('update', data.ereaderDevices || [])
 | 
			
		||||
          this.$toast.success('Device added')
 | 
			
		||||
          this.show = false
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to add device', error)
 | 
			
		||||
          this.$toast.error('Failed to add device')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastDeviceAddFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
 | 
			
		||||
@ -194,7 +194,6 @@ export default {
 | 
			
		||||
          if (data.error) {
 | 
			
		||||
            this.$toast.error(data.error)
 | 
			
		||||
          } else {
 | 
			
		||||
            this.$toast.success('Cover Uploaded')
 | 
			
		||||
            this.resetCoverPreview()
 | 
			
		||||
          }
 | 
			
		||||
          this.processingUpload = false
 | 
			
		||||
@ -204,7 +203,7 @@ export default {
 | 
			
		||||
          if (error.response && error.response.data) {
 | 
			
		||||
            this.$toast.error(error.response.data)
 | 
			
		||||
          } else {
 | 
			
		||||
            this.$toast.error('Oops, something went wrong...')
 | 
			
		||||
            this.$toast.error(this.$strings.ToastUnknownError)
 | 
			
		||||
          }
 | 
			
		||||
          this.processingUpload = false
 | 
			
		||||
        })
 | 
			
		||||
@ -255,7 +254,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    async updateCover(cover) {
 | 
			
		||||
      if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
 | 
			
		||||
        this.$toast.error('Invalid URL')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastInvalidUrl)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -264,11 +263,10 @@ export default {
 | 
			
		||||
        .$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.imageUrl = ''
 | 
			
		||||
          this.$toast.success('Update Successful')
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to update cover', error)
 | 
			
		||||
          this.$toast.error(error.response?.data || 'Failed to update cover')
 | 
			
		||||
          this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.isProcessing = false
 | 
			
		||||
@ -308,12 +306,9 @@ export default {
 | 
			
		||||
      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')
 | 
			
		||||
          this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.isProcessing = false
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
      var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
 | 
			
		||||
      if (!title) {
 | 
			
		||||
        this.$toast.error('Must have a title for quick match')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastTitleRequired)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.quickMatching = true
 | 
			
		||||
@ -108,9 +108,9 @@ export default {
 | 
			
		||||
          if (res.warning) {
 | 
			
		||||
            this.$toast.warning(res.warning)
 | 
			
		||||
          } else if (res.updated) {
 | 
			
		||||
            this.$toast.success('Item details updated')
 | 
			
		||||
            this.$toast.success(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
          } else {
 | 
			
		||||
            this.$toast.info('No updates were made')
 | 
			
		||||
            this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
@ -128,18 +128,18 @@ export default {
 | 
			
		||||
          this.rescanning = false
 | 
			
		||||
          var result = data.result
 | 
			
		||||
          if (!result) {
 | 
			
		||||
            this.$toast.error(`Re-Scan Failed for "${this.title}"`)
 | 
			
		||||
            this.$toast.error(this.$getString('ToastRescanFailed', [this.title]))
 | 
			
		||||
          } else if (result === 'UPDATED') {
 | 
			
		||||
            this.$toast.success(`Re-Scan complete item was updated`)
 | 
			
		||||
            this.$toast.success(this.$strings.ToastRescanUpdated)
 | 
			
		||||
          } else if (result === 'UPTODATE') {
 | 
			
		||||
            this.$toast.success(`Re-Scan complete item was up to date`)
 | 
			
		||||
            this.$toast.success(this.$strings.ToastRescanUpToDate)
 | 
			
		||||
          } else if (result === 'REMOVED') {
 | 
			
		||||
            this.$toast.error(`Re-Scan complete item was removed`)
 | 
			
		||||
            this.$toast.error(this.$strings.ToastRescanRemoved)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to scan library item', error)
 | 
			
		||||
          this.$toast.error('Failed to scan library item')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastScanFailed)
 | 
			
		||||
          this.rescanning = false
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
@ -156,7 +156,7 @@ export default {
 | 
			
		||||
      }
 | 
			
		||||
      var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
 | 
			
		||||
      if (!updatedDetails.hasChanges) {
 | 
			
		||||
        this.$toast.info('No changes were made')
 | 
			
		||||
        this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      return this.updateDetails(updatedDetails)
 | 
			
		||||
@ -170,7 +170,7 @@ export default {
 | 
			
		||||
      this.isProcessing = false
 | 
			
		||||
      if (updateResult) {
 | 
			
		||||
        if (updateResult.updated) {
 | 
			
		||||
          this.$toast.success('Item details updated')
 | 
			
		||||
          this.$toast.success(this.$strings.MessageItemDetailsUpdated)
 | 
			
		||||
          return true
 | 
			
		||||
        } else {
 | 
			
		||||
          this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
 | 
			
		||||
 | 
			
		||||
@ -397,7 +397,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    submitSearch() {
 | 
			
		||||
      if (!this.searchTitle) {
 | 
			
		||||
        this.$toast.warning('Search title is required')
 | 
			
		||||
        this.$toast.warning(this.$strings.ToastTitleRequired)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.persistProvider()
 | 
			
		||||
@ -618,7 +618,7 @@ export default {
 | 
			
		||||
          if (updateResult.updated) {
 | 
			
		||||
            this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
 | 
			
		||||
          } else {
 | 
			
		||||
            this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
 | 
			
		||||
            this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
          }
 | 
			
		||||
          this.clearSelectedMatch()
 | 
			
		||||
          this.$emit('selectTab', 'details')
 | 
			
		||||
 | 
			
		||||
@ -163,7 +163,7 @@ export default {
 | 
			
		||||
      this.isProcessing = false
 | 
			
		||||
      if (updateResult) {
 | 
			
		||||
        if (updateResult.updated) {
 | 
			
		||||
          this.$toast.success('Item details updated')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
 | 
			
		||||
          return true
 | 
			
		||||
        } else {
 | 
			
		||||
          this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
 | 
			
		||||
 | 
			
		||||
@ -156,7 +156,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    validate() {
 | 
			
		||||
      if (!this.libraryCopy.name) {
 | 
			
		||||
        this.$toast.error('Library must have a name')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastNameRequired)
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      if (!this.libraryCopy.folders.length) {
 | 
			
		||||
@ -205,7 +205,7 @@ export default {
 | 
			
		||||
    submitUpdateLibrary() {
 | 
			
		||||
      var newLibraryPayload = this.getLibraryUpdatePayload()
 | 
			
		||||
      if (!Object.keys(newLibraryPayload).length) {
 | 
			
		||||
        this.$toast.info('No updates are necessary')
 | 
			
		||||
        this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -162,7 +162,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to get filesystem paths', error)
 | 
			
		||||
          this.$toast.error('Failed to get filesystem paths')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastFailedToLoadData)
 | 
			
		||||
          return []
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
 | 
			
		||||
@ -86,7 +86,7 @@ export default {
 | 
			
		||||
      return this.selectedEventData && this.selectedEventData.requiresLibrary
 | 
			
		||||
    },
 | 
			
		||||
    title() {
 | 
			
		||||
      return this.isNew ? 'Create Notification' : 'Update Notification'
 | 
			
		||||
      return this.isNew ? this.$strings.HeaderNotificationCreate : this.$strings.HeaderNotificationUpdate
 | 
			
		||||
    },
 | 
			
		||||
    availableVariables() {
 | 
			
		||||
      return this.selectedEventData ? this.selectedEventData.variables || null : null
 | 
			
		||||
@ -106,7 +106,7 @@ export default {
 | 
			
		||||
      this.$refs.urlsInput?.forceBlur()
 | 
			
		||||
 | 
			
		||||
      if (!this.newNotification.urls.length) {
 | 
			
		||||
        this.$toast.error('Must enter an Apprise URL')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastAppriseUrlRequired)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -127,12 +127,12 @@ export default {
 | 
			
		||||
        .$patch(`/api/notifications/${payload.id}`, payload)
 | 
			
		||||
        .then((updatedSettings) => {
 | 
			
		||||
          this.$emit('update', updatedSettings)
 | 
			
		||||
          this.$toast.success('Notification updated')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastNotificationUpdateSuccess)
 | 
			
		||||
          this.show = false
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to update notification', error)
 | 
			
		||||
          this.$toast.error('Failed to update notification')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
@ -149,12 +149,11 @@ export default {
 | 
			
		||||
        .$post('/api/notifications', payload)
 | 
			
		||||
        .then((updatedSettings) => {
 | 
			
		||||
          this.$emit('update', updatedSettings)
 | 
			
		||||
          this.$toast.success('Notification created')
 | 
			
		||||
          this.show = false
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to create notification', error)
 | 
			
		||||
          this.$toast.error('Failed to create notification')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastNotificationCreateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processing = false
 | 
			
		||||
 | 
			
		||||
@ -130,12 +130,12 @@ export default {
 | 
			
		||||
        .$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
 | 
			
		||||
        .then((updatedPlaylist) => {
 | 
			
		||||
          console.log(`Items removed from playlist`, updatedPlaylist)
 | 
			
		||||
          this.$toast.success('Playlist item(s) removed')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
 | 
			
		||||
          this.processing = false
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to remove items from playlist', error)
 | 
			
		||||
          this.$toast.error('Failed to remove playlist item(s)')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
 | 
			
		||||
          this.processing = false
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
@ -148,12 +148,12 @@ export default {
 | 
			
		||||
        .$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
 | 
			
		||||
        .then((updatedPlaylist) => {
 | 
			
		||||
          console.log(`Items added to playlist`, updatedPlaylist)
 | 
			
		||||
          this.$toast.success('Items added to playlist')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
 | 
			
		||||
          this.processing = false
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to add items to playlist', error)
 | 
			
		||||
          this.$toast.error('Failed to add items to playlist')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
 | 
			
		||||
          this.processing = false
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
@ -174,14 +174,14 @@ export default {
 | 
			
		||||
        .$post('/api/playlists', newPlaylist)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          console.log('New playlist created', data)
 | 
			
		||||
          this.$toast.success(`Playlist "${data.name}" created`)
 | 
			
		||||
          this.$toast.success(this.$strings.ToastPlaylistCreateSuccess + ': ' + data.name)
 | 
			
		||||
          this.processing = false
 | 
			
		||||
          this.newPlaylistName = ''
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to create playlist', error)
 | 
			
		||||
          var errMsg = error.response ? error.response.data || '' : ''
 | 
			
		||||
          this.$toast.error(`Failed to create playlist: ${errMsg}`)
 | 
			
		||||
          this.$toast.error(this.$strings.ToastPlaylistCreateFailed + ': ' + errMsg)
 | 
			
		||||
          this.processing = false
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -86,7 +86,7 @@ export default {
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Failed to remove playlist', error)
 | 
			
		||||
            this.processing = false
 | 
			
		||||
            this.$toast.error(this.$strings.ToastPlaylistRemoveFailed)
 | 
			
		||||
            this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
          })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
@ -95,7 +95,7 @@ export default {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (!this.newPlaylistName) {
 | 
			
		||||
        return this.$toast.error('Playlist must have a name')
 | 
			
		||||
        return this.$toast.error(this.$strings.ToastNameRequired)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.processing = true
 | 
			
		||||
 | 
			
		||||
@ -142,7 +142,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
      const updatedDetails = this.getUpdatePayload()
 | 
			
		||||
      if (!Object.keys(updatedDetails).length) {
 | 
			
		||||
        this.$toast.info('No changes were made')
 | 
			
		||||
        this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      return this.updateDetails(updatedDetails)
 | 
			
		||||
 | 
			
		||||
@ -105,7 +105,7 @@ export default {
 | 
			
		||||
      }
 | 
			
		||||
      const updatePayload = this.getUpdatePayload(episodeData)
 | 
			
		||||
      if (!Object.keys(updatePayload).length) {
 | 
			
		||||
        return this.$toast.info('No updates are necessary')
 | 
			
		||||
        return this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
      }
 | 
			
		||||
      console.log('Episode update payload', updatePayload)
 | 
			
		||||
 | 
			
		||||
@ -126,7 +126,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    submitForm() {
 | 
			
		||||
      if (!this.episodeTitle || !this.episodeTitle.length) {
 | 
			
		||||
        this.$toast.error('Must enter an episode title')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastTitleRequired)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.searchedTitle = this.episodeTitle
 | 
			
		||||
 | 
			
		||||
@ -121,14 +121,14 @@ export default {
 | 
			
		||||
  methods: {
 | 
			
		||||
    openFeed() {
 | 
			
		||||
      if (!this.newFeedSlug) {
 | 
			
		||||
        this.$toast.error('Must set a feed slug')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastSlugRequired)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const sanitized = this.$sanitizeSlug(this.newFeedSlug)
 | 
			
		||||
      if (this.newFeedSlug !== sanitized) {
 | 
			
		||||
        this.newFeedSlug = sanitized
 | 
			
		||||
        this.$toast.warning('Slug had to be modified - Run again')
 | 
			
		||||
        this.$toast.warning(this.$strings.ToastSlugMustChange)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -261,7 +261,7 @@ export default {
 | 
			
		||||
            .catch((error) => {
 | 
			
		||||
              console.error('Failed to share', error)
 | 
			
		||||
              if (error.name !== 'AbortError') {
 | 
			
		||||
                this.$toast.error('Failed to share: ' + error.message)
 | 
			
		||||
                this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
@ -237,7 +237,7 @@ export default {
 | 
			
		||||
            .catch((error) => {
 | 
			
		||||
              console.error('Failed to share', error)
 | 
			
		||||
              if (error.name !== 'AbortError') {
 | 
			
		||||
                this.$toast.error('Failed to share: ' + error.message)
 | 
			
		||||
                this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
@ -167,7 +167,7 @@ export default {
 | 
			
		||||
            .catch((error) => {
 | 
			
		||||
              console.error('Failed to share', error)
 | 
			
		||||
              if (error.name !== 'AbortError') {
 | 
			
		||||
                this.$toast.error('Failed to share: ' + error.message)
 | 
			
		||||
                this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
@ -186,7 +186,7 @@ export default {
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.loadBackups()
 | 
			
		||||
    if (this.$route.query.backup) {
 | 
			
		||||
      this.$toast.success('Backup applied successfully')
 | 
			
		||||
      this.$toast.success(this.$strings.ToastBackupAppliedSuccess)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to update collection', error)
 | 
			
		||||
          this.$toast.error('Failed to save collection books order')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
    editBook(book) {
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@ export default {
 | 
			
		||||
  methods: {
 | 
			
		||||
    removeProvider(provider) {
 | 
			
		||||
      const payload = {
 | 
			
		||||
        message: `Are you sure you want remove custom metadata provider "${provider.name}"?`,
 | 
			
		||||
        message: this.$getString('MessageConfirmDeleteMetadataProvider', [provider.name]),
 | 
			
		||||
        callback: (confirmed) => {
 | 
			
		||||
          if (confirmed) {
 | 
			
		||||
            this.$emit('update:processing', true)
 | 
			
		||||
@ -53,12 +53,12 @@ export default {
 | 
			
		||||
            this.$axios
 | 
			
		||||
              .$delete(`/api/custom-metadata-providers/${provider.id}`)
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                this.$toast.success('Provider removed')
 | 
			
		||||
                this.$toast.success(this.$strings.ToastProviderRemoveSuccess)
 | 
			
		||||
                this.$emit('removed', provider.id)
 | 
			
		||||
              })
 | 
			
		||||
              .catch((error) => {
 | 
			
		||||
                console.error('Failed to remove provider', error)
 | 
			
		||||
                this.$toast.error('Failed to remove provider')
 | 
			
		||||
                this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
              })
 | 
			
		||||
              .finally(() => {
 | 
			
		||||
                this.$emit('update:processing', false)
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to update playlist', error)
 | 
			
		||||
          this.$toast.error('Failed to save playlist items order')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
    init() {
 | 
			
		||||
 | 
			
		||||
@ -218,12 +218,12 @@ export default {
 | 
			
		||||
            this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
 | 
			
		||||
          } else {
 | 
			
		||||
            console.log(`Item removed from playlist`, updatedPlaylist)
 | 
			
		||||
            this.$toast.success('Item removed from playlist')
 | 
			
		||||
            this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to remove item from playlist', error)
 | 
			
		||||
          this.$toast.error('Failed to remove item from playlist')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.processingRemove = false
 | 
			
		||||
 | 
			
		||||
@ -270,7 +270,7 @@ export default {
 | 
			
		||||
                if (data.numEpisodesUpdated) {
 | 
			
		||||
                  this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
 | 
			
		||||
                } else {
 | 
			
		||||
                  this.$toast.info('No changes were made')
 | 
			
		||||
                  this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
                }
 | 
			
		||||
              })
 | 
			
		||||
              .catch((error) => {
 | 
			
		||||
@ -295,7 +295,7 @@ export default {
 | 
			
		||||
        episodeId: episode.id,
 | 
			
		||||
        title: episode.title,
 | 
			
		||||
        subtitle: this.mediaMetadata.title,
 | 
			
		||||
        caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
 | 
			
		||||
        caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
 | 
			
		||||
        duration: episode.audioFile.duration || null,
 | 
			
		||||
        coverPath: this.media.coverPath || null
 | 
			
		||||
      }
 | 
			
		||||
@ -372,7 +372,7 @@ export default {
 | 
			
		||||
            episodeId: episode.id,
 | 
			
		||||
            title: episode.title,
 | 
			
		||||
            subtitle: this.mediaMetadata.title,
 | 
			
		||||
            caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
 | 
			
		||||
            caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
 | 
			
		||||
            duration: episode.audioFile.duration || null,
 | 
			
		||||
            coverPath: this.media.coverPath || null
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
        <div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
 | 
			
		||||
        <div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="text-gray-200 text-xs font-light mt-2 text-center">{{ text }}</div>
 | 
			
		||||
      <div class="text-gray-200 text-xs font-light mt-2 text-center">{{ message }}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@ -17,7 +17,12 @@ export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    text: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'Please Wait...'
 | 
			
		||||
      default: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    message() {
 | 
			
		||||
      return this.text || this.$strings.MessagePleaseWait
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -117,10 +117,10 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    submitChangePassword() {
 | 
			
		||||
      if (this.newPassword !== this.confirmPassword) {
 | 
			
		||||
        return this.$toast.error('New password and confirm password do not match')
 | 
			
		||||
        return this.$toast.error(this.$strings.ToastUserPasswordMismatch)
 | 
			
		||||
      }
 | 
			
		||||
      if (this.password === this.newPassword) {
 | 
			
		||||
        return this.$toast.error('Password and New Password cannot be the same')
 | 
			
		||||
        return this.$toast.error(this.$strings.ToastUserPasswordMustChange)
 | 
			
		||||
      }
 | 
			
		||||
      this.changingPassword = true
 | 
			
		||||
      this.$axios
 | 
			
		||||
@ -130,16 +130,16 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          if (res.success) {
 | 
			
		||||
            this.$toast.success('Password Changed Successfully')
 | 
			
		||||
            this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
 | 
			
		||||
            this.resetForm()
 | 
			
		||||
          } else {
 | 
			
		||||
            this.$toast.error(res.error || 'Unknown Error')
 | 
			
		||||
            this.$toast.error(res.error || this.$strings.ToastUnknownError)
 | 
			
		||||
          }
 | 
			
		||||
          this.changingPassword = false
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error(error)
 | 
			
		||||
          this.$toast.error('Api call failed')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastUnknownError)
 | 
			
		||||
          this.changingPassword = false
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -560,7 +560,7 @@ export default {
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          this.findingChapters = false
 | 
			
		||||
          console.error('Failed to get chapter data', error)
 | 
			
		||||
          this.$toast.error('Failed to find chapters')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastFailedToLoadData)
 | 
			
		||||
          this.showFindChaptersModal = false
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
@ -611,7 +611,7 @@ export default {
 | 
			
		||||
        .$post(`/api/items/${this.libraryItem.id}/chapters`, payload)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          if (data.updated) {
 | 
			
		||||
            this.$toast.success('Chapters removed')
 | 
			
		||||
            this.$toast.success(this.$strings.ToastChaptersRemoved)
 | 
			
		||||
            if (this.previousRoute) {
 | 
			
		||||
              this.$router.push(this.previousRoute)
 | 
			
		||||
            } else {
 | 
			
		||||
@ -623,7 +623,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to remove chapters', error)
 | 
			
		||||
          this.$toast.error('Failed to remove chapters')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.saving = false
 | 
			
		||||
 | 
			
		||||
@ -331,11 +331,11 @@ export default {
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.$toast.success('Encode canceled')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastEncodeCancelSucces)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to cancel encode', error)
 | 
			
		||||
          this.$toast.error('Failed to cancel encode')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastEncodeCancelFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.isCancelingEncode = false
 | 
			
		||||
 | 
			
		||||
@ -366,7 +366,7 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!updates.length) {
 | 
			
		||||
        return this.$toast.warning('No updates were made')
 | 
			
		||||
        return this.$toast.warning(this.$strings.ToastNoUpdatesNecessary)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log('Pushing updates', updates)
 | 
			
		||||
 | 
			
		||||
@ -162,7 +162,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to save backup path', error)
 | 
			
		||||
          const errorMsg = error.response?.data || 'Failed to save backup path'
 | 
			
		||||
          const errorMsg = error.response?.data || this.$strings.ToastBackupPathUpdateFailed
 | 
			
		||||
          this.$toast.error(errorMsg)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
@ -171,11 +171,11 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    updateBackupsSettings() {
 | 
			
		||||
      if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) {
 | 
			
		||||
        this.$toast.error('Invalid maximum backup size')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastBackupInvalidMaxSize)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (isNaN(this.backupsToKeep) || this.backupsToKeep <= 0 || this.backupsToKeep > 99) {
 | 
			
		||||
        this.$toast.error('Invalid number of backups to keep')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastBackupInvalidMaxKeep)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const updatePayload = {
 | 
			
		||||
 | 
			
		||||
@ -109,7 +109,7 @@
 | 
			
		||||
        </tr>
 | 
			
		||||
      </table>
 | 
			
		||||
      <div v-else-if="!loading" class="text-center py-4">
 | 
			
		||||
        <p class="text-lg text-gray-100">No Devices</p>
 | 
			
		||||
        <p class="text-lg text-gray-100">{{ $strings.MessageNoDevices }}</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </app-settings-content>
 | 
			
		||||
 | 
			
		||||
@ -199,7 +199,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    deleteDeviceClick(device) {
 | 
			
		||||
      const payload = {
 | 
			
		||||
        message: `Are you sure you want to delete e-reader device "${device.name}"?`,
 | 
			
		||||
        message: this.$getString('MessageConfirmDeleteDevice', [device.name]),
 | 
			
		||||
        callback: (confirmed) => {
 | 
			
		||||
          if (confirmed) {
 | 
			
		||||
            this.deleteDevice(device)
 | 
			
		||||
@ -218,11 +218,10 @@ export default {
 | 
			
		||||
        .$post(`/api/emails/ereader-devices`, payload)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.ereaderDevicesUpdated(data.ereaderDevices)
 | 
			
		||||
          this.$toast.success('Device deleted')
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to delete device', error)
 | 
			
		||||
          this.$toast.error('Failed to delete device')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.deletingDeviceName = null
 | 
			
		||||
@ -246,11 +245,11 @@ export default {
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .$post('/api/emails/test')
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.$toast.success('Test Email Sent')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastDeviceTestEmailSuccess)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to send test email', error)
 | 
			
		||||
          const errorMsg = error.response.data || 'Failed to send test email'
 | 
			
		||||
          const errorMsg = error.response.data || this.$strings.ToastDeviceTestEmailFailed
 | 
			
		||||
          this.$toast.error(errorMsg)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
@ -289,11 +288,11 @@ export default {
 | 
			
		||||
          this.newSettings = {
 | 
			
		||||
            ...data.settings
 | 
			
		||||
          }
 | 
			
		||||
          this.$toast.success('Email settings updated')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastEmailSettingsUpdateSuccess)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to update email settings', error)
 | 
			
		||||
          this.$toast.error('Failed to update email settings')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastEmailSettingsUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.savingSettings = false
 | 
			
		||||
 | 
			
		||||
@ -130,7 +130,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to rename genre', error)
 | 
			
		||||
          this.$toast.error('Failed to rename genre')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastRenameFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.loading = false
 | 
			
		||||
@ -147,7 +147,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to remove genre', error)
 | 
			
		||||
          this.$toast.error('Failed to remove genre')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.loading = false
 | 
			
		||||
 | 
			
		||||
@ -126,7 +126,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to rename tag', error)
 | 
			
		||||
          this.$toast.error('Failed to rename tag')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastRenameFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.loading = false
 | 
			
		||||
@ -143,7 +143,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to remove tag', error)
 | 
			
		||||
          this.$toast.error('Failed to remove tag')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.loading = false
 | 
			
		||||
 | 
			
		||||
@ -105,12 +105,12 @@ export default {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isNaN(this.maxNotificationQueue) || this.maxNotificationQueue <= 0) {
 | 
			
		||||
        this.$toast.error('Max notification queue must be >= 0')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastNotificationQueueMaximum)
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isNaN(this.maxFailedAttempts) || this.maxFailedAttempts <= 0) {
 | 
			
		||||
        this.$toast.error('Max failed attempts must be >= 0')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastNotificationFailedMaximum)
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -128,11 +128,11 @@ export default {
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .$patch('/api/notifications', updatePayload)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.$toast.success('Notification settings updated')
 | 
			
		||||
          this.$toast.success(this.$strings.ToastNotificationSettingsUpdateSuccess)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to update notification settings', error)
 | 
			
		||||
          this.$toast.error('Failed to update notification settings')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastNotificationSettingsUpdateFailed)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.savingSettings = false
 | 
			
		||||
 | 
			
		||||
@ -290,7 +290,6 @@ export default {
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .$post(`/api/sessions/batch/delete`, payload)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.$toast.success('Sessions removed')
 | 
			
		||||
          if (isAllSessions) {
 | 
			
		||||
            // If all sessions were removed from the current page then go to the previous page
 | 
			
		||||
            if (this.currentPage > 0) {
 | 
			
		||||
@ -303,7 +302,7 @@ export default {
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          const errorMsg = error.response?.data || 'Failed to remove sessions'
 | 
			
		||||
          const errorMsg = error.response?.data || this.$strings.ToastRemoveFailed
 | 
			
		||||
          this.$toast.error(errorMsg)
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
@ -358,12 +357,13 @@ export default {
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (!libraryItem) {
 | 
			
		||||
        this.$toast.error('Failed to get library item')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastFailedToLoadData)
 | 
			
		||||
        this.processingGoToTimestamp = false
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
 | 
			
		||||
        this.$toast.error('Failed to get podcast episode')
 | 
			
		||||
        console.error('Episode not found in library item', session.episodeId, libraryItem.media.episodes)
 | 
			
		||||
        this.$toast.error(this.$strings.ToastFailedToLoadData)
 | 
			
		||||
        this.processingGoToTimestamp = false
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
@ -377,7 +377,7 @@ export default {
 | 
			
		||||
          episodeId: episode.id,
 | 
			
		||||
          title: episode.title,
 | 
			
		||||
          subtitle: libraryItem.media.metadata.title,
 | 
			
		||||
          caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
 | 
			
		||||
          caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
 | 
			
		||||
          duration: episode.audioFile.duration || null,
 | 
			
		||||
          coverPath: libraryItem.media.coverPath || null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -127,12 +127,13 @@ export default {
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (!libraryItem) {
 | 
			
		||||
        this.$toast.error('Failed to get library item')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastFailedToLoadData)
 | 
			
		||||
        this.processingGoToTimestamp = false
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
 | 
			
		||||
        this.$toast.error('Failed to get podcast episode')
 | 
			
		||||
        console.error('Episode not found in library item', session.episodeId, libraryItem.media.episodes)
 | 
			
		||||
        this.$toast.error(this.$strings.ToastFailedToLoadData)
 | 
			
		||||
        this.processingGoToTimestamp = false
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
@ -146,7 +147,7 @@ export default {
 | 
			
		||||
          episodeId: episode.id,
 | 
			
		||||
          title: episode.title,
 | 
			
		||||
          subtitle: libraryItem.media.metadata.title,
 | 
			
		||||
          caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
 | 
			
		||||
          caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
 | 
			
		||||
          duration: episode.audioFile.duration || null,
 | 
			
		||||
          coverPath: libraryItem.media.coverPath || null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -484,23 +484,23 @@ export default {
 | 
			
		||||
        this.$axios
 | 
			
		||||
          .$get(`/api/podcasts/${this.libraryItemId}/clear-queue`)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            this.$toast.success('Episode download queue cleared')
 | 
			
		||||
            this.$toast.success(this.$strings.ToastEpisodeDownloadQueueClearSuccess)
 | 
			
		||||
            this.episodeDownloadQueued = []
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Failed to clear queue', error)
 | 
			
		||||
            this.$toast.error('Failed to clear queue')
 | 
			
		||||
            this.$toast.error(this.$strings.ToastEpisodeDownloadQueueClearFailed)
 | 
			
		||||
          })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    async findEpisodesClick() {
 | 
			
		||||
      if (!this.mediaMetadata.feedUrl) {
 | 
			
		||||
        return this.$toast.error('Podcast does not have an RSS Feed')
 | 
			
		||||
        return this.$toast.error(this.$strings.ToastNoRSSFeed)
 | 
			
		||||
      }
 | 
			
		||||
      this.fetchingRSSFeed = true
 | 
			
		||||
      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('Failed to get podcast feed')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
 | 
			
		||||
        return null
 | 
			
		||||
      })
 | 
			
		||||
      this.fetchingRSSFeed = false
 | 
			
		||||
@ -509,7 +509,7 @@ export default {
 | 
			
		||||
      console.log('Podcast feed', payload)
 | 
			
		||||
      const podcastfeed = payload.podcast
 | 
			
		||||
      if (!podcastfeed.episodes || !podcastfeed.episodes.length) {
 | 
			
		||||
        this.$toast.info('No episodes found in RSS feed')
 | 
			
		||||
        this.$toast.info(this.$strings.ToastPodcastNoEpisodesInFeed)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -578,7 +578,7 @@ export default {
 | 
			
		||||
              episodeId: episode.id,
 | 
			
		||||
              title: episode.title,
 | 
			
		||||
              subtitle: this.title,
 | 
			
		||||
              caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
 | 
			
		||||
              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
 | 
			
		||||
            })
 | 
			
		||||
@ -622,13 +622,12 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    clearProgressClick() {
 | 
			
		||||
      if (!this.userMediaProgress) return
 | 
			
		||||
      if (confirm(`Are you sure you want to reset your progress?`)) {
 | 
			
		||||
      if (confirm(this.$strings.MessageConfirmResetProgress)) {
 | 
			
		||||
        this.resettingProgress = true
 | 
			
		||||
        this.$axios
 | 
			
		||||
          .$delete(`/api/me/progress/${this.userMediaProgress.id}`)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            console.log('Progress reset complete')
 | 
			
		||||
            this.$toast.success(`Your progress was reset`)
 | 
			
		||||
            this.resettingProgress = false
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
@ -722,12 +721,12 @@ export default {
 | 
			
		||||
            this.$axios
 | 
			
		||||
              .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                this.$toast.success('Item deleted')
 | 
			
		||||
                this.$toast.success(this.$strings.ToastItemDeletedSuccess)
 | 
			
		||||
                this.$router.replace(`/library/${this.libraryId}`)
 | 
			
		||||
              })
 | 
			
		||||
              .catch((error) => {
 | 
			
		||||
                console.error('Failed to delete item', error)
 | 
			
		||||
                this.$toast.error('Failed to delete item')
 | 
			
		||||
                this.$toast.error(this.$strings.ToastItemDeleteFailed)
 | 
			
		||||
              })
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -138,7 +138,7 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to remove narrator', error)
 | 
			
		||||
          this.$toast.error('Failed to remove narrator')
 | 
			
		||||
          this.$toast.error(this.$strings.ToastRemoveFailed)
 | 
			
		||||
          this.loading = false
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -111,7 +111,7 @@ export default {
 | 
			
		||||
      this.processing = true
 | 
			
		||||
      const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => {
 | 
			
		||||
        console.error('Failed to get download queue', error)
 | 
			
		||||
        this.$toast.error('Failed to get download queue')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastFailedToLoadData)
 | 
			
		||||
        return null
 | 
			
		||||
      })
 | 
			
		||||
      this.processing = false
 | 
			
		||||
 | 
			
		||||
@ -234,7 +234,7 @@ export default {
 | 
			
		||||
            episodeId: episode.id,
 | 
			
		||||
            title: episode.title,
 | 
			
		||||
            subtitle: episode.podcast.metadata.title,
 | 
			
		||||
            caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
 | 
			
		||||
            caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
 | 
			
		||||
            duration: episode.duration || null,
 | 
			
		||||
            coverPath: episode.podcast.coverPath || null
 | 
			
		||||
          })
 | 
			
		||||
@ -251,7 +251,7 @@ export default {
 | 
			
		||||
      this.processing = true
 | 
			
		||||
      const episodePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/recent-episodes?limit=25&page=${page}`).catch((error) => {
 | 
			
		||||
        console.error('Failed to get recent episodes', error)
 | 
			
		||||
        this.$toast.error('Failed to get recent episodes')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastFailedToLoadData)
 | 
			
		||||
        return null
 | 
			
		||||
      })
 | 
			
		||||
      this.processing = false
 | 
			
		||||
@ -271,7 +271,7 @@ export default {
 | 
			
		||||
          episodeId: episode.id,
 | 
			
		||||
          title: episode.title,
 | 
			
		||||
          subtitle: episode.podcast.metadata.title,
 | 
			
		||||
          caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
 | 
			
		||||
          caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
 | 
			
		||||
          duration: episode.duration || null,
 | 
			
		||||
          coverPath: episode.podcast.coverPath || null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -146,7 +146,7 @@ export default {
 | 
			
		||||
      this.processing = true
 | 
			
		||||
      var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed }).catch((error) => {
 | 
			
		||||
        console.error('Failed to get feed', error)
 | 
			
		||||
        this.$toast.error('Failed to get podcast feed')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
 | 
			
		||||
        return null
 | 
			
		||||
      })
 | 
			
		||||
      this.processing = false
 | 
			
		||||
@ -197,7 +197,7 @@ export default {
 | 
			
		||||
      this.processing = true
 | 
			
		||||
      const payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => {
 | 
			
		||||
        console.error('Failed to get feed', error)
 | 
			
		||||
        this.$toast.error('Failed to get podcast feed')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
 | 
			
		||||
        return null
 | 
			
		||||
      })
 | 
			
		||||
      this.processing = false
 | 
			
		||||
 | 
			
		||||
@ -132,11 +132,11 @@ export default {
 | 
			
		||||
  methods: {
 | 
			
		||||
    async submitServerSetup() {
 | 
			
		||||
      if (!this.newRoot.username || !this.newRoot.username.trim()) {
 | 
			
		||||
        this.$toast.error('Must enter a root username')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastUserRootRequireName)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (this.newRoot.password !== this.confirmPassword) {
 | 
			
		||||
        this.$toast.error('Password mismatch')
 | 
			
		||||
        this.$toast.error(this.$strings.ToastUserPasswordMismatch)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (!this.newRoot.password) {
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@
 | 
			
		||||
  "ButtonChooseFiles": "Choose files",
 | 
			
		||||
  "ButtonClearFilter": "Clear Filter",
 | 
			
		||||
  "ButtonCloseFeed": "Close Feed",
 | 
			
		||||
  "ButtonCloseSession": "Close Open Session",
 | 
			
		||||
  "ButtonCollections": "Collections",
 | 
			
		||||
  "ButtonConfigureScanner": "Configure Scanner",
 | 
			
		||||
  "ButtonCreate": "Create",
 | 
			
		||||
@ -28,6 +29,9 @@
 | 
			
		||||
  "ButtonEdit": "Edit",
 | 
			
		||||
  "ButtonEditChapters": "Edit Chapters",
 | 
			
		||||
  "ButtonEditPodcast": "Edit Podcast",
 | 
			
		||||
  "ButtonEnable": "Enable",
 | 
			
		||||
  "ButtonFireAndFail": "Fire and Fail",
 | 
			
		||||
  "ButtonFireOnTest": "Fire onTest event",
 | 
			
		||||
  "ButtonForceReScan": "Force Re-Scan",
 | 
			
		||||
  "ButtonFullPath": "Full Path",
 | 
			
		||||
  "ButtonHide": "Hide",
 | 
			
		||||
@ -56,6 +60,7 @@
 | 
			
		||||
  "ButtonPlaylists": "Playlists",
 | 
			
		||||
  "ButtonPrevious": "Previous",
 | 
			
		||||
  "ButtonPreviousChapter": "Previous Chapter",
 | 
			
		||||
  "ButtonProbeAudioFile": "Probe Audio File",
 | 
			
		||||
  "ButtonPurgeAllCache": "Purge All Cache",
 | 
			
		||||
  "ButtonPurgeItemsCache": "Purge Items Cache",
 | 
			
		||||
  "ButtonQueueAddItem": "Add to queue",
 | 
			
		||||
@ -93,6 +98,7 @@
 | 
			
		||||
  "ButtonStats": "Stats",
 | 
			
		||||
  "ButtonSubmit": "Submit",
 | 
			
		||||
  "ButtonTest": "Test",
 | 
			
		||||
  "ButtonUnlinkOpedId": "Unlink OpenID",
 | 
			
		||||
  "ButtonUpload": "Upload",
 | 
			
		||||
  "ButtonUploadBackup": "Upload Backup",
 | 
			
		||||
  "ButtonUploadCover": "Upload Cover",
 | 
			
		||||
@ -105,6 +111,7 @@
 | 
			
		||||
  "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
 | 
			
		||||
  "ErrorUploadLacksTitle": "Must have a title",
 | 
			
		||||
  "HeaderAccount": "Account",
 | 
			
		||||
  "HeaderAddCustomMetadataProvider": "Add Custom Metadata Provider",
 | 
			
		||||
  "HeaderAdvanced": "Advanced",
 | 
			
		||||
  "HeaderAppriseNotificationSettings": "Apprise Notification Settings",
 | 
			
		||||
  "HeaderAudioTracks": "Audio Tracks",
 | 
			
		||||
@ -150,6 +157,8 @@
 | 
			
		||||
  "HeaderMetadataToEmbed": "Metadata to embed",
 | 
			
		||||
  "HeaderNewAccount": "New Account",
 | 
			
		||||
  "HeaderNewLibrary": "New Library",
 | 
			
		||||
  "HeaderNotificationCreate": "Create Notification",
 | 
			
		||||
  "HeaderNotificationUpdate": "Update Notification",
 | 
			
		||||
  "HeaderNotifications": "Notifications",
 | 
			
		||||
  "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
 | 
			
		||||
  "HeaderOpenRSSFeed": "Open RSS Feed",
 | 
			
		||||
@ -206,8 +215,8 @@
 | 
			
		||||
  "LabelAddToCollectionBatch": "Add {0} Books to Collection",
 | 
			
		||||
  "LabelAddToPlaylist": "Add to Playlist",
 | 
			
		||||
  "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
 | 
			
		||||
  "LabelAdded": "Added",
 | 
			
		||||
  "LabelAddedAt": "Added At",
 | 
			
		||||
  "LabelAddedDate": "Added {0}",
 | 
			
		||||
  "LabelAdminUsersOnly": "Admin users only",
 | 
			
		||||
  "LabelAll": "All",
 | 
			
		||||
  "LabelAllUsers": "All Users",
 | 
			
		||||
@ -298,6 +307,7 @@
 | 
			
		||||
  "LabelEpisode": "Episode",
 | 
			
		||||
  "LabelEpisodeTitle": "Episode Title",
 | 
			
		||||
  "LabelEpisodeType": "Episode Type",
 | 
			
		||||
  "LabelEpisodes": "Episodes",
 | 
			
		||||
  "LabelExample": "Example",
 | 
			
		||||
  "LabelExpandSeries": "Expand Series",
 | 
			
		||||
  "LabelExpandSubSeries": "Expand Sub Series",
 | 
			
		||||
@ -309,7 +319,9 @@
 | 
			
		||||
  "LabelFetchingMetadata": "Fetching Metadata",
 | 
			
		||||
  "LabelFile": "File",
 | 
			
		||||
  "LabelFileBirthtime": "File Birthtime",
 | 
			
		||||
  "LabelFileBornDate": "Born {0}",
 | 
			
		||||
  "LabelFileModified": "File Modified",
 | 
			
		||||
  "LabelFileModifiedDate": "Modified {0}",
 | 
			
		||||
  "LabelFilename": "Filename",
 | 
			
		||||
  "LabelFilterByUser": "Filter by User",
 | 
			
		||||
  "LabelFindEpisodes": "Find Episodes",
 | 
			
		||||
@ -448,8 +460,10 @@
 | 
			
		||||
  "LabelPrimaryEbook": "Primary ebook",
 | 
			
		||||
  "LabelProgress": "Progress",
 | 
			
		||||
  "LabelProvider": "Provider",
 | 
			
		||||
  "LabelProviderAuthorizationValue": "Authorization Header Value",
 | 
			
		||||
  "LabelPubDate": "Pub Date",
 | 
			
		||||
  "LabelPublishYear": "Publish Year",
 | 
			
		||||
  "LabelPublishedDate": "Published {0}",
 | 
			
		||||
  "LabelPublisher": "Publisher",
 | 
			
		||||
  "LabelPublishers": "Publishers",
 | 
			
		||||
  "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
 | 
			
		||||
@ -595,6 +609,7 @@
 | 
			
		||||
  "LabelUnabridged": "Unabridged",
 | 
			
		||||
  "LabelUndo": "Undo",
 | 
			
		||||
  "LabelUnknown": "Unknown",
 | 
			
		||||
  "LabelUnknownPublishDate": "Unknown publish date",
 | 
			
		||||
  "LabelUpdateCover": "Update Cover",
 | 
			
		||||
  "LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
 | 
			
		||||
  "LabelUpdateDetails": "Update Details",
 | 
			
		||||
@ -643,16 +658,22 @@
 | 
			
		||||
  "MessageCheckingCron": "Checking cron...",
 | 
			
		||||
  "MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
 | 
			
		||||
  "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
 | 
			
		||||
  "MessageConfirmDeleteDevice": "Are you sure you want to delete e-reader device \"{0}\"?",
 | 
			
		||||
  "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
 | 
			
		||||
  "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
 | 
			
		||||
  "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
 | 
			
		||||
  "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
 | 
			
		||||
  "MessageConfirmDeleteMetadataProvider": "Are you sure you want to delete custom metadata provider \"{0}\"?",
 | 
			
		||||
  "MessageConfirmDeleteNotification": "Are you sure you want to delete this notification?",
 | 
			
		||||
  "MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
 | 
			
		||||
  "MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
 | 
			
		||||
  "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
 | 
			
		||||
  "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
 | 
			
		||||
  "MessageConfirmMarkItemFinished": "Are you sure you want to mark \"{0}\" as finished?",
 | 
			
		||||
  "MessageConfirmMarkItemNotFinished": "Are you sure you want to mark \"{0}\" as not finished?",
 | 
			
		||||
  "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
 | 
			
		||||
  "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
 | 
			
		||||
  "MessageConfirmNotificationTestTrigger": "Trigger this notification with test data?",
 | 
			
		||||
  "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at <code>/metadata/cache</code>. <br /><br />Are you sure you want to remove the cache directory?",
 | 
			
		||||
  "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
 | 
			
		||||
  "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
 | 
			
		||||
@ -671,7 +692,9 @@
 | 
			
		||||
  "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
 | 
			
		||||
  "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
 | 
			
		||||
  "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
 | 
			
		||||
  "MessageConfirmResetProgress": "Are you sure you want to reset your progress?",
 | 
			
		||||
  "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
 | 
			
		||||
  "MessageConfirmUnlinkOpenId": "Are you sure you want to unlink this user from OpenID?",
 | 
			
		||||
  "MessageDownloadingEpisode": "Downloading episode",
 | 
			
		||||
  "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
 | 
			
		||||
  "MessageEmbedFailed": "Embed Failed!",
 | 
			
		||||
@ -706,6 +729,7 @@
 | 
			
		||||
  "MessageNoCollections": "No Collections",
 | 
			
		||||
  "MessageNoCoversFound": "No Covers Found",
 | 
			
		||||
  "MessageNoDescription": "No description",
 | 
			
		||||
  "MessageNoDevices": "No devices",
 | 
			
		||||
  "MessageNoDownloadsInProgress": "No downloads currently in progress",
 | 
			
		||||
  "MessageNoDownloadsQueued": "No downloads queued",
 | 
			
		||||
  "MessageNoEpisodeMatchesFound": "No episode matches found",
 | 
			
		||||
@ -725,7 +749,6 @@
 | 
			
		||||
  "MessageNoSeries": "No Series",
 | 
			
		||||
  "MessageNoTags": "No Tags",
 | 
			
		||||
  "MessageNoTasksRunning": "No Tasks Running",
 | 
			
		||||
  "MessageNoUpdateNecessary": "No update necessary",
 | 
			
		||||
  "MessageNoUpdatesWereNecessary": "No updates were necessary",
 | 
			
		||||
  "MessageNoUserPlaylists": "You have no playlists",
 | 
			
		||||
  "MessageNotYetImplemented": "Not yet implemented",
 | 
			
		||||
@ -734,6 +757,7 @@
 | 
			
		||||
  "MessagePauseChapter": "Pause chapter playback",
 | 
			
		||||
  "MessagePlayChapter": "Listen to beginning of chapter",
 | 
			
		||||
  "MessagePlaylistCreateFromCollection": "Create playlist from collection",
 | 
			
		||||
  "MessagePleaseWait": "Please wait...",
 | 
			
		||||
  "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
 | 
			
		||||
  "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
 | 
			
		||||
  "MessageRemoveChapter": "Remove chapter",
 | 
			
		||||
@ -794,24 +818,32 @@
 | 
			
		||||
  "StatsYearInReview": "YEAR IN REVIEW",
 | 
			
		||||
  "ToastAccountUpdateFailed": "Failed to update account",
 | 
			
		||||
  "ToastAccountUpdateSuccess": "Account updated",
 | 
			
		||||
  "ToastAuthorImageRemoveFailed": "Failed to remove image",
 | 
			
		||||
  "ToastAppriseUrlRequired": "Must enter an Apprise URL",
 | 
			
		||||
  "ToastAuthorImageRemoveSuccess": "Author image removed",
 | 
			
		||||
  "ToastAuthorNotFound": "Author \"{0}\" not found",
 | 
			
		||||
  "ToastAuthorRemoveSuccess": "Author removed",
 | 
			
		||||
  "ToastAuthorSearchNotFound": "Author not found",
 | 
			
		||||
  "ToastAuthorUpdateFailed": "Failed to update author",
 | 
			
		||||
  "ToastAuthorUpdateMerged": "Author merged",
 | 
			
		||||
  "ToastAuthorUpdateSuccess": "Author updated",
 | 
			
		||||
  "ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)",
 | 
			
		||||
  "ToastBackupAppliedSuccess": "Backup applied",
 | 
			
		||||
  "ToastBackupCreateFailed": "Failed to create backup",
 | 
			
		||||
  "ToastBackupCreateSuccess": "Backup created",
 | 
			
		||||
  "ToastBackupDeleteFailed": "Failed to delete backup",
 | 
			
		||||
  "ToastBackupDeleteSuccess": "Backup deleted",
 | 
			
		||||
  "ToastBackupInvalidMaxKeep": "Invalid number of backups to keep",
 | 
			
		||||
  "ToastBackupInvalidMaxSize": "Invalid maximum backup size",
 | 
			
		||||
  "ToastBackupPathUpdateFailed": "Failed to update backup path",
 | 
			
		||||
  "ToastBackupRestoreFailed": "Failed to restore backup",
 | 
			
		||||
  "ToastBackupUploadFailed": "Failed to upload backup",
 | 
			
		||||
  "ToastBackupUploadSuccess": "Backup uploaded",
 | 
			
		||||
  "ToastBatchDeleteFailed": "Batch delete failed",
 | 
			
		||||
  "ToastBatchDeleteSuccess": "Batch delete success",
 | 
			
		||||
  "ToastBatchUpdateFailed": "Batch update failed",
 | 
			
		||||
  "ToastBatchUpdateSuccess": "Batch update success",
 | 
			
		||||
  "ToastBookmarkCreateFailed": "Failed to create bookmark",
 | 
			
		||||
  "ToastBookmarkCreateSuccess": "Bookmark added",
 | 
			
		||||
  "ToastBookmarkRemoveFailed": "Failed to remove bookmark",
 | 
			
		||||
  "ToastBookmarkRemoveSuccess": "Bookmark removed",
 | 
			
		||||
  "ToastBookmarkUpdateFailed": "Failed to update bookmark",
 | 
			
		||||
  "ToastBookmarkUpdateSuccess": "Bookmark updated",
 | 
			
		||||
@ -819,25 +851,46 @@
 | 
			
		||||
  "ToastCachePurgeSuccess": "Cache purged successfully",
 | 
			
		||||
  "ToastChaptersHaveErrors": "Chapters have errors",
 | 
			
		||||
  "ToastChaptersMustHaveTitles": "Chapters must have titles",
 | 
			
		||||
  "ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection",
 | 
			
		||||
  "ToastChaptersRemoved": "Chapters removed",
 | 
			
		||||
  "ToastCollectionItemsAddFailed": "Item(s) added to collection failed",
 | 
			
		||||
  "ToastCollectionItemsAddSuccess": "Item(s) added to collection success",
 | 
			
		||||
  "ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
 | 
			
		||||
  "ToastCollectionRemoveFailed": "Failed to remove collection",
 | 
			
		||||
  "ToastCollectionRemoveSuccess": "Collection removed",
 | 
			
		||||
  "ToastCollectionUpdateFailed": "Failed to update collection",
 | 
			
		||||
  "ToastCollectionUpdateSuccess": "Collection updated",
 | 
			
		||||
  "ToastCoverUpdateFailed": "Cover update failed",
 | 
			
		||||
  "ToastDeleteFileFailed": "Failed to delete file",
 | 
			
		||||
  "ToastDeleteFileSuccess": "File deleted",
 | 
			
		||||
  "ToastDeviceAddFailed": "Failed to add device",
 | 
			
		||||
  "ToastDeviceNameAlreadyExists": "Ereader device with that name already exists",
 | 
			
		||||
  "ToastDeviceTestEmailFailed": "Failed to send test email",
 | 
			
		||||
  "ToastDeviceTestEmailSuccess": "Test email sent",
 | 
			
		||||
  "ToastDeviceUpdateFailed": "Failed to update device",
 | 
			
		||||
  "ToastEmailSettingsUpdateFailed": "Failed to update email settings",
 | 
			
		||||
  "ToastEmailSettingsUpdateSuccess": "Email settings updated",
 | 
			
		||||
  "ToastEncodeCancelFailed": "Failed to cancel encode",
 | 
			
		||||
  "ToastEncodeCancelSucces": "Encode canceled",
 | 
			
		||||
  "ToastEpisodeDownloadQueueClearFailed": "Failed to clear queue",
 | 
			
		||||
  "ToastEpisodeDownloadQueueClearSuccess": "Episode download queue cleared",
 | 
			
		||||
  "ToastErrorCannotShare": "Cannot share natively on this device",
 | 
			
		||||
  "ToastFailedToLoadData": "Failed to load data",
 | 
			
		||||
  "ToastFailedToShare": "Failed to share",
 | 
			
		||||
  "ToastFailedToUpdateAccount": "Failed to update account",
 | 
			
		||||
  "ToastFailedToUpdateUser": "Failed to update user",
 | 
			
		||||
  "ToastInvalidImageUrl": "Invalid image URL",
 | 
			
		||||
  "ToastInvalidUrl": "Invalid URL",
 | 
			
		||||
  "ToastItemCoverUpdateFailed": "Failed to update item cover",
 | 
			
		||||
  "ToastItemCoverUpdateSuccess": "Item cover updated",
 | 
			
		||||
  "ToastItemDeletedFailed": "Failed to delete item",
 | 
			
		||||
  "ToastItemDeletedSuccess": "Deleted item",
 | 
			
		||||
  "ToastItemDetailsUpdateFailed": "Failed to update item details",
 | 
			
		||||
  "ToastItemDetailsUpdateSuccess": "Item details updated",
 | 
			
		||||
  "ToastItemDetailsUpdateUnneeded": "No updates needed for item details",
 | 
			
		||||
  "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
 | 
			
		||||
  "ToastItemMarkedAsFinishedSuccess": "Item marked as Finished",
 | 
			
		||||
  "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
 | 
			
		||||
  "ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished",
 | 
			
		||||
  "ToastItemUpdateFailed": "Failed to update item",
 | 
			
		||||
  "ToastItemUpdateSuccess": "Item updated",
 | 
			
		||||
  "ToastLibraryCreateFailed": "Failed to create library",
 | 
			
		||||
  "ToastLibraryCreateSuccess": "Library \"{0}\" created",
 | 
			
		||||
  "ToastLibraryDeleteFailed": "Failed to delete library",
 | 
			
		||||
@ -846,32 +899,78 @@
 | 
			
		||||
  "ToastLibraryScanStarted": "Library scan started",
 | 
			
		||||
  "ToastLibraryUpdateFailed": "Failed to update library",
 | 
			
		||||
  "ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
 | 
			
		||||
  "ToastNameEmailRequired": "Name and email are required",
 | 
			
		||||
  "ToastNameRequired": "Name is required",
 | 
			
		||||
  "ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"",
 | 
			
		||||
  "ToastNewUserCreatedSuccess": "New account created",
 | 
			
		||||
  "ToastNewUserLibraryError": "Must select at least one library",
 | 
			
		||||
  "ToastNewUserPasswordError": "Must have a password, only root user can have an empty password",
 | 
			
		||||
  "ToastNewUserTagError": "Must select at least one tag",
 | 
			
		||||
  "ToastNewUserUsernameError": "Enter a username",
 | 
			
		||||
  "ToastNoUpdatesNecessary": "No updates necessary",
 | 
			
		||||
  "ToastNotificationCreateFailed": "Failed to create notification",
 | 
			
		||||
  "ToastNotificationDeleteFailed": "Failed to delete notification",
 | 
			
		||||
  "ToastNotificationFailedMaximum": "Max failed attempts must be >= 0",
 | 
			
		||||
  "ToastNotificationQueueMaximum": "Max notification queue must be >= 0",
 | 
			
		||||
  "ToastNotificationSettingsUpdateFailed": "Failed to update notification settings",
 | 
			
		||||
  "ToastNotificationSettingsUpdateSuccess": "Notification settings updated",
 | 
			
		||||
  "ToastNotificationTestTriggerFailed": "Failed to trigger test notification",
 | 
			
		||||
  "ToastNotificationTestTriggerSuccess": "Triggered test notification",
 | 
			
		||||
  "ToastNotificationUpdateFailed": "Failed to update notification",
 | 
			
		||||
  "ToastNotificationUpdateSuccess": "Notification updated",
 | 
			
		||||
  "ToastPlaylistCreateFailed": "Failed to create playlist",
 | 
			
		||||
  "ToastPlaylistCreateSuccess": "Playlist created",
 | 
			
		||||
  "ToastPlaylistRemoveFailed": "Failed to remove playlist",
 | 
			
		||||
  "ToastPlaylistRemoveSuccess": "Playlist removed",
 | 
			
		||||
  "ToastPlaylistUpdateFailed": "Failed to update playlist",
 | 
			
		||||
  "ToastPlaylistUpdateSuccess": "Playlist updated",
 | 
			
		||||
  "ToastPodcastCreateFailed": "Failed to create podcast",
 | 
			
		||||
  "ToastPodcastCreateSuccess": "Podcast created successfully",
 | 
			
		||||
  "ToastPodcastGetFeedFailed": "Failed to get podcast feed",
 | 
			
		||||
  "ToastPodcastNoEpisodesInFeed": "No episodes found in RSS feed",
 | 
			
		||||
  "ToastPodcastNoRssFeed": "Podcast does not have an RSS feed",
 | 
			
		||||
  "ToastProviderCreatedFailed": "Failed to add provider",
 | 
			
		||||
  "ToastProviderCreatedSuccess": "New provider added",
 | 
			
		||||
  "ToastProviderNameAndUrlRequired": "Name and Url required",
 | 
			
		||||
  "ToastProviderRemoveSuccess": "Provider removed",
 | 
			
		||||
  "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
 | 
			
		||||
  "ToastRSSFeedCloseSuccess": "RSS feed closed",
 | 
			
		||||
  "ToastRemoveFailed": "Failed to remove",
 | 
			
		||||
  "ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
 | 
			
		||||
  "ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
 | 
			
		||||
  "ToastRemoveItemsWithIssuesFailed": "Failed to remove library items with issues",
 | 
			
		||||
  "ToastRemoveItemsWithIssuesSuccess": "Removed library items with issues",
 | 
			
		||||
  "ToastRenameFailed": "Failed to rename",
 | 
			
		||||
  "ToastRescanFailed": "Re-Scan Failed for {0}",
 | 
			
		||||
  "ToastRescanRemoved": "Re-Scan complete item was removed",
 | 
			
		||||
  "ToastRescanUpToDate": "Re-Scan complete item was up to date",
 | 
			
		||||
  "ToastRescanUpdated": "Re-Scan complete item was updated",
 | 
			
		||||
  "ToastScanFailed": "Failed to scan library item",
 | 
			
		||||
  "ToastSelectAtLeastOneUser": "Select at least one user",
 | 
			
		||||
  "ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
 | 
			
		||||
  "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
 | 
			
		||||
  "ToastSeriesUpdateFailed": "Series update failed",
 | 
			
		||||
  "ToastSeriesUpdateSuccess": "Series update success",
 | 
			
		||||
  "ToastServerSettingsUpdateFailed": "Failed to update server settings",
 | 
			
		||||
  "ToastServerSettingsUpdateSuccess": "Server settings updated",
 | 
			
		||||
  "ToastSessionCloseFailed": "Failed to close session",
 | 
			
		||||
  "ToastSessionDeleteFailed": "Failed to delete session",
 | 
			
		||||
  "ToastSessionDeleteSuccess": "Session deleted",
 | 
			
		||||
  "ToastSlugMustChange": "Slug contains invalid characters",
 | 
			
		||||
  "ToastSlugRequired": "Slug is required",
 | 
			
		||||
  "ToastSocketConnected": "Socket connected",
 | 
			
		||||
  "ToastSocketDisconnected": "Socket disconnected",
 | 
			
		||||
  "ToastSocketFailedToConnect": "Socket failed to connect",
 | 
			
		||||
  "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
 | 
			
		||||
  "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
 | 
			
		||||
  "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
 | 
			
		||||
  "ToastTitleRequired": "Title is required",
 | 
			
		||||
  "ToastUnknownError": "Unknown error",
 | 
			
		||||
  "ToastUnlinkOpenIdFailed": "Failed to unlink user from OpenID",
 | 
			
		||||
  "ToastUnlinkOpenIdSuccess": "User unlinked from OpenID",
 | 
			
		||||
  "ToastUserDeleteFailed": "Failed to delete user",
 | 
			
		||||
  "ToastUserDeleteSuccess": "User deleted"
 | 
			
		||||
  "ToastUserDeleteSuccess": "User deleted",
 | 
			
		||||
  "ToastUserPasswordChangeSuccess": "Password changed successfully",
 | 
			
		||||
  "ToastUserPasswordMismatch": "Passwords do not match",
 | 
			
		||||
  "ToastUserPasswordMustChange": "New password cannot match old password",
 | 
			
		||||
  "ToastUserRootRequireName": "Must enter a root username"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user