This commit is contained in:
advplyr 2024-08-31 13:27:54 -05:00
commit 4c8b91e9d9
59 changed files with 330 additions and 237 deletions

View File

@ -332,13 +332,13 @@ export default {
libraryItemIds: this.selectedMediaItems.map((i) => i.id) libraryItemIds: this.selectedMediaItems.map((i) => i.id)
}) })
.then(() => { .then(() => {
this.$toast.success('Batch delete success') this.$toast.success(this.$strings.ToastBatchDeleteSuccess)
this.$store.commit('globals/resetSelectedMediaItems', []) this.$store.commit('globals/resetSelectedMediaItems', [])
this.$eventBus.$emit('bookshelf_clear_selection') this.$eventBus.$emit('bookshelf_clear_selection')
}) })
.catch((error) => { .catch((error) => {
console.error('Batch delete failed', error) console.error('Batch delete failed', error)
this.$toast.error('Batch delete failed') this.$toast.error(this.$strings.ToastBatchDeleteFailed)
}) })
.finally(() => { .finally(() => {
this.$store.commit('setProcessingBatch', false) this.$store.commit('setProcessingBatch', false)

View File

@ -473,11 +473,11 @@ export default {
this.$axios this.$axios
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`) .$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
.then(() => { .then(() => {
this.$toast.success('Series re-added to continue listening') this.$toast.success(this.$strings.ToastItemUpdateSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to re-add series to continue listening', 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(() => { .finally(() => {
this.processingSeries = false this.processingSeries = false
@ -504,7 +504,7 @@ export default {
}) })
if (!response) { if (!response) {
console.error(`Author ${author.name} not found`) 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) { } else if (response.updated) {
if (response.author.imagePath) console.log(`Author ${response.author.name} was 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)`) else console.log(`Author ${response.author.name} was updated (no image found)`)
@ -522,13 +522,13 @@ export default {
this.$axios this.$axios
.$delete(`/api/libraries/${this.currentLibraryId}/issues`) .$delete(`/api/libraries/${this.currentLibraryId}/issues`)
.then(() => { .then(() => {
this.$toast.success('Removed library items with issues') this.$toast.success(this.$strings.ToastRemoveItemsWithIssuesSuccess)
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`) this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
this.$store.dispatch('libraries/fetch', this.currentLibraryId) this.$store.dispatch('libraries/fetch', this.currentLibraryId)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove library items with issues', 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(() => { .finally(() => {
this.processingIssues = false this.processingIssues = false

View File

@ -346,14 +346,14 @@ export default {
}, },
displaySortLine() { displaySortLine() {
if (this.collapsedSeries) return null if (this.collapsedSeries) return null
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat) if (this.orderBy === 'mtimeMs') return this.$getString('LabelFileModifiedDate', [this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)])
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat) if (this.orderBy === 'birthtimeMs') return this.$getString('LabelFileBornDate', [this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)])
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat) if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt, this.dateFormat)])
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false) if (this.orderBy === 'media.duration') return this.$strings.LabelDuration + ': ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) if (this.orderBy === 'size') return this.$strings.LabelSize + ': ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes` if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} ` + this.$strings.LabelEpisodes
if (this.orderBy === 'media.metadata.publishedYear') { 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 '\u00A0'
} }
return null return null
@ -710,7 +710,7 @@ export default {
toggleFinished(confirmed = false) { toggleFinished(confirmed = false) {
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) { if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
const payload = { const payload = {
message: `Are you sure you want to mark "${this.displayTitle}" as finished?`, message: this.$getString('MessageConfirmMarkItemFinished', [this.displayTitle]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.toggleFinished(true) this.toggleFinished(true)
@ -755,18 +755,18 @@ export default {
.then((data) => { .then((data) => {
var result = data.result var result = data.result
if (!result) { if (!result) {
this.$toast.error(`Re-Scan Failed for "${this.title}"`) this.$toast.error(this.$getString('ToastRescanFailed', [this.displayTitle]))
} else if (result === 'UPDATED') { } else if (result === 'UPDATED') {
this.$toast.success(`Re-Scan complete item was updated`) this.$toast.success(this.$strings.ToastRescanUpdated)
} else if (result === 'UPTODATE') { } 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') { } else if (result === 'REMOVED') {
this.$toast.error(`Re-Scan complete item was removed`) this.$toast.error(this.$strings.ToastRescanRemoved)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to scan library item', error) console.error('Failed to scan library item', error)
this.$toast.error('Failed to scan library item') this.$toast.error(this.$strings.ToastScanFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -823,7 +823,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove series from home', error) console.error('Failed to remove series from home', error)
this.$toast.error('Failed to update user') this.$toast.error(this.$strings.ToastFailedToUpdateUser)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -841,7 +841,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to hide item from home', error) console.error('Failed to hide item from home', error)
this.$toast.error('Failed to update user') this.$toast.error(this.$strings.ToastFailedToUpdateUser)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -856,7 +856,7 @@ export default {
episodeId: this.recentEpisode.id, episodeId: this.recentEpisode.id,
title: this.recentEpisode.title, title: this.recentEpisode.title,
subtitle: this.mediaMetadata.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, duration: this.recentEpisode.audioFile.duration || null,
coverPath: this.media.coverPath || null coverPath: this.media.coverPath || null
} }
@ -906,11 +906,11 @@ export default {
axios axios
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
.then(() => { .then(() => {
this.$toast.success('Item deleted') this.$toast.success(this.$strings.ToastItemDeletedSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete item', error) console.error('Failed to delete item', error)
this.$toast.error('Failed to delete item') this.$toast.error(this.$strings.ToastItemDeletedFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -1016,7 +1016,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.mediaMetadata.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, duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null coverPath: this.media.coverPath || null
}) })

View File

@ -96,7 +96,7 @@ export default {
displaySortLine() { displaySortLine() {
switch (this.orderBy) { switch (this.orderBy) {
case 'addedAt': case 'addedAt':
return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}` return this.$getString('LabelAddedDate', [this.$formatDate(this.addedAt, this.dateFormat)])
case 'totalDuration': case 'totalDuration':
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}` return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
case 'lastBookUpdated': case 'lastBookUpdated':

View File

@ -4,11 +4,11 @@
<p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p> <p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
<div class="flex-grow" /> <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" @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">Fire & Fail</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-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-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">Enable</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 :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" /> <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 this.$axios
.$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`) .$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
.then(() => { .then(() => {
this.$toast.success('Triggered onTest Event') this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
const errorMsg = error.response ? error.response.data : null 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(() => { .finally(() => {
this.testing = false this.testing = false
@ -91,7 +91,7 @@ export default {
// End testing functions // End testing functions
sendTestClick() { sendTestClick() {
const payload = { const payload = {
message: `Trigger this notification with test data?`, message: this.$strings.MessageConfirmNotificationTestTrigger,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.sendTest() this.sendTest()
@ -106,12 +106,12 @@ export default {
this.$axios this.$axios
.$get(`/api/notifications/${this.notification.id}/test`) .$get(`/api/notifications/${this.notification.id}/test`)
.then(() => { .then(() => {
this.$toast.success('Triggered test notification') this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
const errorMsg = error.response ? error.response.data : null 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(() => { .finally(() => {
this.sendingTest = false this.sendingTest = false
@ -127,11 +127,10 @@ export default {
.$patch(`/api/notifications/${this.notification.id}`, payload) .$patch(`/api/notifications/${this.notification.id}`, payload)
.then((updatedSettings) => { .then((updatedSettings) => {
this.$emit('update', updatedSettings) this.$emit('update', updatedSettings)
this.$toast.success('Notification enabled')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update notification', error) console.error('Failed to update notification', error)
this.$toast.error('Failed to update notification') this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.enabling = false this.enabling = false
@ -139,7 +138,7 @@ export default {
}, },
deleteNotificationClick() { deleteNotificationClick() {
const payload = { const payload = {
message: `Are you sure you want to delete this notification?`, message: this.$strings.MessageConfirmDeleteNotification,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.deleteNotification() this.deleteNotification()
@ -155,11 +154,10 @@ export default {
.$delete(`/api/notifications/${this.notification.id}`) .$delete(`/api/notifications/${this.notification.id}`)
.then((updatedSettings) => { .then((updatedSettings) => {
this.$emit('update', updatedSettings) this.$emit('update', updatedSettings)
this.$toast.success('Deleted notification')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.$toast.error('Failed to delete notification') this.$toast.error(this.$strings.ToastNotificationDeleteFailed)
}) })
.finally(() => { .finally(() => {
this.deleting = false this.deleting = false

View File

@ -111,7 +111,7 @@
</div> </div>
<div class="flex pt-4 px-2"> <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> <ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn> <ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
@ -212,19 +212,19 @@ export default {
}, },
unlinkOpenID() { unlinkOpenID() {
const payload = { const payload = {
message: 'Are you sure you want to unlink this user from OpenID?', message: this.$strings.MessageConfirmUnlinkOpenId,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.unlinkingFromOpenID = true this.unlinkingFromOpenID = true
this.$axios this.$axios
.$patch(`/api/users/${this.account.id}/openid-unlink`) .$patch(`/api/users/${this.account.id}/openid-unlink`)
.then(() => { .then(() => {
this.$toast.success('User unlinked from OpenID') this.$toast.success(this.$strings.ToastUnlinkOpenIdSuccess)
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to unlink user from OpenID', 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(() => { .finally(() => {
this.unlinkingFromOpenID = false this.unlinkingFromOpenID = false
@ -265,15 +265,15 @@ export default {
}, },
submitForm() { submitForm() {
if (!this.newUser.username) { if (!this.newUser.username) {
this.$toast.error('Enter a username') this.$toast.error(this.$strings.ToastNewUserUsernameError)
return return
} }
if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) { 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 return
} }
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) { 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 return
} }
@ -313,12 +313,12 @@ export default {
this.processing = false this.processing = false
console.error('Failed to update account', error) console.error('Failed to update account', error)
var errMsg = error.response ? error.response.data || '' : '' var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || 'Failed to update account') this.$toast.error(errMsg || this.$strings.ToastFailedToUpdateAccount)
}) })
}, },
submitCreateAccount() { submitCreateAccount() {
if (!this.newUser.password) { 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 return
} }
@ -329,9 +329,9 @@ export default {
.then((data) => { .then((data) => {
this.processing = false this.processing = false
if (data.error) { if (data.error) {
this.$toast.error(`Failed to create account: ${data.error}`) this.$toast.error(this.$strings.ToastNewUserCreatedFailed + ': ' + data.error)
} else { } else {
this.$toast.success('New account created') this.$toast.success(this.$strings.ToastNewUserCreatedSuccess)
this.show = false this.show = false
} }
}) })

View File

@ -2,7 +2,7 @@
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing"> <modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
<template #outer> <template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> <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> </div>
</template> </template>
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
@ -20,7 +20,7 @@
<ui-text-input-with-label v-model="newUrl" label="URL" /> <ui-text-input-with-label v-model="newUrl" label="URL" />
</div> </div>
<div class="w-full mb-2 p-1"> <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>
<div class="flex px-1 pt-4"> <div class="flex px-1 pt-4">
<div class="flex-grow" /> <div class="flex-grow" />
@ -67,7 +67,7 @@ export default {
methods: { methods: {
submitForm() { submitForm() {
if (!this.newName || !this.newUrl) { if (!this.newName || !this.newUrl) {
this.$toast.error('Must add name and url') this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired)
return return
} }
@ -81,13 +81,13 @@ export default {
}) })
.then((data) => { .then((data) => {
this.$emit('added', data.provider) this.$emit('added', data.provider)
this.$toast.success('New provider added') this.$toast.success(this.$strings.ToastProviderCreatedSuccess)
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
const errorMsg = error.response?.data || 'Unknown error' const errorMsg = error.response?.data || 'Unknown error'
console.error('Failed to add provider', error) console.error('Failed to add provider', error)
this.$toast.error('Failed to add provider: ' + errorMsg) this.$toast.error(this.$strings.ToastProviderCreatedFailed + ': ' + errorMsg)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false

View File

@ -4,7 +4,7 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p> <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-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>
<div class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full h-px bg-white bg-opacity-10 my-4" />
@ -159,7 +159,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to get ffprobe data', error) console.error('Failed to get ffprobe data', error)
this.$toast.error('FFProbe failed') this.$toast.error(this.$strings.ToastFailedToLoadData)
}) })
.finally(() => { .finally(() => {
this.probingFile = false this.probingFile = false

View File

@ -9,7 +9,7 @@
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" /> <widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
<div class="flex items-center justify-end"> <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>
</div> </div>
</modals-modal> </modals-modal>

View File

@ -94,7 +94,7 @@ export default {
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess) this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
}) })
.catch((error) => { .catch((error) => {
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
console.error(error) console.error(error)
}) })
this.show = false this.show = false

View File

@ -100,7 +100,7 @@
<div class="flex items-center"> <div class="flex items-center">
<ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn> <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>
</div> </div>
</modals-modal> </modals-modal>
@ -206,14 +206,13 @@ export default {
this.$axios this.$axios
.$post(`/api/session/${this._session.id}/close`) .$post(`/api/session/${this._session.id}/close`)
.then(() => { .then(() => {
this.$toast.success('Session closed')
this.show = false this.show = false
this.$emit('closedSession') this.$emit('closedSession')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to close session', error) console.error('Failed to close session', error)
const errMsg = error.response?.data || '' const errMsg = error.response?.data || ''
this.$toast.error(errMsg || 'Failed to close open session') this.$toast.error(errMsg || this.$strings.ToastSessionCloseFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false

View File

@ -165,7 +165,7 @@ export default {
}, },
openShare() { openShare() {
if (!this.newShareSlug) { if (!this.newShareSlug) {
this.$toast.error('Slug is required') this.$toast.error(this.$strings.ToastSlugRequired)
return return
} }
const payload = { const payload = {

View File

@ -15,7 +15,7 @@
</template> </template>
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime"> <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-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> </form>
</div> </div>
<div v-if="timerSet" class="w-full p-4"> <div v-if="timerSet" class="w-full p-4">

View File

@ -78,14 +78,13 @@ export default {
if (data.error) { if (data.error) {
this.$toast.error(data.error) this.$toast.error(data.error)
} else { } else {
this.$toast.success('Cover Uploaded')
this.resetCoverPreview() this.resetCoverPreview()
} }
this.processingUpload = false this.processingUpload = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', 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.$toast.error(errorMsg)
this.processingUpload = false 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) => { 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) 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) this.$toast.error(errorMsg)
return false return false
}) })

View File

@ -116,12 +116,12 @@ export default {
this.$axios this.$axios
.$delete(`/api/authors/${this.authorId}`) .$delete(`/api/authors/${this.authorId}`)
.then(() => { .then(() => {
this.$toast.success('Author removed') this.$toast.success(this.$strings.ToastAuthorRemoveSuccess)
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove author', error) console.error('Failed to remove author', error)
this.$toast.error('Failed to remove author') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -141,7 +141,7 @@ export default {
} }
}) })
if (!Object.keys(updatePayload).length) { if (!Object.keys(updatePayload).length) {
this.$toast.info(this.$strings.MessageNoUpdateNecessary) this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return return
} }
this.processing = true this.processing = true
@ -158,7 +158,7 @@ export default {
} else if (result.merged) { } else if (result.merged) {
this.$toast.success(this.$strings.ToastAuthorUpdateMerged) this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
this.show = false this.show = false
} else this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) } else this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
this.processing = false this.processing = false
}, },
@ -174,7 +174,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -182,7 +182,7 @@ export default {
}, },
submitUploadCover() { submitUploadCover() {
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) { if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
this.$toast.error('Invalid image url') this.$toast.error(this.$strings.ToastInvalidImageUrl)
return return
} }
@ -194,14 +194,14 @@ export default {
.$post(`/api/authors/${this.authorId}/image`, updatePayload) .$post(`/api/authors/${this.authorId}/image`, updatePayload)
.then((data) => { .then((data) => {
this.imageUrl = '' this.imageUrl = ''
this.$toast.success('Author image updated') this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
this.authorCopy.updatedAt = data.author.updatedAt this.authorCopy.updatedAt = data.author.updatedAt
this.authorCopy.imagePath = data.author.imagePath this.authorCopy.imagePath = data.author.imagePath
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', 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(() => { .finally(() => {
this.processing = false this.processing = false
@ -209,7 +209,7 @@ export default {
}, },
async searchAuthor() { async searchAuthor() {
if (!this.authorCopy.name && !this.authorCopy.asin) { if (!this.authorCopy.name && !this.authorCopy.asin) {
this.$toast.error('Must enter an author name') this.$toast.error(this.$strings.ToastNameRequired)
return return
} }
this.processing = true this.processing = true
@ -228,17 +228,19 @@ export default {
return null return null
}) })
if (!response) { if (!response) {
this.$toast.error('Author not found') this.$toast.error(this.$strings.ToastAuthorSearchNotFound)
} else if (response.updated) { } else if (response.updated) {
if (response.author.imagePath) { if (response.author.imagePath) {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound) } else {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
}
this.authorCopy = { this.authorCopy = {
...response.author ...response.author
} }
} else { } else {
this.$toast.info('No updates were made for Author') this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
this.processing = false this.processing = false
} }

View File

@ -143,7 +143,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove books from collection', 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 this.processing = false
}) })
} else { } else {
@ -157,7 +157,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove book from collection', 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 this.processing = false
}) })
} }
@ -172,12 +172,12 @@ export default {
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds }) .$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Books added to collection`, updatedCollection) console.log(`Books added to collection`, updatedCollection)
this.$toast.success('Books added to collection') this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to add books to collection', 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 this.processing = false
}) })
} else { } else {
@ -187,12 +187,12 @@ export default {
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId }) .$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Book added to collection`, updatedCollection) console.log(`Book added to collection`, updatedCollection)
this.$toast.success('Book added to collection') this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to add book to collection', 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 this.processing = false
}) })
} }
@ -221,7 +221,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to create collection', error) console.error('Failed to create collection', error)
var errMsg = error.response ? error.response.data || '' : '' 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 this.processing = false
}) })
} }

View File

@ -106,7 +106,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to remove collection', error) console.error('Failed to remove collection', error)
this.processing = false this.processing = false
this.$toast.error(this.$strings.ToastCollectionRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
} }
}, },
@ -115,7 +115,7 @@ export default {
return return
} }
if (!this.newCollectionName) { if (!this.newCollectionName) {
return this.$toast.error('Collection must have a name') return this.$toast.error(this.$strings.ToastNameRequired)
} }
this.processing = true this.processing = true

View File

@ -125,12 +125,12 @@ export default {
this.$refs.ereaderEmailInput.blur() this.$refs.ereaderEmailInput.blur()
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) { if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
this.$toast.error('Name and email required') this.$toast.error(this.$strings.ToastNameEmailRequired)
return return
} }
if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) { 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 return
} }
if (this.newDevice.availabilityOption !== 'specificUsers') { if (this.newDevice.availabilityOption !== 'specificUsers') {
@ -142,14 +142,14 @@ export default {
if (!this.ereaderDevice) { if (!this.ereaderDevice) {
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) { 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 return
} }
this.submitCreate() this.submitCreate()
} else { } else {
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) { 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 return
} }
@ -174,12 +174,11 @@ export default {
.$post(`/api/emails/ereader-devices`, payload) .$post(`/api/emails/ereader-devices`, payload)
.then((data) => { .then((data) => {
this.$emit('update', data.ereaderDevices) this.$emit('update', data.ereaderDevices)
this.$toast.success('Device updated')
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update device', error) console.error('Failed to update device', error)
this.$toast.error('Failed to update device') this.$toast.error(this.$strings.ToastDeviceUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -201,12 +200,11 @@ export default {
.$post('/api/emails/ereader-devices', payload) .$post('/api/emails/ereader-devices', payload)
.then((data) => { .then((data) => {
this.$emit('update', data.ereaderDevices || []) this.$emit('update', data.ereaderDevices || [])
this.$toast.success('Device added')
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to add device', error) console.error('Failed to add device', error)
this.$toast.error('Failed to add device') this.$toast.error(this.$strings.ToastDeviceAddFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false

View File

@ -194,7 +194,6 @@ export default {
if (data.error) { if (data.error) {
this.$toast.error(data.error) this.$toast.error(data.error)
} else { } else {
this.$toast.success('Cover Uploaded')
this.resetCoverPreview() this.resetCoverPreview()
} }
this.processingUpload = false this.processingUpload = false
@ -204,7 +203,7 @@ export default {
if (error.response && error.response.data) { if (error.response && error.response.data) {
this.$toast.error(error.response.data) this.$toast.error(error.response.data)
} else { } else {
this.$toast.error('Oops, something went wrong...') this.$toast.error(this.$strings.ToastUnknownError)
} }
this.processingUpload = false this.processingUpload = false
}) })
@ -255,7 +254,7 @@ export default {
}, },
async updateCover(cover) { async updateCover(cover) {
if (!cover.startsWith('http:') && !cover.startsWith('https:')) { if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
this.$toast.error('Invalid URL') this.$toast.error(this.$strings.ToastInvalidUrl)
return return
} }
@ -264,11 +263,10 @@ export default {
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }) .$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
.then(() => { .then(() => {
this.imageUrl = '' this.imageUrl = ''
this.$toast.success('Update Successful')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update cover', 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(() => { .finally(() => {
this.isProcessing = false this.isProcessing = false
@ -308,12 +306,9 @@ export default {
this.isProcessing = true this.isProcessing = true
this.$axios this.$axios
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path }) .$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
.then(() => {
this.$toast.success('Update Successful')
})
.catch((error) => { .catch((error) => {
console.error('Failed to set local cover', 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(() => { .finally(() => {
this.isProcessing = false this.isProcessing = false

View File

@ -92,7 +92,7 @@ export default {
var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName() var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
if (!title) { if (!title) {
this.$toast.error('Must have a title for quick match') this.$toast.error(this.$strings.ToastTitleRequired)
return return
} }
this.quickMatching = true this.quickMatching = true
@ -108,9 +108,9 @@ export default {
if (res.warning) { if (res.warning) {
this.$toast.warning(res.warning) this.$toast.warning(res.warning)
} else if (res.updated) { } else if (res.updated) {
this.$toast.success('Item details updated') this.$toast.success(this.$strings.ToastNoUpdatesNecessary)
} else { } else {
this.$toast.info('No updates were made') this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
} }
}) })
.catch((error) => { .catch((error) => {
@ -128,18 +128,18 @@ export default {
this.rescanning = false this.rescanning = false
var result = data.result var result = data.result
if (!result) { if (!result) {
this.$toast.error(`Re-Scan Failed for "${this.title}"`) this.$toast.error(this.$getString('ToastRescanFailed', [this.title]))
} else if (result === 'UPDATED') { } else if (result === 'UPDATED') {
this.$toast.success(`Re-Scan complete item was updated`) this.$toast.success(this.$strings.ToastRescanUpdated)
} else if (result === 'UPTODATE') { } 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') { } else if (result === 'REMOVED') {
this.$toast.error(`Re-Scan complete item was removed`) this.$toast.error(this.$strings.ToastRescanRemoved)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to scan library item', 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 this.rescanning = false
}) })
}, },
@ -156,7 +156,7 @@ export default {
} }
var updatedDetails = this.$refs.itemDetailsEdit.getDetails() var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
if (!updatedDetails.hasChanges) { if (!updatedDetails.hasChanges) {
this.$toast.info('No changes were made') this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
return false return false
} }
return this.updateDetails(updatedDetails) return this.updateDetails(updatedDetails)
@ -170,7 +170,7 @@ export default {
this.isProcessing = false this.isProcessing = false
if (updateResult) { if (updateResult) {
if (updateResult.updated) { if (updateResult.updated) {
this.$toast.success('Item details updated') this.$toast.success(this.$strings.MessageItemDetailsUpdated)
return true return true
} else { } else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)

View File

@ -397,7 +397,7 @@ export default {
}, },
submitSearch() { submitSearch() {
if (!this.searchTitle) { if (!this.searchTitle) {
this.$toast.warning('Search title is required') this.$toast.warning(this.$strings.ToastTitleRequired)
return return
} }
this.persistProvider() this.persistProvider()
@ -618,7 +618,7 @@ export default {
if (updateResult.updated) { if (updateResult.updated) {
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess) this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
} else { } else {
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded) this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
this.clearSelectedMatch() this.clearSelectedMatch()
this.$emit('selectTab', 'details') this.$emit('selectTab', 'details')

View File

@ -163,7 +163,7 @@ export default {
this.isProcessing = false this.isProcessing = false
if (updateResult) { if (updateResult) {
if (updateResult.updated) { if (updateResult.updated) {
this.$toast.success('Item details updated') this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
return true return true
} else { } else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)

View File

@ -156,7 +156,7 @@ export default {
}, },
validate() { validate() {
if (!this.libraryCopy.name) { if (!this.libraryCopy.name) {
this.$toast.error('Library must have a name') this.$toast.error(this.$strings.ToastNameRequired)
return false return false
} }
if (!this.libraryCopy.folders.length) { if (!this.libraryCopy.folders.length) {
@ -205,7 +205,7 @@ export default {
submitUpdateLibrary() { submitUpdateLibrary() {
var newLibraryPayload = this.getLibraryUpdatePayload() var newLibraryPayload = this.getLibraryUpdatePayload()
if (!Object.keys(newLibraryPayload).length) { if (!Object.keys(newLibraryPayload).length) {
this.$toast.info('No updates are necessary') this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return return
} }

View File

@ -162,7 +162,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to get filesystem paths', error) console.error('Failed to get filesystem paths', error)
this.$toast.error('Failed to get filesystem paths') this.$toast.error(this.$strings.ToastFailedToLoadData)
return [] return []
}) })
.finally(() => { .finally(() => {

View File

@ -86,7 +86,7 @@ export default {
return this.selectedEventData && this.selectedEventData.requiresLibrary return this.selectedEventData && this.selectedEventData.requiresLibrary
}, },
title() { title() {
return this.isNew ? 'Create Notification' : 'Update Notification' return this.isNew ? this.$strings.HeaderNotificationCreate : this.$strings.HeaderNotificationUpdate
}, },
availableVariables() { availableVariables() {
return this.selectedEventData ? this.selectedEventData.variables || null : null return this.selectedEventData ? this.selectedEventData.variables || null : null
@ -106,7 +106,7 @@ export default {
this.$refs.urlsInput?.forceBlur() this.$refs.urlsInput?.forceBlur()
if (!this.newNotification.urls.length) { if (!this.newNotification.urls.length) {
this.$toast.error('Must enter an Apprise URL') this.$toast.error(this.$strings.ToastAppriseUrlRequired)
return return
} }
@ -127,12 +127,12 @@ export default {
.$patch(`/api/notifications/${payload.id}`, payload) .$patch(`/api/notifications/${payload.id}`, payload)
.then((updatedSettings) => { .then((updatedSettings) => {
this.$emit('update', updatedSettings) this.$emit('update', updatedSettings)
this.$toast.success('Notification updated') this.$toast.success(this.$strings.ToastNotificationUpdateSuccess)
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update notification', error) console.error('Failed to update notification', error)
this.$toast.error('Failed to update notification') this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -149,12 +149,11 @@ export default {
.$post('/api/notifications', payload) .$post('/api/notifications', payload)
.then((updatedSettings) => { .then((updatedSettings) => {
this.$emit('update', updatedSettings) this.$emit('update', updatedSettings)
this.$toast.success('Notification created')
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to create notification', error) console.error('Failed to create notification', error)
this.$toast.error('Failed to create notification') this.$toast.error(this.$strings.ToastNotificationCreateFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false

View File

@ -130,12 +130,12 @@ export default {
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects }) .$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
.then((updatedPlaylist) => { .then((updatedPlaylist) => {
console.log(`Items removed from playlist`, updatedPlaylist) console.log(`Items removed from playlist`, updatedPlaylist)
this.$toast.success('Playlist item(s) removed') this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove items from playlist', 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 this.processing = false
}) })
}, },
@ -148,12 +148,12 @@ export default {
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects }) .$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
.then((updatedPlaylist) => { .then((updatedPlaylist) => {
console.log(`Items added to playlist`, updatedPlaylist) console.log(`Items added to playlist`, updatedPlaylist)
this.$toast.success('Items added to playlist') this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to add items to playlist', 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 this.processing = false
}) })
}, },
@ -174,14 +174,14 @@ export default {
.$post('/api/playlists', newPlaylist) .$post('/api/playlists', newPlaylist)
.then((data) => { .then((data) => {
console.log('New playlist created', 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.processing = false
this.newPlaylistName = '' this.newPlaylistName = ''
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to create playlist', error) console.error('Failed to create playlist', error)
var errMsg = error.response ? error.response.data || '' : '' 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 this.processing = false
}) })
} }

View File

@ -86,7 +86,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to remove playlist', error) console.error('Failed to remove playlist', error)
this.processing = false this.processing = false
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
} }
}, },
@ -95,7 +95,7 @@ export default {
return return
} }
if (!this.newPlaylistName) { if (!this.newPlaylistName) {
return this.$toast.error('Playlist must have a name') return this.$toast.error(this.$strings.ToastNameRequired)
} }
this.processing = true this.processing = true

View File

@ -142,7 +142,7 @@ export default {
const updatedDetails = this.getUpdatePayload() const updatedDetails = this.getUpdatePayload()
if (!Object.keys(updatedDetails).length) { if (!Object.keys(updatedDetails).length) {
this.$toast.info('No changes were made') this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return false return false
} }
return this.updateDetails(updatedDetails) return this.updateDetails(updatedDetails)

View File

@ -105,7 +105,7 @@ export default {
} }
const updatePayload = this.getUpdatePayload(episodeData) const updatePayload = this.getUpdatePayload(episodeData)
if (!Object.keys(updatePayload).length) { 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) console.log('Episode update payload', updatePayload)
@ -126,7 +126,7 @@ export default {
}, },
submitForm() { submitForm() {
if (!this.episodeTitle || !this.episodeTitle.length) { if (!this.episodeTitle || !this.episodeTitle.length) {
this.$toast.error('Must enter an episode title') this.$toast.error(this.$strings.ToastTitleRequired)
return return
} }
this.searchedTitle = this.episodeTitle this.searchedTitle = this.episodeTitle

View File

@ -121,14 +121,14 @@ export default {
methods: { methods: {
openFeed() { openFeed() {
if (!this.newFeedSlug) { if (!this.newFeedSlug) {
this.$toast.error('Must set a feed slug') this.$toast.error(this.$strings.ToastSlugRequired)
return return
} }
const sanitized = this.$sanitizeSlug(this.newFeedSlug) const sanitized = this.$sanitizeSlug(this.newFeedSlug)
if (this.newFeedSlug !== sanitized) { if (this.newFeedSlug !== sanitized) {
this.newFeedSlug = sanitized this.newFeedSlug = sanitized
this.$toast.warning('Slug had to be modified - Run again') this.$toast.warning(this.$strings.ToastSlugMustChange)
return return
} }

View File

@ -261,7 +261,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to share', error) console.error('Failed to share', error)
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message) this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
} }
}) })
} else { } else {

View File

@ -237,7 +237,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to share', error) console.error('Failed to share', error)
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message) this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
} }
}) })
} else { } else {

View File

@ -167,7 +167,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to share', error) console.error('Failed to share', error)
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message) this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
} }
}) })
} else { } else {

View File

@ -186,7 +186,7 @@ export default {
mounted() { mounted() {
this.loadBackups() this.loadBackups()
if (this.$route.query.backup) { if (this.$route.query.backup) {
this.$toast.success('Backup applied successfully') this.$toast.success(this.$strings.ToastBackupAppliedSuccess)
} }
} }
} }

View File

@ -78,7 +78,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update collection', 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) { editBook(book) {

View File

@ -45,7 +45,7 @@ export default {
methods: { methods: {
removeProvider(provider) { removeProvider(provider) {
const payload = { const payload = {
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`, message: this.$getString('MessageConfirmDeleteMetadataProvider', [provider.name]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.$emit('update:processing', true) this.$emit('update:processing', true)
@ -53,12 +53,12 @@ export default {
this.$axios this.$axios
.$delete(`/api/custom-metadata-providers/${provider.id}`) .$delete(`/api/custom-metadata-providers/${provider.id}`)
.then(() => { .then(() => {
this.$toast.success('Provider removed') this.$toast.success(this.$strings.ToastProviderRemoveSuccess)
this.$emit('removed', provider.id) this.$emit('removed', provider.id)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove provider', error) console.error('Failed to remove provider', error)
this.$toast.error('Failed to remove provider') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.$emit('update:processing', false) this.$emit('update:processing', false)

View File

@ -92,7 +92,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update playlist', error) console.error('Failed to update playlist', error)
this.$toast.error('Failed to save playlist items order') this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
}) })
}, },
init() { init() {

View File

@ -218,12 +218,12 @@ export default {
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess) this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
} else { } else {
console.log(`Item removed from playlist`, updatedPlaylist) console.log(`Item removed from playlist`, updatedPlaylist)
this.$toast.success('Item removed from playlist') this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove item from playlist', 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(() => { .finally(() => {
this.processingRemove = false this.processingRemove = false

View File

@ -270,7 +270,7 @@ export default {
if (data.numEpisodesUpdated) { if (data.numEpisodesUpdated) {
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`) this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
} else { } else {
this.$toast.info('No changes were made') this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
}) })
.catch((error) => { .catch((error) => {
@ -295,7 +295,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.mediaMetadata.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, duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null coverPath: this.media.coverPath || null
} }
@ -372,7 +372,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.mediaMetadata.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, duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null coverPath: this.media.coverPath || null
}) })

View File

@ -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 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>
<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>
</div> </div>
</template> </template>
@ -17,7 +17,12 @@ export default {
props: { props: {
text: { text: {
type: String, type: String,
default: 'Please Wait...' default: null
}
},
computed: {
message() {
return this.text || this.$strings.MessagePleaseWait
} }
} }
} }

View File

@ -117,10 +117,10 @@ export default {
}, },
submitChangePassword() { submitChangePassword() {
if (this.newPassword !== this.confirmPassword) { 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) { 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.changingPassword = true
this.$axios this.$axios
@ -130,16 +130,16 @@ export default {
}) })
.then((res) => { .then((res) => {
if (res.success) { if (res.success) {
this.$toast.success('Password Changed Successfully') this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
this.resetForm() this.resetForm()
} else { } else {
this.$toast.error(res.error || 'Unknown Error') this.$toast.error(res.error || this.$strings.ToastUnknownError)
} }
this.changingPassword = false this.changingPassword = false
}) })
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)
this.$toast.error('Api call failed') this.$toast.error(this.$strings.ToastUnknownError)
this.changingPassword = false this.changingPassword = false
}) })
} }

View File

@ -560,7 +560,7 @@ export default {
.catch((error) => { .catch((error) => {
this.findingChapters = false this.findingChapters = false
console.error('Failed to get chapter data', error) console.error('Failed to get chapter data', error)
this.$toast.error('Failed to find chapters') this.$toast.error(this.$strings.ToastFailedToLoadData)
this.showFindChaptersModal = false this.showFindChaptersModal = false
}) })
}, },
@ -611,7 +611,7 @@ export default {
.$post(`/api/items/${this.libraryItem.id}/chapters`, payload) .$post(`/api/items/${this.libraryItem.id}/chapters`, payload)
.then((data) => { .then((data) => {
if (data.updated) { if (data.updated) {
this.$toast.success('Chapters removed') this.$toast.success(this.$strings.ToastChaptersRemoved)
if (this.previousRoute) { if (this.previousRoute) {
this.$router.push(this.previousRoute) this.$router.push(this.previousRoute)
} else { } else {
@ -623,7 +623,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove chapters', error) console.error('Failed to remove chapters', error)
this.$toast.error('Failed to remove chapters') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.saving = false this.saving = false

View File

@ -331,11 +331,11 @@ export default {
this.$axios this.$axios
.$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`) .$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
.then(() => { .then(() => {
this.$toast.success('Encode canceled') this.$toast.success(this.$strings.ToastEncodeCancelSucces)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to cancel encode', error) console.error('Failed to cancel encode', error)
this.$toast.error('Failed to cancel encode') this.$toast.error(this.$strings.ToastEncodeCancelFailed)
}) })
.finally(() => { .finally(() => {
this.isCancelingEncode = false this.isCancelingEncode = false

View File

@ -366,7 +366,7 @@ export default {
} }
} }
if (!updates.length) { if (!updates.length) {
return this.$toast.warning('No updates were made') return this.$toast.warning(this.$strings.ToastNoUpdatesNecessary)
} }
console.log('Pushing updates', updates) console.log('Pushing updates', updates)

View File

@ -162,7 +162,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to save backup path', 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) this.$toast.error(errorMsg)
}) })
.finally(() => { .finally(() => {
@ -171,11 +171,11 @@ export default {
}, },
updateBackupsSettings() { updateBackupsSettings() {
if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) { if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) {
this.$toast.error('Invalid maximum backup size') this.$toast.error(this.$strings.ToastBackupInvalidMaxSize)
return return
} }
if (isNaN(this.backupsToKeep) || this.backupsToKeep <= 0 || this.backupsToKeep > 99) { 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 return
} }
const updatePayload = { const updatePayload = {

View File

@ -109,7 +109,7 @@
</tr> </tr>
</table> </table>
<div v-else-if="!loading" class="text-center py-4"> <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> </div>
</app-settings-content> </app-settings-content>
@ -199,7 +199,7 @@ export default {
}, },
deleteDeviceClick(device) { deleteDeviceClick(device) {
const payload = { const payload = {
message: `Are you sure you want to delete e-reader device "${device.name}"?`, message: this.$getString('MessageConfirmDeleteDevice', [device.name]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.deleteDevice(device) this.deleteDevice(device)
@ -218,11 +218,10 @@ export default {
.$post(`/api/emails/ereader-devices`, payload) .$post(`/api/emails/ereader-devices`, payload)
.then((data) => { .then((data) => {
this.ereaderDevicesUpdated(data.ereaderDevices) this.ereaderDevicesUpdated(data.ereaderDevices)
this.$toast.success('Device deleted')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete device', error) console.error('Failed to delete device', error)
this.$toast.error('Failed to delete device') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.deletingDeviceName = null this.deletingDeviceName = null
@ -246,11 +245,11 @@ export default {
this.$axios this.$axios
.$post('/api/emails/test') .$post('/api/emails/test')
.then(() => { .then(() => {
this.$toast.success('Test Email Sent') this.$toast.success(this.$strings.ToastDeviceTestEmailSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to send test email', 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) this.$toast.error(errorMsg)
}) })
.finally(() => { .finally(() => {
@ -289,11 +288,11 @@ export default {
this.newSettings = { this.newSettings = {
...data.settings ...data.settings
} }
this.$toast.success('Email settings updated') this.$toast.success(this.$strings.ToastEmailSettingsUpdateSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update email settings', error) console.error('Failed to update email settings', error)
this.$toast.error('Failed to update email settings') this.$toast.error(this.$strings.ToastEmailSettingsUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.savingSettings = false this.savingSettings = false

View File

@ -130,7 +130,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to rename genre', error) console.error('Failed to rename genre', error)
this.$toast.error('Failed to rename genre') this.$toast.error(this.$strings.ToastRenameFailed)
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false
@ -147,7 +147,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove genre', error) console.error('Failed to remove genre', error)
this.$toast.error('Failed to remove genre') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false

View File

@ -126,7 +126,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to rename tag', error) console.error('Failed to rename tag', error)
this.$toast.error('Failed to rename tag') this.$toast.error(this.$strings.ToastRenameFailed)
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false
@ -143,7 +143,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove tag', error) console.error('Failed to remove tag', error)
this.$toast.error('Failed to remove tag') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false

View File

@ -105,12 +105,12 @@ export default {
} }
if (isNaN(this.maxNotificationQueue) || this.maxNotificationQueue <= 0) { if (isNaN(this.maxNotificationQueue) || this.maxNotificationQueue <= 0) {
this.$toast.error('Max notification queue must be >= 0') this.$toast.error(this.$strings.ToastNotificationQueueMaximum)
return false return false
} }
if (isNaN(this.maxFailedAttempts) || this.maxFailedAttempts <= 0) { if (isNaN(this.maxFailedAttempts) || this.maxFailedAttempts <= 0) {
this.$toast.error('Max failed attempts must be >= 0') this.$toast.error(this.$strings.ToastNotificationFailedMaximum)
return false return false
} }
@ -128,11 +128,11 @@ export default {
this.$axios this.$axios
.$patch('/api/notifications', updatePayload) .$patch('/api/notifications', updatePayload)
.then(() => { .then(() => {
this.$toast.success('Notification settings updated') this.$toast.success(this.$strings.ToastNotificationSettingsUpdateSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update notification settings', error) console.error('Failed to update notification settings', error)
this.$toast.error('Failed to update notification settings') this.$toast.error(this.$strings.ToastNotificationSettingsUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.savingSettings = false this.savingSettings = false

View File

@ -290,7 +290,6 @@ export default {
this.$axios this.$axios
.$post(`/api/sessions/batch/delete`, payload) .$post(`/api/sessions/batch/delete`, payload)
.then(() => { .then(() => {
this.$toast.success('Sessions removed')
if (isAllSessions) { if (isAllSessions) {
// If all sessions were removed from the current page then go to the previous page // If all sessions were removed from the current page then go to the previous page
if (this.currentPage > 0) { if (this.currentPage > 0) {
@ -303,7 +302,7 @@ export default {
} }
}) })
.catch((error) => { .catch((error) => {
const errorMsg = error.response?.data || 'Failed to remove sessions' const errorMsg = error.response?.data || this.$strings.ToastRemoveFailed
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
}) })
.finally(() => { .finally(() => {
@ -358,12 +357,13 @@ export default {
}) })
if (!libraryItem) { if (!libraryItem) {
this.$toast.error('Failed to get library item') this.$toast.error(this.$strings.ToastFailedToLoadData)
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) { 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 this.processingGoToTimestamp = false
return return
} }
@ -377,7 +377,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: libraryItem.media.metadata.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, duration: episode.audioFile.duration || null,
coverPath: libraryItem.media.coverPath || null coverPath: libraryItem.media.coverPath || null
} }

View File

@ -127,12 +127,13 @@ export default {
}) })
if (!libraryItem) { if (!libraryItem) {
this.$toast.error('Failed to get library item') this.$toast.error(this.$strings.ToastFailedToLoadData)
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) { 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 this.processingGoToTimestamp = false
return return
} }
@ -146,7 +147,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: libraryItem.media.metadata.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, duration: episode.audioFile.duration || null,
coverPath: libraryItem.media.coverPath || null coverPath: libraryItem.media.coverPath || null
} }

View File

@ -484,23 +484,23 @@ export default {
this.$axios this.$axios
.$get(`/api/podcasts/${this.libraryItemId}/clear-queue`) .$get(`/api/podcasts/${this.libraryItemId}/clear-queue`)
.then(() => { .then(() => {
this.$toast.success('Episode download queue cleared') this.$toast.success(this.$strings.ToastEpisodeDownloadQueueClearSuccess)
this.episodeDownloadQueued = [] this.episodeDownloadQueued = []
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to clear queue', error) console.error('Failed to clear queue', error)
this.$toast.error('Failed to clear queue') this.$toast.error(this.$strings.ToastEpisodeDownloadQueueClearFailed)
}) })
} }
}, },
async findEpisodesClick() { async findEpisodesClick() {
if (!this.mediaMetadata.feedUrl) { 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 this.fetchingRSSFeed = true
var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => { var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => {
console.error('Failed to get feed', error) console.error('Failed to get feed', error)
this.$toast.error('Failed to get podcast feed') this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
return null return null
}) })
this.fetchingRSSFeed = false this.fetchingRSSFeed = false
@ -509,7 +509,7 @@ export default {
console.log('Podcast feed', payload) console.log('Podcast feed', payload)
const podcastfeed = payload.podcast const podcastfeed = payload.podcast
if (!podcastfeed.episodes || !podcastfeed.episodes.length) { if (!podcastfeed.episodes || !podcastfeed.episodes.length) {
this.$toast.info('No episodes found in RSS feed') this.$toast.info(this.$strings.ToastPodcastNoEpisodesInFeed)
return return
} }
@ -578,7 +578,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.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, duration: episode.audioFile.duration || null,
coverPath: this.libraryItem.media.coverPath || null coverPath: this.libraryItem.media.coverPath || null
}) })
@ -622,13 +622,12 @@ export default {
}, },
clearProgressClick() { clearProgressClick() {
if (!this.userMediaProgress) return if (!this.userMediaProgress) return
if (confirm(`Are you sure you want to reset your progress?`)) { if (confirm(this.$strings.MessageConfirmResetProgress)) {
this.resettingProgress = true this.resettingProgress = true
this.$axios this.$axios
.$delete(`/api/me/progress/${this.userMediaProgress.id}`) .$delete(`/api/me/progress/${this.userMediaProgress.id}`)
.then(() => { .then(() => {
console.log('Progress reset complete') console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`)
this.resettingProgress = false this.resettingProgress = false
}) })
.catch((error) => { .catch((error) => {
@ -722,12 +721,12 @@ export default {
this.$axios this.$axios
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
.then(() => { .then(() => {
this.$toast.success('Item deleted') this.$toast.success(this.$strings.ToastItemDeletedSuccess)
this.$router.replace(`/library/${this.libraryId}`) this.$router.replace(`/library/${this.libraryId}`)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete item', error) console.error('Failed to delete item', error)
this.$toast.error('Failed to delete item') this.$toast.error(this.$strings.ToastItemDeleteFailed)
}) })
} }
}, },

View File

@ -138,7 +138,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove narrator', error) console.error('Failed to remove narrator', error)
this.$toast.error('Failed to remove narrator') this.$toast.error(this.$strings.ToastRemoveFailed)
this.loading = false this.loading = false
}) })
}, },

View File

@ -111,7 +111,7 @@ export default {
this.processing = true this.processing = true
const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => { const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => {
console.error('Failed to get download queue', 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 return null
}) })
this.processing = false this.processing = false

View File

@ -234,7 +234,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: episode.podcast.metadata.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, duration: episode.duration || null,
coverPath: episode.podcast.coverPath || null coverPath: episode.podcast.coverPath || null
}) })
@ -251,7 +251,7 @@ export default {
this.processing = true this.processing = true
const episodePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/recent-episodes?limit=25&page=${page}`).catch((error) => { 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) console.error('Failed to get recent episodes', error)
this.$toast.error('Failed to get recent episodes') this.$toast.error(this.$strings.ToastFailedToLoadData)
return null return null
}) })
this.processing = false this.processing = false
@ -271,7 +271,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: episode.podcast.metadata.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, duration: episode.duration || null,
coverPath: episode.podcast.coverPath || null coverPath: episode.podcast.coverPath || null
} }

View File

@ -146,7 +146,7 @@ export default {
this.processing = true this.processing = true
var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed }).catch((error) => { var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed }).catch((error) => {
console.error('Failed to get feed', error) console.error('Failed to get feed', error)
this.$toast.error('Failed to get podcast feed') this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
return null return null
}) })
this.processing = false this.processing = false
@ -197,7 +197,7 @@ export default {
this.processing = true this.processing = true
const payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => { const payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => {
console.error('Failed to get feed', error) console.error('Failed to get feed', error)
this.$toast.error('Failed to get podcast feed') this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
return null return null
}) })
this.processing = false this.processing = false

View File

@ -132,11 +132,11 @@ export default {
methods: { methods: {
async submitServerSetup() { async submitServerSetup() {
if (!this.newRoot.username || !this.newRoot.username.trim()) { if (!this.newRoot.username || !this.newRoot.username.trim()) {
this.$toast.error('Must enter a root username') this.$toast.error(this.$strings.ToastUserRootRequireName)
return return
} }
if (this.newRoot.password !== this.confirmPassword) { if (this.newRoot.password !== this.confirmPassword) {
this.$toast.error('Password mismatch') this.$toast.error(this.$strings.ToastUserPasswordMismatch)
return return
} }
if (!this.newRoot.password) { if (!this.newRoot.password) {

View File

@ -19,6 +19,7 @@
"ButtonChooseFiles": "Choose files", "ButtonChooseFiles": "Choose files",
"ButtonClearFilter": "Clear Filter", "ButtonClearFilter": "Clear Filter",
"ButtonCloseFeed": "Close Feed", "ButtonCloseFeed": "Close Feed",
"ButtonCloseSession": "Close Open Session",
"ButtonCollections": "Collections", "ButtonCollections": "Collections",
"ButtonConfigureScanner": "Configure Scanner", "ButtonConfigureScanner": "Configure Scanner",
"ButtonCreate": "Create", "ButtonCreate": "Create",
@ -28,6 +29,9 @@
"ButtonEdit": "Edit", "ButtonEdit": "Edit",
"ButtonEditChapters": "Edit Chapters", "ButtonEditChapters": "Edit Chapters",
"ButtonEditPodcast": "Edit Podcast", "ButtonEditPodcast": "Edit Podcast",
"ButtonEnable": "Enable",
"ButtonFireAndFail": "Fire and Fail",
"ButtonFireOnTest": "Fire onTest event",
"ButtonForceReScan": "Force Re-Scan", "ButtonForceReScan": "Force Re-Scan",
"ButtonFullPath": "Full Path", "ButtonFullPath": "Full Path",
"ButtonHide": "Hide", "ButtonHide": "Hide",
@ -56,6 +60,7 @@
"ButtonPlaylists": "Playlists", "ButtonPlaylists": "Playlists",
"ButtonPrevious": "Previous", "ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter", "ButtonPreviousChapter": "Previous Chapter",
"ButtonProbeAudioFile": "Probe Audio File",
"ButtonPurgeAllCache": "Purge All Cache", "ButtonPurgeAllCache": "Purge All Cache",
"ButtonPurgeItemsCache": "Purge Items Cache", "ButtonPurgeItemsCache": "Purge Items Cache",
"ButtonQueueAddItem": "Add to queue", "ButtonQueueAddItem": "Add to queue",
@ -93,6 +98,7 @@
"ButtonStats": "Stats", "ButtonStats": "Stats",
"ButtonSubmit": "Submit", "ButtonSubmit": "Submit",
"ButtonTest": "Test", "ButtonTest": "Test",
"ButtonUnlinkOpedId": "Unlink OpenID",
"ButtonUpload": "Upload", "ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload Backup", "ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Cover", "ButtonUploadCover": "Upload Cover",
@ -105,6 +111,7 @@
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title", "ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Account", "HeaderAccount": "Account",
"HeaderAddCustomMetadataProvider": "Add Custom Metadata Provider",
"HeaderAdvanced": "Advanced", "HeaderAdvanced": "Advanced",
"HeaderAppriseNotificationSettings": "Apprise Notification Settings", "HeaderAppriseNotificationSettings": "Apprise Notification Settings",
"HeaderAudioTracks": "Audio Tracks", "HeaderAudioTracks": "Audio Tracks",
@ -150,6 +157,8 @@
"HeaderMetadataToEmbed": "Metadata to embed", "HeaderMetadataToEmbed": "Metadata to embed",
"HeaderNewAccount": "New Account", "HeaderNewAccount": "New Account",
"HeaderNewLibrary": "New Library", "HeaderNewLibrary": "New Library",
"HeaderNotificationCreate": "Create Notification",
"HeaderNotificationUpdate": "Update Notification",
"HeaderNotifications": "Notifications", "HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Open RSS Feed", "HeaderOpenRSSFeed": "Open RSS Feed",
@ -206,8 +215,8 @@
"LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToCollectionBatch": "Add {0} Books to Collection",
"LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylist": "Add to Playlist",
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
"LabelAdded": "Added",
"LabelAddedAt": "Added At", "LabelAddedAt": "Added At",
"LabelAddedDate": "Added {0}",
"LabelAdminUsersOnly": "Admin users only", "LabelAdminUsersOnly": "Admin users only",
"LabelAll": "All", "LabelAll": "All",
"LabelAllUsers": "All Users", "LabelAllUsers": "All Users",
@ -298,6 +307,7 @@
"LabelEpisode": "Episode", "LabelEpisode": "Episode",
"LabelEpisodeTitle": "Episode Title", "LabelEpisodeTitle": "Episode Title",
"LabelEpisodeType": "Episode Type", "LabelEpisodeType": "Episode Type",
"LabelEpisodes": "Episodes",
"LabelExample": "Example", "LabelExample": "Example",
"LabelExpandSeries": "Expand Series", "LabelExpandSeries": "Expand Series",
"LabelExpandSubSeries": "Expand Sub Series", "LabelExpandSubSeries": "Expand Sub Series",
@ -309,7 +319,9 @@
"LabelFetchingMetadata": "Fetching Metadata", "LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File", "LabelFile": "File",
"LabelFileBirthtime": "File Birthtime", "LabelFileBirthtime": "File Birthtime",
"LabelFileBornDate": "Born {0}",
"LabelFileModified": "File Modified", "LabelFileModified": "File Modified",
"LabelFileModifiedDate": "Modified {0}",
"LabelFilename": "Filename", "LabelFilename": "Filename",
"LabelFilterByUser": "Filter by User", "LabelFilterByUser": "Filter by User",
"LabelFindEpisodes": "Find Episodes", "LabelFindEpisodes": "Find Episodes",
@ -448,8 +460,10 @@
"LabelPrimaryEbook": "Primary ebook", "LabelPrimaryEbook": "Primary ebook",
"LabelProgress": "Progress", "LabelProgress": "Progress",
"LabelProvider": "Provider", "LabelProvider": "Provider",
"LabelProviderAuthorizationValue": "Authorization Header Value",
"LabelPubDate": "Pub Date", "LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year", "LabelPublishYear": "Publish Year",
"LabelPublishedDate": "Published {0}",
"LabelPublisher": "Publisher", "LabelPublisher": "Publisher",
"LabelPublishers": "Publishers", "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
@ -595,6 +609,7 @@
"LabelUnabridged": "Unabridged", "LabelUnabridged": "Unabridged",
"LabelUndo": "Undo", "LabelUndo": "Undo",
"LabelUnknown": "Unknown", "LabelUnknown": "Unknown",
"LabelUnknownPublishDate": "Unknown publish date",
"LabelUpdateCover": "Update Cover", "LabelUpdateCover": "Update Cover",
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located", "LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
"LabelUpdateDetails": "Update Details", "LabelUpdateDetails": "Update Details",
@ -643,16 +658,22 @@
"MessageCheckingCron": "Checking cron...", "MessageCheckingCron": "Checking cron...",
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?", "MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "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?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", "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?", "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?", "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?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "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?", "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?", "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?", "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?", "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?", "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?", "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?", "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.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "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}\"?", "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", "MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFailed": "Embed Failed!", "MessageEmbedFailed": "Embed Failed!",
@ -706,6 +729,7 @@
"MessageNoCollections": "No Collections", "MessageNoCollections": "No Collections",
"MessageNoCoversFound": "No Covers Found", "MessageNoCoversFound": "No Covers Found",
"MessageNoDescription": "No description", "MessageNoDescription": "No description",
"MessageNoDevices": "No devices",
"MessageNoDownloadsInProgress": "No downloads currently in progress", "MessageNoDownloadsInProgress": "No downloads currently in progress",
"MessageNoDownloadsQueued": "No downloads queued", "MessageNoDownloadsQueued": "No downloads queued",
"MessageNoEpisodeMatchesFound": "No episode matches found", "MessageNoEpisodeMatchesFound": "No episode matches found",
@ -725,7 +749,6 @@
"MessageNoSeries": "No Series", "MessageNoSeries": "No Series",
"MessageNoTags": "No Tags", "MessageNoTags": "No Tags",
"MessageNoTasksRunning": "No Tasks Running", "MessageNoTasksRunning": "No Tasks Running",
"MessageNoUpdateNecessary": "No update necessary",
"MessageNoUpdatesWereNecessary": "No updates were necessary", "MessageNoUpdatesWereNecessary": "No updates were necessary",
"MessageNoUserPlaylists": "You have no playlists", "MessageNoUserPlaylists": "You have no playlists",
"MessageNotYetImplemented": "Not yet implemented", "MessageNotYetImplemented": "Not yet implemented",
@ -734,6 +757,7 @@
"MessagePauseChapter": "Pause chapter playback", "MessagePauseChapter": "Pause chapter playback",
"MessagePlayChapter": "Listen to beginning of chapter", "MessagePlayChapter": "Listen to beginning of chapter",
"MessagePlaylistCreateFromCollection": "Create playlist from collection", "MessagePlaylistCreateFromCollection": "Create playlist from collection",
"MessagePleaseWait": "Please wait...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching", "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.", "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", "MessageRemoveChapter": "Remove chapter",
@ -794,24 +818,32 @@
"StatsYearInReview": "YEAR IN REVIEW", "StatsYearInReview": "YEAR IN REVIEW",
"ToastAccountUpdateFailed": "Failed to update account", "ToastAccountUpdateFailed": "Failed to update account",
"ToastAccountUpdateSuccess": "Account updated", "ToastAccountUpdateSuccess": "Account updated",
"ToastAuthorImageRemoveFailed": "Failed to remove image", "ToastAppriseUrlRequired": "Must enter an Apprise URL",
"ToastAuthorImageRemoveSuccess": "Author image removed", "ToastAuthorImageRemoveSuccess": "Author image removed",
"ToastAuthorNotFound": "Author \"{0}\" not found",
"ToastAuthorRemoveSuccess": "Author removed",
"ToastAuthorSearchNotFound": "Author not found",
"ToastAuthorUpdateFailed": "Failed to update author", "ToastAuthorUpdateFailed": "Failed to update author",
"ToastAuthorUpdateMerged": "Author merged", "ToastAuthorUpdateMerged": "Author merged",
"ToastAuthorUpdateSuccess": "Author updated", "ToastAuthorUpdateSuccess": "Author updated",
"ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)", "ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)",
"ToastBackupAppliedSuccess": "Backup applied",
"ToastBackupCreateFailed": "Failed to create backup", "ToastBackupCreateFailed": "Failed to create backup",
"ToastBackupCreateSuccess": "Backup created", "ToastBackupCreateSuccess": "Backup created",
"ToastBackupDeleteFailed": "Failed to delete backup", "ToastBackupDeleteFailed": "Failed to delete backup",
"ToastBackupDeleteSuccess": "Backup deleted", "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", "ToastBackupRestoreFailed": "Failed to restore backup",
"ToastBackupUploadFailed": "Failed to upload backup", "ToastBackupUploadFailed": "Failed to upload backup",
"ToastBackupUploadSuccess": "Backup uploaded", "ToastBackupUploadSuccess": "Backup uploaded",
"ToastBatchDeleteFailed": "Batch delete failed",
"ToastBatchDeleteSuccess": "Batch delete success",
"ToastBatchUpdateFailed": "Batch update failed", "ToastBatchUpdateFailed": "Batch update failed",
"ToastBatchUpdateSuccess": "Batch update success", "ToastBatchUpdateSuccess": "Batch update success",
"ToastBookmarkCreateFailed": "Failed to create bookmark", "ToastBookmarkCreateFailed": "Failed to create bookmark",
"ToastBookmarkCreateSuccess": "Bookmark added", "ToastBookmarkCreateSuccess": "Bookmark added",
"ToastBookmarkRemoveFailed": "Failed to remove bookmark",
"ToastBookmarkRemoveSuccess": "Bookmark removed", "ToastBookmarkRemoveSuccess": "Bookmark removed",
"ToastBookmarkUpdateFailed": "Failed to update bookmark", "ToastBookmarkUpdateFailed": "Failed to update bookmark",
"ToastBookmarkUpdateSuccess": "Bookmark updated", "ToastBookmarkUpdateSuccess": "Bookmark updated",
@ -819,25 +851,46 @@
"ToastCachePurgeSuccess": "Cache purged successfully", "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Chapters have errors", "ToastChaptersHaveErrors": "Chapters have errors",
"ToastChaptersMustHaveTitles": "Chapters must have titles", "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", "ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
"ToastCollectionRemoveFailed": "Failed to remove collection",
"ToastCollectionRemoveSuccess": "Collection removed", "ToastCollectionRemoveSuccess": "Collection removed",
"ToastCollectionUpdateFailed": "Failed to update collection", "ToastCollectionUpdateFailed": "Failed to update collection",
"ToastCollectionUpdateSuccess": "Collection updated", "ToastCollectionUpdateSuccess": "Collection updated",
"ToastCoverUpdateFailed": "Cover update failed",
"ToastDeleteFileFailed": "Failed to delete file", "ToastDeleteFileFailed": "Failed to delete file",
"ToastDeleteFileSuccess": "File deleted", "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", "ToastErrorCannotShare": "Cannot share natively on this device",
"ToastFailedToLoadData": "Failed to load data", "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", "ToastItemCoverUpdateFailed": "Failed to update item cover",
"ToastItemCoverUpdateSuccess": "Item cover updated", "ToastItemCoverUpdateSuccess": "Item cover updated",
"ToastItemDeletedFailed": "Failed to delete item",
"ToastItemDeletedSuccess": "Deleted item",
"ToastItemDetailsUpdateFailed": "Failed to update item details", "ToastItemDetailsUpdateFailed": "Failed to update item details",
"ToastItemDetailsUpdateSuccess": "Item details updated", "ToastItemDetailsUpdateSuccess": "Item details updated",
"ToastItemDetailsUpdateUnneeded": "No updates needed for item details",
"ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished", "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
"ToastItemMarkedAsFinishedSuccess": "Item marked as Finished", "ToastItemMarkedAsFinishedSuccess": "Item marked as Finished",
"ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished", "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
"ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished", "ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished",
"ToastItemUpdateFailed": "Failed to update item",
"ToastItemUpdateSuccess": "Item updated",
"ToastLibraryCreateFailed": "Failed to create library", "ToastLibraryCreateFailed": "Failed to create library",
"ToastLibraryCreateSuccess": "Library \"{0}\" created", "ToastLibraryCreateSuccess": "Library \"{0}\" created",
"ToastLibraryDeleteFailed": "Failed to delete library", "ToastLibraryDeleteFailed": "Failed to delete library",
@ -846,32 +899,78 @@
"ToastLibraryScanStarted": "Library scan started", "ToastLibraryScanStarted": "Library scan started",
"ToastLibraryUpdateFailed": "Failed to update library", "ToastLibraryUpdateFailed": "Failed to update library",
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated", "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", "ToastPlaylistCreateFailed": "Failed to create playlist",
"ToastPlaylistCreateSuccess": "Playlist created", "ToastPlaylistCreateSuccess": "Playlist created",
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
"ToastPlaylistRemoveSuccess": "Playlist removed", "ToastPlaylistRemoveSuccess": "Playlist removed",
"ToastPlaylistUpdateFailed": "Failed to update playlist", "ToastPlaylistUpdateFailed": "Failed to update playlist",
"ToastPlaylistUpdateSuccess": "Playlist updated", "ToastPlaylistUpdateSuccess": "Playlist updated",
"ToastPodcastCreateFailed": "Failed to create podcast", "ToastPodcastCreateFailed": "Failed to create podcast",
"ToastPodcastCreateSuccess": "Podcast created successfully", "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", "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed", "ToastRSSFeedCloseSuccess": "RSS feed closed",
"ToastRemoveFailed": "Failed to remove",
"ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection", "ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
"ToastRemoveItemFromCollectionSuccess": "Item removed 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", "ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"", "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed", "ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success", "ToastSeriesUpdateSuccess": "Series update success",
"ToastServerSettingsUpdateFailed": "Failed to update server settings", "ToastServerSettingsUpdateFailed": "Failed to update server settings",
"ToastServerSettingsUpdateSuccess": "Server settings updated", "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionCloseFailed": "Failed to close session",
"ToastSessionDeleteFailed": "Failed to delete session", "ToastSessionDeleteFailed": "Failed to delete session",
"ToastSessionDeleteSuccess": "Session deleted", "ToastSessionDeleteSuccess": "Session deleted",
"ToastSlugMustChange": "Slug contains invalid characters",
"ToastSlugRequired": "Slug is required",
"ToastSocketConnected": "Socket connected", "ToastSocketConnected": "Socket connected",
"ToastSocketDisconnected": "Socket disconnected", "ToastSocketDisconnected": "Socket disconnected",
"ToastSocketFailedToConnect": "Socket failed to connect", "ToastSocketFailedToConnect": "Socket failed to connect",
"ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix", "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
"ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes", "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
"ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)", "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", "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"
} }