mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-09 10:51:37 +00:00
Merge branch 'advplyr:master' into feat/all-stats-page
This commit is contained in:
commit
df15c22c6d
336 changed files with 21709 additions and 13151 deletions
|
|
@ -32,9 +32,48 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="showEreaderTable">
|
||||
<div class="w-full h-px bg-white/10 my-4" />
|
||||
|
||||
<app-settings-content :header-text="$strings.HeaderEreaderDevices">
|
||||
<template #header-items>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-btn color="primary" small @click="addNewDeviceClick">{{ $strings.ButtonAddDevice }}</ui-btn>
|
||||
</template>
|
||||
|
||||
<table v-if="ereaderDevices.length" class="tracksTable mt-4">
|
||||
<tr>
|
||||
<th class="text-left">{{ $strings.LabelName }}</th>
|
||||
<th class="text-left">{{ $strings.LabelEmail }}</th>
|
||||
<th class="w-40"></th>
|
||||
</tr>
|
||||
<tr v-for="device in ereaderDevices" :key="device.name">
|
||||
<td>
|
||||
<p class="text-sm md:text-base text-gray-100">{{ device.name }}</p>
|
||||
</td>
|
||||
<td class="text-left">
|
||||
<p class="text-sm md:text-base text-gray-100">{{ device.email }}</p>
|
||||
</td>
|
||||
<td class="w-40">
|
||||
<div class="flex justify-end items-center h-10">
|
||||
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name || device.users?.length !== 1" class="mx-1" @click="editDeviceClick(device)" />
|
||||
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name || device.users?.length !== 1" @click="deleteDeviceClick(device)" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div v-else-if="!loading" class="text-center py-4">
|
||||
<p class="text-lg text-gray-100">{{ $strings.MessageNoDevices }}</p>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
</div>
|
||||
|
||||
<div class="py-4 mt-8 flex">
|
||||
<ui-btn color="primary flex items-center text-lg" @click="logout"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
|
||||
</div>
|
||||
|
||||
<modals-emails-user-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="revisedEreaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -43,11 +82,20 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
password: null,
|
||||
newPassword: null,
|
||||
confirmPassword: null,
|
||||
changingPassword: false,
|
||||
selectedLanguage: ''
|
||||
selectedLanguage: '',
|
||||
newEReaderDevice: {
|
||||
name: '',
|
||||
email: ''
|
||||
},
|
||||
ereaderDevices: [],
|
||||
deletingDeviceName: null,
|
||||
selectedEReaderDevice: null,
|
||||
showEReaderDeviceModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -75,6 +123,12 @@ export default {
|
|||
},
|
||||
showChangePasswordForm() {
|
||||
return !this.isGuest && this.isPasswordAuthEnabled
|
||||
},
|
||||
showEreaderTable() {
|
||||
return this.usertype !== 'root' && this.usertype !== 'admin' && this.user.permissions?.createEreader
|
||||
},
|
||||
revisedEreaderDevices() {
|
||||
return this.ereaderDevices.filter((device) => device.users?.length === 1)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -117,10 +171,10 @@ export default {
|
|||
},
|
||||
submitChangePassword() {
|
||||
if (this.newPassword !== this.confirmPassword) {
|
||||
return this.$toast.error('New password and confirm password do not match')
|
||||
return this.$toast.error(this.$strings.ToastUserPasswordMismatch)
|
||||
}
|
||||
if (this.password === this.newPassword) {
|
||||
return this.$toast.error('Password and New Password cannot be the same')
|
||||
return this.$toast.error(this.$strings.ToastUserPasswordMustChange)
|
||||
}
|
||||
this.changingPassword = true
|
||||
this.$axios
|
||||
|
|
@ -130,22 +184,64 @@ export default {
|
|||
})
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
this.$toast.success('Password Changed Successfully')
|
||||
this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
|
||||
this.resetForm()
|
||||
} else {
|
||||
this.$toast.error(res.error || 'Unknown Error')
|
||||
this.$toast.error(res.error || this.$strings.ToastUnknownError)
|
||||
}
|
||||
this.changingPassword = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.$toast.error('Api call failed')
|
||||
this.$toast.error(this.$strings.ToastUnknownError)
|
||||
this.changingPassword = false
|
||||
})
|
||||
},
|
||||
addNewDeviceClick() {
|
||||
this.selectedEReaderDevice = null
|
||||
this.showEReaderDeviceModal = true
|
||||
},
|
||||
editDeviceClick(device) {
|
||||
this.selectedEReaderDevice = device
|
||||
this.showEReaderDeviceModal = true
|
||||
},
|
||||
deleteDeviceClick(device) {
|
||||
const payload = {
|
||||
message: this.$getString('MessageConfirmDeleteDevice', [device.name]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteDevice(device)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
deleteDevice(device) {
|
||||
const payload = {
|
||||
ereaderDevices: this.revisedEreaderDevices.filter((d) => d.name !== device.name)
|
||||
}
|
||||
this.deletingDeviceName = device.name
|
||||
this.$axios
|
||||
.$post(`/api/me/ereader-devices`, payload)
|
||||
.then((data) => {
|
||||
this.ereaderDevicesUpdated(data.ereaderDevices)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete device', error)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.deletingDeviceName = null
|
||||
})
|
||||
},
|
||||
ereaderDevicesUpdated(ereaderDevices) {
|
||||
this.ereaderDevices = ereaderDevices
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.selectedLanguage = this.$languageCodes.current
|
||||
this.ereaderDevices = this.$store.state.libraries.ereaderDevices || []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ export default {
|
|||
const audioEl = this.audioEl || document.createElement('audio')
|
||||
var src = audioTrack.contentUrl + `?token=${this.userToken}`
|
||||
if (this.$isDev) {
|
||||
src = `http://localhost:3333${this.$config.routerBasePath}${src}`
|
||||
src = `${process.env.serverUrl}${src}`
|
||||
}
|
||||
|
||||
audioEl.src = src
|
||||
|
|
@ -486,7 +486,7 @@ export default {
|
|||
.then((data) => {
|
||||
this.saving = false
|
||||
if (data.updated) {
|
||||
this.$toast.success('Chapters updated')
|
||||
this.$toast.success(this.$strings.ToastChaptersUpdated)
|
||||
if (this.previousRoute) {
|
||||
this.$router.push(this.previousRoute)
|
||||
} else {
|
||||
|
|
@ -499,7 +499,7 @@ export default {
|
|||
.catch((error) => {
|
||||
this.saving = false
|
||||
console.error('Failed to update chapters', error)
|
||||
this.$toast.error('Failed to update chapters')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
},
|
||||
applyChapterNamesOnly() {
|
||||
|
|
@ -533,7 +533,7 @@ export default {
|
|||
},
|
||||
findChapters() {
|
||||
if (!this.asinInput) {
|
||||
this.$toast.error('Must input an ASIN')
|
||||
this.$toast.error(this.$strings.ToastAsinRequired)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -560,7 +560,7 @@ export default {
|
|||
.catch((error) => {
|
||||
this.findingChapters = false
|
||||
console.error('Failed to get chapter data', error)
|
||||
this.$toast.error('Failed to find chapters')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
this.showFindChaptersModal = false
|
||||
})
|
||||
},
|
||||
|
|
@ -611,7 +611,7 @@ export default {
|
|||
.$post(`/api/items/${this.libraryItem.id}/chapters`, payload)
|
||||
.then((data) => {
|
||||
if (data.updated) {
|
||||
this.$toast.success('Chapters removed')
|
||||
this.$toast.success(this.$strings.ToastChaptersRemoved)
|
||||
if (this.previousRoute) {
|
||||
this.$router.push(this.previousRoute)
|
||||
} else {
|
||||
|
|
@ -623,20 +623,32 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove chapters', error)
|
||||
this.$toast.error('Failed to remove chapters')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false
|
||||
})
|
||||
},
|
||||
libraryItemUpdated(libraryItem) {
|
||||
if (libraryItem.id === this.libraryItem.id) {
|
||||
if (!!libraryItem.media.metadata.asin && this.mediaMetadata.asin !== libraryItem.media.metadata.asin) {
|
||||
this.asinInput = libraryItem.media.metadata.asin
|
||||
}
|
||||
this.libraryItem = libraryItem
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.regionInput = localStorage.getItem('audibleRegion') || 'US'
|
||||
this.asinInput = this.mediaMetadata.asin || null
|
||||
this.initChapters()
|
||||
|
||||
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroyAudioEl()
|
||||
|
||||
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -63,11 +63,11 @@
|
|||
<div class="w-full max-w-4xl mx-auto">
|
||||
<!-- queued alert -->
|
||||
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mb-4">
|
||||
<p class="text-lg">Audiobook is queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p>
|
||||
<p class="text-lg">{{ $getString('MessageEmbedQueue', [queuedEmbedLIds.length]) }}</p>
|
||||
</widgets-alert>
|
||||
<!-- metadata embed action buttons -->
|
||||
<div v-else-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
||||
<ui-checkbox v-if="!isTaskFinished" v-model="shouldBackupAudioFiles" :disabled="processing" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
||||
<ui-checkbox v-if="!isTaskFinished" v-model="shouldBackupAudioFiles" :disabled="processing" :label="$strings.LabelBackupAudioFiles" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
||||
|
||||
<div class="flex-grow" />
|
||||
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
<!-- m4b embed action buttons -->
|
||||
<div v-else class="w-full flex items-center mb-4">
|
||||
<button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions">
|
||||
<span class="material-symbols text-xl">{{ showEncodeOptions || usingCustomEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">Use Advanced Options</span>
|
||||
<span class="material-symbols text-xl">{{ showEncodeOptions || usingCustomEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">{{ $strings.LabelUseAdvancedOptions }}</span>
|
||||
</button>
|
||||
|
||||
<div class="flex-grow" />
|
||||
|
|
@ -94,11 +94,11 @@
|
|||
<transition name="slide">
|
||||
<div v-if="showEncodeOptions || usingCustomEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
||||
<div class="flex flex-wrap -mx-2">
|
||||
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="'Audio Bitrate (e.g. 128k)'" class="m-2 max-w-40" @input="bitrateChanged" />
|
||||
<ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" :disabled="processing || isTaskFinished" :label="'Audio Channels (1 or 2)'" class="m-2 max-w-40" @input="channelsChanged" />
|
||||
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="'Audio Codec'" class="m-2 max-w-40" @input="codecChanged" />
|
||||
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioBitrate" class="m-2 max-w-40" @input="bitrateChanged" />
|
||||
<ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioChannels" class="m-2 max-w-40" @input="channelsChanged" />
|
||||
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioCodec" class="m-2 max-w-40" @input="codecChanged" />
|
||||
</div>
|
||||
<p class="text-sm text-warning">Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.</p>
|
||||
<p class="text-sm text-warning">{{ $strings.LabelEncodingWarningAdvancedSettings }}</p>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
|
@ -106,36 +106,36 @@
|
|||
<div class="mb-4">
|
||||
<div v-if="isEmbedTool" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Metadata will be embedded in the audio tracks inside your audiobook folder.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingInfoEmbedded }}</p>
|
||||
</div>
|
||||
<div v-else class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">
|
||||
Finished M4B will be put into your audiobook folder at <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">.../{{ libraryItemRelPath }}/</span>.
|
||||
{{ $strings.LabelEncodingFinishedM4B }} <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">.../{{ libraryItemRelPath }}/</span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="shouldBackupAudioFiles || isM4BTool" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">
|
||||
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache.
|
||||
{{ $strings.LabelEncodingBackupLocation }} <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. {{ $strings.LabelEncodingClearItemCache }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="isEmbedTool && audioFiles.length > 1" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Chapters are not embedded in multi-track audiobooks.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingChaptersNotEmbedded }}</p>
|
||||
</div>
|
||||
<div v-if="isM4BTool" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Encoding can take up to 30 minutes.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingTimeWarning }}</p>
|
||||
</div>
|
||||
<div v-if="isM4BTool" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">If you have the watcher disabled you will need to re-scan this audiobook afterwards.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingWatcherDisabled }}</p>
|
||||
</div>
|
||||
<div class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Once the task is started you can navigate away from this page.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingStartedNavigation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -269,11 +269,11 @@ export default {
|
|||
},
|
||||
availableTools() {
|
||||
if (this.isSingleM4b) {
|
||||
return [{ value: 'embed', text: 'Embed Metadata' }]
|
||||
return [{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata }]
|
||||
} else {
|
||||
return [
|
||||
{ value: 'embed', text: 'Embed Metadata' },
|
||||
{ value: 'm4b', text: 'M4B Encoder' }
|
||||
{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata },
|
||||
{ value: 'm4b', text: this.$strings.LabelToolsM4bEncoder }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -331,11 +331,11 @@ export default {
|
|||
this.$axios
|
||||
.$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
|
||||
.then(() => {
|
||||
this.$toast.success('Encode canceled')
|
||||
this.$toast.success(this.$strings.ToastEncodeCancelSucces)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to cancel encode', error)
|
||||
this.$toast.error('Failed to cancel encode')
|
||||
this.$toast.error(this.$strings.ToastEncodeCancelFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isCancelingEncode = false
|
||||
|
|
@ -370,7 +370,7 @@ export default {
|
|||
},
|
||||
embedClick() {
|
||||
const payload = {
|
||||
message: `Are you sure you want to embed metadata in ${this.audioFiles.length} audio files?`,
|
||||
message: this.$getString('MessageConfirmEmbedMetadataInAudioFiles', [this.audioFiles.length]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.updateAudioFileMetadata()
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export default {
|
|||
})
|
||||
|
||||
if (!author) {
|
||||
return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
|
||||
return redirect(`/library/${store.state.libraries.currentLibraryId}/bookshelf/authors`)
|
||||
}
|
||||
|
||||
if (store.state.libraries.currentLibraryId !== author.libraryId || !store.state.libraries.filterData) {
|
||||
|
|
@ -109,7 +109,7 @@ export default {
|
|||
authorRemoved(author) {
|
||||
if (author.id === this.author.id) {
|
||||
console.warn('Author was removed')
|
||||
this.$router.replace(`/library/${this.currentLibraryId}/authors`)
|
||||
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/authors`)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@
|
|||
<div class="flex justify-center flex-wrap">
|
||||
<template v-for="libraryItem in libraryItemCopies">
|
||||
<div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
|
||||
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
|
||||
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
|
||||
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
|
||||
<div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||
<ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" :disabled="!hasChanges" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -170,7 +170,8 @@ export default {
|
|||
abridged: false
|
||||
},
|
||||
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
|
||||
openMapOptions: false
|
||||
openMapOptions: false,
|
||||
itemsWithChanges: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -221,9 +222,19 @@ export default {
|
|||
},
|
||||
hasSelectedBatchUsage() {
|
||||
return Object.values(this.selectedBatchUsage).some((b) => !!b)
|
||||
},
|
||||
hasChanges() {
|
||||
return this.itemsWithChanges.length > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleItemChange(itemChange) {
|
||||
if (!itemChange.hasChanges) {
|
||||
this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId)
|
||||
} else if (!this.itemsWithChanges.includes(itemChange.libraryItemId)) {
|
||||
this.itemsWithChanges.push(itemChange.libraryItemId)
|
||||
}
|
||||
},
|
||||
blurBatchForm() {
|
||||
if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) {
|
||||
this.$refs.seriesSelect.forceBlur()
|
||||
|
|
@ -283,38 +294,10 @@ export default {
|
|||
removedSeriesItem(item) {},
|
||||
newNarratorItem(item) {},
|
||||
removedNarratorItem(item) {},
|
||||
newTagItem(item) {
|
||||
// if (item && !this.newTagItems.includes(item)) {
|
||||
// this.newTagItems.push(item)
|
||||
// }
|
||||
},
|
||||
removedTagItem(item) {
|
||||
// If newly added, remove if not used on any other items
|
||||
// if (item && this.newTagItems.includes(item)) {
|
||||
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
|
||||
// return ab.tags && ab.tags.includes(item)
|
||||
// })
|
||||
// if (!usedByOtherAb) {
|
||||
// this.newTagItems = this.newTagItems.filter((t) => t !== item)
|
||||
// }
|
||||
// }
|
||||
},
|
||||
newGenreItem(item) {
|
||||
// if (item && !this.newGenreItems.includes(item)) {
|
||||
// this.newGenreItems.push(item)
|
||||
// }
|
||||
},
|
||||
removedGenreItem(item) {
|
||||
// If newly added, remove if not used on any other items
|
||||
// if (item && this.newGenreItems.includes(item)) {
|
||||
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
|
||||
// return ab.book.genres && ab.book.genres.includes(item)
|
||||
// })
|
||||
// if (!usedByOtherAb) {
|
||||
// this.newGenreItems = this.newGenreItems.filter((t) => t !== item)
|
||||
// }
|
||||
// }
|
||||
},
|
||||
newTagItem(item) {},
|
||||
removedTagItem(item) {},
|
||||
newGenreItem(item) {},
|
||||
removedGenreItem(item) {},
|
||||
init() {
|
||||
// TODO: Better deep cloning of library items
|
||||
this.libraryItemCopies = this.libraryItems.map((li) => {
|
||||
|
|
@ -366,7 +349,7 @@ export default {
|
|||
}
|
||||
}
|
||||
if (!updates.length) {
|
||||
return this.$toast.warning('No updates were made')
|
||||
return this.$toast.warning(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
|
||||
console.log('Pushing updates', updates)
|
||||
|
|
@ -376,6 +359,7 @@ export default {
|
|||
.then((data) => {
|
||||
this.isProcessing = false
|
||||
if (data.updates) {
|
||||
this.itemsWithChanges = []
|
||||
this.$toast.success(`Successfully updated ${data.updates} items`)
|
||||
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
|
||||
} else {
|
||||
|
|
@ -387,10 +371,28 @@ export default {
|
|||
this.$toast.error('Failed to batch update')
|
||||
this.isProcessing = false
|
||||
})
|
||||
},
|
||||
beforeUnload(e) {
|
||||
if (!e || !this.hasChanges) return
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (this.hasChanges) {
|
||||
next(false)
|
||||
window.location = to.path
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
|
||||
window.addEventListener('beforeunload', this.beforeUnload)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('beforeunload', this.beforeUnload)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -406,4 +408,4 @@ export default {
|
|||
transform: translateY(-100%);
|
||||
transition: all 150ms ease-in 0s;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
||||
<span v-show="!streaming" class="material-symbols fill text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlayAll }}
|
||||
</ui-btn>
|
||||
|
||||
<!-- RSS feed -->
|
||||
|
|
|
|||
|
|
@ -64,6 +64,20 @@
|
|||
<ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" />
|
||||
<p class="sm:pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" />
|
||||
|
||||
<div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
|
||||
<div class="w-44">
|
||||
<ui-dropdown v-model="newAuthSettings.authOpenIDSubfolderForRedirectURLs" small :items="subfolderOptions" :label="$strings.LabelWebRedirectURLsSubfolder" :disabled="savingSettings" />
|
||||
</div>
|
||||
<div class="mt-2 sm:mt-5">
|
||||
<p class="sm:pl-4 text-sm text-gray-300">{{ $strings.LabelWebRedirectURLsDescription }}</p>
|
||||
<p class="sm:pl-4 text-sm text-gray-300 mb-2">
|
||||
<code>{{ webCallbackURL }}</code>
|
||||
<br />
|
||||
<code>{{ mobileAppCallbackURL }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" />
|
||||
|
||||
<div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
|
||||
|
|
@ -164,6 +178,27 @@ export default {
|
|||
value: 'username'
|
||||
}
|
||||
]
|
||||
},
|
||||
subfolderOptions() {
|
||||
const options = [
|
||||
{
|
||||
text: 'None',
|
||||
value: ''
|
||||
}
|
||||
]
|
||||
if (this.$config.routerBasePath) {
|
||||
options.push({
|
||||
text: this.$config.routerBasePath,
|
||||
value: this.$config.routerBasePath
|
||||
})
|
||||
}
|
||||
return options
|
||||
},
|
||||
webCallbackURL() {
|
||||
return `https://<your.server.com>${this.newAuthSettings.authOpenIDSubfolderForRedirectURLs ? this.newAuthSettings.authOpenIDSubfolderForRedirectURLs : ''}/auth/openid/callback`
|
||||
},
|
||||
mobileAppCallbackURL() {
|
||||
return `https://<your.server.com>${this.newAuthSettings.authOpenIDSubfolderForRedirectURLs ? this.newAuthSettings.authOpenIDSubfolderForRedirectURLs : ''}/auth/openid/mobile-redirect`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -317,7 +352,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.$toast.error(this.$strings.ToastServerSettingsUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingSettings = false
|
||||
|
|
@ -325,7 +360,8 @@ export default {
|
|||
},
|
||||
init() {
|
||||
this.newAuthSettings = {
|
||||
...this.authSettings
|
||||
...this.authSettings,
|
||||
authOpenIDSubfolderForRedirectURLs: this.authSettings.authOpenIDSubfolderForRedirectURLs === undefined ? this.$config.routerBasePath : this.authSettings.authOpenIDSubfolderForRedirectURLs
|
||||
}
|
||||
this.enableLocalAuth = this.authMethods.includes('local')
|
||||
this.enableOpenIDAuth = this.authMethods.includes('openid')
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to save backup path', error)
|
||||
const errorMsg = error.response?.data || 'Failed to save backup path'
|
||||
const errorMsg = error.response?.data || this.$strings.ToastFailedToUpdate
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -171,11 +171,11 @@ export default {
|
|||
},
|
||||
updateBackupsSettings() {
|
||||
if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) {
|
||||
this.$toast.error('Invalid maximum backup size')
|
||||
this.$toast.error(this.$strings.ToastBackupInvalidMaxSize)
|
||||
return
|
||||
}
|
||||
if (isNaN(this.backupsToKeep) || this.backupsToKeep <= 0 || this.backupsToKeep > 99) {
|
||||
this.$toast.error('Invalid number of backups to keep')
|
||||
this.$toast.error(this.$strings.ToastBackupInvalidMaxKeep)
|
||||
return
|
||||
}
|
||||
const updatePayload = {
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@
|
|||
</tr>
|
||||
</table>
|
||||
<div v-else-if="!loading" class="text-center py-4">
|
||||
<p class="text-lg text-gray-100">No Devices</p>
|
||||
<p class="text-lg text-gray-100">{{ $strings.MessageNoDevices }}</p>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
|
|
@ -199,7 +199,7 @@ export default {
|
|||
},
|
||||
deleteDeviceClick(device) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to delete e-reader device "${device.name}"?`,
|
||||
message: this.$getString('MessageConfirmDeleteDevice', [device.name]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteDevice(device)
|
||||
|
|
@ -218,11 +218,10 @@ export default {
|
|||
.$post(`/api/emails/ereader-devices`, payload)
|
||||
.then((data) => {
|
||||
this.ereaderDevicesUpdated(data.ereaderDevices)
|
||||
this.$toast.success('Device deleted')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete device', error)
|
||||
this.$toast.error('Failed to delete device')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.deletingDeviceName = null
|
||||
|
|
@ -246,11 +245,11 @@ export default {
|
|||
this.$axios
|
||||
.$post('/api/emails/test')
|
||||
.then(() => {
|
||||
this.$toast.success('Test Email Sent')
|
||||
this.$toast.success(this.$strings.ToastDeviceTestEmailSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to send test email', error)
|
||||
const errorMsg = error.response.data || 'Failed to send test email'
|
||||
const errorMsg = error.response.data || this.$strings.ToastDeviceTestEmailFailed
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -289,11 +288,11 @@ export default {
|
|||
this.newSettings = {
|
||||
...data.settings
|
||||
}
|
||||
this.$toast.success('Email settings updated')
|
||||
this.$toast.success(this.$strings.ToastEmailSettingsUpdateSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update email settings', error)
|
||||
this.$toast.error('Failed to update email settings')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingSettings = false
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
<div class="pt-4">
|
||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
|
||||
</div>
|
||||
<div class="flex items-end py-2">
|
||||
<ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsStoreCoversWithItemHelp" class="flex items-end py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsStoreCoversWithItem" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
|
|
@ -16,9 +16,9 @@
|
|||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-store-metadata-with-items" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsStoreMetadataWithItemHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsStoreMetadataWithItem" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-store-metadata-with-items">{{ $strings.LabelSettingsStoreMetadataWithItem }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
|
|
@ -26,9 +26,9 @@
|
|||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-sorting-ignore-prefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsSortingIgnorePrefixesHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsSortingIgnorePrefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-sorting-ignore-prefixes">{{ $strings.LabelSettingsSortingIgnorePrefixes }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
|
|
@ -42,18 +42,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2 mb-2">
|
||||
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
||||
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-parse-subtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsParseSubtitlesHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsParseSubtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsParseSubtitlesHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
|
|
@ -61,9 +56,9 @@
|
|||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-find-covers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsFindCoversHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsFindCovers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsFindCoversHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
|
|
@ -75,9 +70,9 @@
|
|||
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-prefer-matched-metadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsPreferMatchedMetadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
|
|
@ -85,15 +80,29 @@
|
|||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsEnableWatcherHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsEnableWatcherHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsEnableWatcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsEnableWatcherHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsWebClient }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :label="$strings.LabelSettingsChromecastSupport" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
||||
<p aria-hidden="true" class="pl-4">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2 mb-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.allowIframe" :label="$strings.LabelSettingsAllowIframe" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('allowIframe', val)" />
|
||||
<p aria-hidden="true" class="pl-4">{{ $strings.LabelSettingsAllowIframe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
|
|
@ -290,7 +299,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update prefixes', error)
|
||||
this.$toast.error(this.$strings.ToastSortingPrefixesUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingPrefixes = false
|
||||
|
|
@ -324,22 +333,21 @@ export default {
|
|||
},
|
||||
updateServerSettings(payload) {
|
||||
this.updatingServerSettings = true
|
||||
this.$store
|
||||
.dispatch('updateServerSettings', payload)
|
||||
.then(() => {
|
||||
this.updatingServerSettings = false
|
||||
this.$toast.success(this.$strings.ToastServerSettingsUpdateSuccess)
|
||||
this.$store.dispatch('updateServerSettings', payload).then((response) => {
|
||||
this.updatingServerSettings = false
|
||||
|
||||
if (payload.language) {
|
||||
// Updating language after save allows for re-rendering
|
||||
this.$setLanguageCode(payload.language)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.updatingServerSettings = false
|
||||
this.$toast.error(this.$strings.ToastServerSettingsUpdateFailed)
|
||||
})
|
||||
if (response.error) {
|
||||
console.error('Failed to update server settins', response.error)
|
||||
this.$toast.error(response.error)
|
||||
this.initServerSettings()
|
||||
return
|
||||
}
|
||||
|
||||
if (payload.language) {
|
||||
// Updating language after save allows for re-rendering
|
||||
this.$setLanguageCode(payload.language)
|
||||
}
|
||||
})
|
||||
},
|
||||
initServerSettings() {
|
||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to rename genre', error)
|
||||
this.$toast.error('Failed to rename genre')
|
||||
this.$toast.error(this.$strings.ToastRenameFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
|
|
@ -147,7 +147,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove genre', error)
|
||||
this.$toast.error('Failed to remove genre')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to rename tag', error)
|
||||
this.$toast.error('Failed to rename tag')
|
||||
this.$toast.error(this.$strings.ToastRenameFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
|
|
@ -143,7 +143,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove tag', error)
|
||||
this.$toast.error('Failed to remove tag')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
</template>
|
||||
|
||||
<div class="flex justify-between mb-2 place-items-end">
|
||||
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
||||
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
||||
|
||||
<ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" class="w-full sm:w-44" />
|
||||
<ui-dropdown v-model="newServerSettings.logLevel" :label="$strings.LabelServerLogLevel" :items="logLevelItems" @input="logLevelUpdated" class="w-full sm:w-44" />
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
|
|
|
|||
|
|
@ -105,12 +105,12 @@ export default {
|
|||
}
|
||||
|
||||
if (isNaN(this.maxNotificationQueue) || this.maxNotificationQueue <= 0) {
|
||||
this.$toast.error('Max notification queue must be >= 0')
|
||||
this.$toast.error(this.$strings.ToastNotificationQueueMaximum)
|
||||
return false
|
||||
}
|
||||
|
||||
if (isNaN(this.maxFailedAttempts) || this.maxFailedAttempts <= 0) {
|
||||
this.$toast.error('Max failed attempts must be >= 0')
|
||||
this.$toast.error(this.$strings.ToastNotificationFailedMaximum)
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -128,11 +128,11 @@ export default {
|
|||
this.$axios
|
||||
.$patch('/api/notifications', updatePayload)
|
||||
.then(() => {
|
||||
this.$toast.success('Notification settings updated')
|
||||
this.$toast.success(this.$strings.ToastNotificationSettingsUpdateSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update notification settings', error)
|
||||
this.$toast.error('Failed to update notification settings')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingSettings = false
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<tr v-for="feed in feeds" :key="feed.id" class="cursor-pointer h-12" @click="showFeed(feed)">
|
||||
<!-- -->
|
||||
<td>
|
||||
<img :src="coverUrl(feed)" class="h-full w-full" />
|
||||
<img :src="coverUrl(feed)" class="h-auto w-full" />
|
||||
</td>
|
||||
<!-- -->
|
||||
<td class="w-48 max-w-64 min-w-24 text-left truncate">
|
||||
|
|
@ -126,7 +126,7 @@ export default {
|
|||
},
|
||||
coverUrl(feed) {
|
||||
if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png`
|
||||
return `${feed.feedUrl}/cover`
|
||||
return `${this.$config.routerBasePath}${feed.feedUrl}/cover`
|
||||
},
|
||||
async loadFeeds() {
|
||||
const data = await this.$axios.$get(`/api/feeds`).catch((err) => {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
<ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" />
|
||||
</div>
|
||||
<div class="inline-flex items-center">
|
||||
<p class="text-sm mx-2">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
||||
<p class="text-sm mx-2">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
|
||||
<ui-icon-btn icon="arrow_back_ios_new" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||
<ui-icon-btn icon="arrow_forward_ios" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
||||
</div>
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
<div v-if="openListeningSessions.length" class="w-full my-8 h-px bg-white/10" />
|
||||
|
||||
<!-- open listening sessions table -->
|
||||
<p v-if="openListeningSessions.length" class="text-lg my-4">Open Listening Sessions</p>
|
||||
<p v-if="openListeningSessions.length" class="text-lg my-4">{{ $strings.HeaderOpenListeningSessions }}</p>
|
||||
<div v-if="openListeningSessions.length" class="block max-w-full">
|
||||
<table class="userSessionsTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
|
|
@ -290,7 +290,6 @@ export default {
|
|||
this.$axios
|
||||
.$post(`/api/sessions/batch/delete`, payload)
|
||||
.then(() => {
|
||||
this.$toast.success('Sessions removed')
|
||||
if (isAllSessions) {
|
||||
// If all sessions were removed from the current page then go to the previous page
|
||||
if (this.currentPage > 0) {
|
||||
|
|
@ -303,7 +302,7 @@ export default {
|
|||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMsg = error.response?.data || 'Failed to remove sessions'
|
||||
const errorMsg = error.response?.data || this.$strings.ToastRemoveFailed
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -358,12 +357,13 @@ export default {
|
|||
})
|
||||
|
||||
if (!libraryItem) {
|
||||
this.$toast.error('Failed to get library item')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
this.processingGoToTimestamp = false
|
||||
return
|
||||
}
|
||||
if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
|
||||
this.$toast.error('Failed to get podcast episode')
|
||||
console.error('Episode not found in library item', session.episodeId, libraryItem.media.episodes)
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
this.processingGoToTimestamp = false
|
||||
return
|
||||
}
|
||||
|
|
@ -377,7 +377,7 @@ export default {
|
|||
episodeId: episode.id,
|
||||
title: episode.title,
|
||||
subtitle: libraryItem.media.metadata.title,
|
||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: episode.audioFile.duration || null,
|
||||
coverPath: libraryItem.media.coverPath || null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||
</div>
|
||||
<div v-if="userToken" class="flex text-xs mt-4">
|
||||
<ui-text-input-with-label label="API Token" :value="userToken" readonly />
|
||||
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly />
|
||||
|
||||
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
|
||||
<span class="material-symbols pl-2 text-base">content_copy</span>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
</table>
|
||||
<div class="flex items-center justify-end py-1">
|
||||
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
||||
<p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
|
||||
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -127,12 +127,13 @@ export default {
|
|||
})
|
||||
|
||||
if (!libraryItem) {
|
||||
this.$toast.error('Failed to get library item')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
this.processingGoToTimestamp = false
|
||||
return
|
||||
}
|
||||
if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
|
||||
this.$toast.error('Failed to get podcast episode')
|
||||
console.error('Episode not found in library item', session.episodeId, libraryItem.media.episodes)
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
this.processingGoToTimestamp = false
|
||||
return
|
||||
}
|
||||
|
|
@ -146,7 +147,7 @@ export default {
|
|||
episodeId: episode.id,
|
||||
title: episode.title,
|
||||
subtitle: libraryItem.media.metadata.title,
|
||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: episode.audioFile.duration || null,
|
||||
coverPath: libraryItem.media.coverPath || null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderUsers">
|
||||
<template #header-items>
|
||||
<div v-if="numUsers" class="mx-2 px-1.5 rounded-lg bg-primary/50 text-gray-300/90 text-sm inline-flex items-center justify-center">
|
||||
<span>{{ numUsers }}</span>
|
||||
</div>
|
||||
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/users" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
|
|
@ -13,7 +17,7 @@
|
|||
<ui-btn color="primary" small @click="setShowUserModal()">{{ $strings.ButtonAddUser }}</ui-btn>
|
||||
</template>
|
||||
|
||||
<tables-users-table class="pt-2" @edit="setShowUserModal" />
|
||||
<tables-users-table class="pt-2" @edit="setShowUserModal" @numUsers="(count) => (numUsers = count)" />
|
||||
</app-settings-content>
|
||||
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
|
||||
</div>
|
||||
|
|
@ -29,7 +33,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
selectedAccount: null,
|
||||
showAccountModal: false
|
||||
showAccountModal: false,
|
||||
numUsers: 0
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@
|
|||
<!-- Item Cover Overlay -->
|
||||
<div class="absolute top-0 left-0 w-full h-full z-10 opacity-0 group-hover:opacity-100 pointer-events-none">
|
||||
<div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none">
|
||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="playItem">
|
||||
<button class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" :aria-label="$strings.ButtonPlay" @click.stop.prevent="playItem">
|
||||
<span class="material-symbols fill text-4xl">play_arrow</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="absolute bottom-2.5 right-2.5 z-10 material-symbols text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200 pointer-events-auto" @click="showEditCover">edit</span>
|
||||
<button class="absolute bottom-2.5 right-2.5 z-10 material-symbols text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200 pointer-events-auto" :aria-label="$strings.ButtonEdit" @click="showEditCover">edit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -39,16 +39,11 @@
|
|||
><span :key="index" v-if="index < seriesList.length - 1">, </span>
|
||||
</template>
|
||||
|
||||
<template v-if="!isVideo">
|
||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">{{ $getString('LabelByAuthor', [podcastAuthor]) }}</p>
|
||||
<p v-else-if="musicArtists.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
||||
<nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">, </span></nuxt-link>
|
||||
</p>
|
||||
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
||||
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||
</p>
|
||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||
</template>
|
||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">{{ $getString('LabelByAuthor', [podcastAuthor]) }}</p>
|
||||
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
||||
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||
</p>
|
||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||
|
||||
<content-library-item-details :library-item="libraryItem" />
|
||||
</div>
|
||||
|
|
@ -92,7 +87,7 @@
|
|||
</ui-btn>
|
||||
|
||||
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||
<span v-show="!isStreaming" class="material-symbols text-2xl -ml-2 pr-1 text-white">error</span>
|
||||
<span class="material-symbols text-2xl -ml-2 pr-1 text-white">error</span>
|
||||
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
|
||||
</ui-btn>
|
||||
|
||||
|
|
@ -101,26 +96,26 @@
|
|||
</ui-tooltip>
|
||||
|
||||
<ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
||||
<span class="material-symbols text-2xl -ml-2 pr-2 text-white">auto_stories</span>
|
||||
<span class="material-symbols text-2xl -ml-2 pr-2 text-white" aria-hidden="true">auto_stories</span>
|
||||
{{ $strings.ButtonRead }}
|
||||
</ui-btn>
|
||||
|
||||
<ui-tooltip v-if="userCanUpdate" :text="$strings.LabelEdit" direction="top">
|
||||
<ui-icon-btn icon="" outlined class="mx-0.5" @click="editClick" />
|
||||
<ui-icon-btn icon="" outlined class="mx-0.5" :aria-label="$strings.LabelEdit" @click="editClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="!isPodcast && !isMusic" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||
<ui-tooltip v-if="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- Only admin or root user can download new episodes -->
|
||||
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :aria-label="$strings.LabelFindEpisodes" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction">
|
||||
<template #default="{ showMenu, clickShowMenu, disabled }">
|
||||
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" :aria-label="$strings.LabelMore" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
</button>
|
||||
</template>
|
||||
|
|
@ -220,12 +215,6 @@ export default {
|
|||
isPodcast() {
|
||||
return this.libraryItem.mediaType === 'podcast'
|
||||
},
|
||||
isVideo() {
|
||||
return this.libraryItem.mediaType === 'video'
|
||||
},
|
||||
isMusic() {
|
||||
return this.libraryItem.mediaType === 'music'
|
||||
},
|
||||
isMissing() {
|
||||
return this.libraryItem.isMissing
|
||||
},
|
||||
|
|
@ -240,8 +229,6 @@ export default {
|
|||
},
|
||||
showPlayButton() {
|
||||
if (this.isMissing || this.isInvalid) return false
|
||||
if (this.isMusic) return !!this.audioFile
|
||||
if (this.isVideo) return !!this.videoFile
|
||||
if (this.isPodcast) return this.podcastEpisodes.length
|
||||
return this.tracks.length
|
||||
},
|
||||
|
|
@ -292,9 +279,6 @@ export default {
|
|||
authors() {
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
musicArtists() {
|
||||
return this.mediaMetadata.artists || []
|
||||
},
|
||||
series() {
|
||||
return this.mediaMetadata.series || []
|
||||
},
|
||||
|
|
@ -309,7 +293,7 @@ export default {
|
|||
})
|
||||
},
|
||||
duration() {
|
||||
if (!this.tracks.length && !this.audioFile) return 0
|
||||
if (!this.tracks.length) return 0
|
||||
return this.media.duration
|
||||
},
|
||||
libraryFiles() {
|
||||
|
|
@ -321,18 +305,10 @@ export default {
|
|||
ebookFile() {
|
||||
return this.media.ebookFile
|
||||
},
|
||||
videoFile() {
|
||||
return this.media.videoFile
|
||||
},
|
||||
audioFile() {
|
||||
// Music track
|
||||
return this.media.audioFile
|
||||
},
|
||||
description() {
|
||||
return this.mediaMetadata.description || ''
|
||||
},
|
||||
userMediaProgress() {
|
||||
if (this.isMusic) return null
|
||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||
},
|
||||
userIsFinished() {
|
||||
|
|
@ -484,23 +460,23 @@ export default {
|
|||
this.$axios
|
||||
.$get(`/api/podcasts/${this.libraryItemId}/clear-queue`)
|
||||
.then(() => {
|
||||
this.$toast.success('Episode download queue cleared')
|
||||
this.$toast.success(this.$strings.ToastEpisodeDownloadQueueClearSuccess)
|
||||
this.episodeDownloadQueued = []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to clear queue', error)
|
||||
this.$toast.error('Failed to clear queue')
|
||||
this.$toast.error(this.$strings.ToastEpisodeDownloadQueueClearFailed)
|
||||
})
|
||||
}
|
||||
},
|
||||
async findEpisodesClick() {
|
||||
if (!this.mediaMetadata.feedUrl) {
|
||||
return this.$toast.error('Podcast does not have an RSS Feed')
|
||||
return this.$toast.error(this.$strings.ToastNoRSSFeed)
|
||||
}
|
||||
this.fetchingRSSFeed = true
|
||||
var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => {
|
||||
console.error('Failed to get feed', error)
|
||||
this.$toast.error('Failed to get podcast feed')
|
||||
this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
|
||||
return null
|
||||
})
|
||||
this.fetchingRSSFeed = false
|
||||
|
|
@ -509,7 +485,7 @@ export default {
|
|||
console.log('Podcast feed', payload)
|
||||
const podcastfeed = payload.podcast
|
||||
if (!podcastfeed.episodes || !podcastfeed.episodes.length) {
|
||||
this.$toast.info('No episodes found in RSS feed')
|
||||
this.$toast.info(this.$strings.ToastPodcastNoEpisodesInFeed)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -578,7 +554,7 @@ export default {
|
|||
episodeId: episode.id,
|
||||
title: episode.title,
|
||||
subtitle: this.title,
|
||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: episode.audioFile.duration || null,
|
||||
coverPath: this.libraryItem.media.coverPath || null
|
||||
})
|
||||
|
|
@ -622,13 +598,12 @@ export default {
|
|||
},
|
||||
clearProgressClick() {
|
||||
if (!this.userMediaProgress) return
|
||||
if (confirm(`Are you sure you want to reset your progress?`)) {
|
||||
if (confirm(this.$strings.MessageConfirmResetProgress)) {
|
||||
this.resettingProgress = true
|
||||
this.$axios
|
||||
.$delete(`/api/me/progress/${this.userMediaProgress.id}`)
|
||||
.then(() => {
|
||||
console.log('Progress reset complete')
|
||||
this.$toast.success(`Your progress was reset`)
|
||||
this.resettingProgress = false
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
@ -663,6 +638,11 @@ export default {
|
|||
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
|
||||
}
|
||||
},
|
||||
episodeDownloadQueueCleared(libraryItemId) {
|
||||
if (libraryItemId === this.libraryItemId) {
|
||||
this.episodeDownloadsQueued = []
|
||||
}
|
||||
},
|
||||
rssFeedOpen(data) {
|
||||
if (data.entityId === this.libraryItemId) {
|
||||
console.log('RSS Feed Opened', data)
|
||||
|
|
@ -722,12 +702,12 @@ export default {
|
|||
this.$axios
|
||||
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
|
||||
.then(() => {
|
||||
this.$toast.success('Item deleted')
|
||||
this.$toast.success(this.$strings.ToastItemDeletedSuccess)
|
||||
this.$router.replace(`/library/${this.libraryId}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete item', error)
|
||||
this.$toast.error('Failed to delete item')
|
||||
this.$toast.error(this.$strings.ToastItemDeleteFailed)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
@ -801,6 +781,7 @@ export default {
|
|||
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
|
||||
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
|
||||
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
|
||||
this.$root.socket.on('episode_download_queue_cleared', this.episodeDownloadQueueCleared)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
|
|
@ -812,6 +793,7 @@ export default {
|
|||
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
|
||||
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
|
||||
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
|
||||
this.$root.socket.off('episode_download_queue_cleared', this.episodeDownloadQueueCleared)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
<template>
|
||||
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<app-book-shelf-toolbar page="authors" is-home :authors="authors" />
|
||||
<div id="bookshelf" class="w-full h-full p-8e overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
|
||||
<!-- Cover size widget -->
|
||||
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
|
||||
<div class="flex flex-wrap justify-center">
|
||||
<template v-for="author in authorsSorted">
|
||||
<cards-author-card :key="author.id" :author="author" class="p-3e" @edit="editAuthor" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ store, params, redirect, query, app }) {
|
||||
var libraryId = params.library
|
||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!libraryData) {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
|
||||
const library = libraryData.library
|
||||
if (library.mediaType === 'podcast') {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
return {
|
||||
libraryId
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
authors: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sizeMultiplier() {
|
||||
return this.$store.getters['user/getSizeMultiplier']
|
||||
},
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
selectedAuthor() {
|
||||
return this.$store.state.globals.selectedAuthor
|
||||
},
|
||||
authorSortBy() {
|
||||
return this.$store.getters['user/getUserSetting']('authorSortBy') || 'name'
|
||||
},
|
||||
authorSortDesc() {
|
||||
return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
|
||||
},
|
||||
authorsSorted() {
|
||||
const sortProp = this.authorSortBy
|
||||
const bDesc = this.authorSortDesc ? -1 : 1
|
||||
return this.authors.sort((a, b) => {
|
||||
if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') {
|
||||
return a[sortProp] > b[sortProp] ? bDesc : -bDesc
|
||||
}
|
||||
return a[sortProp]?.localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
this.authors = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/authors`)
|
||||
.then((response) => response.authors)
|
||||
.catch((error) => {
|
||||
console.error('Failed to load authors', error)
|
||||
return []
|
||||
})
|
||||
this.loading = false
|
||||
},
|
||||
authorAdded(author) {
|
||||
if (!this.authors.some((au) => au.id === author.id)) {
|
||||
this.authors.push(author)
|
||||
}
|
||||
},
|
||||
authorUpdated(author) {
|
||||
this.authors = this.authors.map((au) => {
|
||||
if (au.id === author.id) {
|
||||
return author
|
||||
}
|
||||
return au
|
||||
})
|
||||
},
|
||||
authorRemoved(author) {
|
||||
this.authors = this.authors.filter((au) => au.id !== author.id)
|
||||
},
|
||||
editAuthor(author) {
|
||||
this.$store.commit('globals/showEditAuthorModal', author)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
this.$root.socket.on('author_added', this.authorAdded)
|
||||
this.$root.socket.on('author_updated', this.authorUpdated)
|
||||
this.$root.socket.on('author_removed', this.authorRemoved)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.socket.off('author_added', this.authorAdded)
|
||||
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||
this.$root.socket.off('author_removed', this.authorRemoved)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -27,7 +27,7 @@ export default {
|
|||
|
||||
// Redirect podcast libraries
|
||||
const library = libraryData.library
|
||||
if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series')) {
|
||||
if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series' || params.id === 'authors')) {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to updated narrator', error)
|
||||
this.$toast.error('Failed to update narrator')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
|
@ -138,7 +138,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove narrator', error)
|
||||
this.$toast.error('Failed to remove narrator')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
|
@ -158,4 +158,4 @@ export default {
|
|||
},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -104,14 +104,11 @@ export default {
|
|||
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
|
||||
}
|
||||
},
|
||||
episodeDownloadQueueUpdated(downloadQueueDetails) {
|
||||
this.episodeDownloadsQueued = downloadQueueDetails.queue.filter((q) => q.libraryId == this.libraryId)
|
||||
},
|
||||
async loadInitialDownloadQueue() {
|
||||
this.processing = true
|
||||
const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => {
|
||||
console.error('Failed to get download queue', error)
|
||||
this.$toast.error('Failed to get download queue')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return null
|
||||
})
|
||||
this.processing = false
|
||||
|
|
@ -128,7 +125,6 @@ export default {
|
|||
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
|
||||
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
|
||||
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
|
||||
this.$root.socket.on('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -138,7 +134,6 @@ export default {
|
|||
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
|
||||
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
|
||||
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
|
||||
this.$root.socket.off('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
<p dir="auto" class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" />
|
||||
|
||||
<div class="flex items-center">
|
||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress?.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
||||
<span v-if="episodeIdStreaming === episode.id" class="material-symbols text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||
<span v-else class="material-symbols fill text-2xl text-success">play_arrow</span>
|
||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
|
||||
|
|
@ -56,9 +56,10 @@
|
|||
|
||||
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="playerQueueEpisodeIdMap[episode.id] ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" direction="top">
|
||||
<ui-icon-btn :icon="playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick(episode)" />
|
||||
<!-- <button class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
|
||||
<span class="material-symbols text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
|
||||
</button> -->
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip :text="!!episode.progress?.isFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||
<ui-read-icon-btn :disabled="episodesProcessingMap[episode.id]" :is-read="!!episode.progress?.isFinished" borderless class="mx-1 mt-0.5" @click="toggleEpisodeFinished(episode)" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
|
||||
|
|
@ -98,6 +99,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
recentEpisodes: [],
|
||||
episodesProcessingMap: {},
|
||||
totalEpisodes: 0,
|
||||
currentPage: 0,
|
||||
processing: false,
|
||||
|
|
@ -143,6 +145,44 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
async toggleEpisodeFinished(episode, confirmed = false) {
|
||||
if (this.episodesProcessingMap[episode.id]) {
|
||||
console.warn('Episode is already processing')
|
||||
return
|
||||
}
|
||||
|
||||
const isFinished = !!episode.progress?.isFinished
|
||||
const itemProgressPercent = episode.progress?.progress || 0
|
||||
if (!isFinished && itemProgressPercent > 0 && !confirmed) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to mark "${episode.title}" as finished?`,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.toggleEpisodeFinished(episode, true)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
return
|
||||
}
|
||||
|
||||
const updatePayload = {
|
||||
isFinished: !isFinished
|
||||
}
|
||||
|
||||
this.$set(this.episodesProcessingMap, episode.id, true)
|
||||
|
||||
this.$axios
|
||||
.$patch(`/api/me/progress/${episode.libraryItemId}/${episode.id}`, updatePayload)
|
||||
.catch((error) => {
|
||||
console.error('Failed to update progress', error)
|
||||
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this.episodesProcessingMap, episode.id, false)
|
||||
})
|
||||
},
|
||||
clickAddToPlaylist(episode) {
|
||||
// Makeshift libraryItem
|
||||
const libraryItem = {
|
||||
|
|
@ -194,7 +234,7 @@ export default {
|
|||
episodeId: episode.id,
|
||||
title: episode.title,
|
||||
subtitle: episode.podcast.metadata.title,
|
||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: episode.duration || null,
|
||||
coverPath: episode.podcast.coverPath || null
|
||||
})
|
||||
|
|
@ -211,11 +251,10 @@ export default {
|
|||
this.processing = true
|
||||
const episodePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/recent-episodes?limit=25&page=${page}`).catch((error) => {
|
||||
console.error('Failed to get recent episodes', error)
|
||||
this.$toast.error('Failed to get recent episodes')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return null
|
||||
})
|
||||
this.processing = false
|
||||
console.log('Episodes', episodePayload)
|
||||
this.recentEpisodes = episodePayload.episodes || []
|
||||
this.totalEpisodes = episodePayload.total
|
||||
this.currentPage = page
|
||||
|
|
@ -232,7 +271,7 @@ export default {
|
|||
episodeId: episode.id,
|
||||
title: episode.title,
|
||||
subtitle: episode.podcast.metadata.title,
|
||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: episode.duration || null,
|
||||
coverPath: episode.podcast.coverPath || null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
||||
<div class="w-full max-w-4xl mx-auto flex">
|
||||
<form @submit.prevent="submit" class="flex flex-grow">
|
||||
<ui-text-input v-model="searchInput" type="search" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2 text-sm md:text-base" />
|
||||
<ui-text-input v-model="searchInput" type="search" :disabled="processing" :placeholder="$strings.MessagePodcastSearchField" class="flex-grow mr-2 text-sm md:text-base" />
|
||||
<ui-btn type="submit" :disabled="processing" class="hidden md:block">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<ui-btn type="submit" :disabled="processing" class="block md:hidden" small>{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</form>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
<div class="flex items-center">
|
||||
<a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
|
||||
<widgets-explicit-indicator v-if="podcast.explicit" />
|
||||
<widgets-already-in-library-indicator :already-in-library="podcast.alreadyInLibrary" />
|
||||
<widgets-already-in-library-indicator v-if="podcast.alreadyInLibrary" />
|
||||
</div>
|
||||
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">{{ $getString('LabelByAuthor', [podcast.artistName]) }}</p>
|
||||
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
|
||||
|
|
@ -108,7 +108,7 @@ export default {
|
|||
|
||||
if (!txt || !txt.includes('<opml') || !txt.includes('<outline ')) {
|
||||
// Quick lazy check for valid OPML
|
||||
this.$toast.error('Invalid OPML file <opml> tag not found OR an <outline> tag was not found')
|
||||
this.$toast.error(this.$strings.MessageTaskOpmlParseFastFail)
|
||||
this.processing = false
|
||||
return
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ export default {
|
|||
.$post(`/api/podcasts/opml/parse`, { opmlText: txt })
|
||||
.then((data) => {
|
||||
if (!data.feeds?.length) {
|
||||
this.$toast.error('No feeds found in OPML file')
|
||||
this.$toast.error(this.$strings.MessageTaskOpmlParseNoneFound)
|
||||
} else {
|
||||
this.opmlFeeds = data.feeds || []
|
||||
this.showOPMLFeedsModal = true
|
||||
|
|
@ -125,7 +125,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error('Failed to parse OPML file')
|
||||
this.$toast.error(this.$strings.MessageTaskOpmlParseFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
|
@ -146,7 +146,7 @@ export default {
|
|||
this.processing = true
|
||||
var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed }).catch((error) => {
|
||||
console.error('Failed to get feed', error)
|
||||
this.$toast.error('Failed to get podcast feed')
|
||||
this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
|
||||
return null
|
||||
})
|
||||
this.processing = false
|
||||
|
|
@ -191,13 +191,13 @@ export default {
|
|||
return
|
||||
}
|
||||
if (!podcast.feedUrl) {
|
||||
this.$toast.error('Invalid podcast - no feed')
|
||||
this.$toast.error(this.$strings.MessageNoPodcastFeed)
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
const payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => {
|
||||
console.error('Failed to get feed', error)
|
||||
this.$toast.error('Failed to get podcast feed')
|
||||
this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
|
||||
return null
|
||||
})
|
||||
this.processing = false
|
||||
|
|
@ -211,15 +211,15 @@ export default {
|
|||
async fetchExistentPodcastsInYourLibrary() {
|
||||
this.processing = true
|
||||
|
||||
const podcasts = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/items?page=0&minified=1`).catch((error) => {
|
||||
const podcastsResponse = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/podcast-titles`).catch((error) => {
|
||||
console.error('Failed to fetch podcasts', error)
|
||||
return []
|
||||
})
|
||||
this.existentPodcasts = podcasts.results.map((p) => {
|
||||
this.existentPodcasts = podcastsResponse.podcasts.map((p) => {
|
||||
return {
|
||||
title: p.media.metadata.title.toLowerCase(),
|
||||
itunesId: p.media.metadata.itunesId,
|
||||
id: p.id
|
||||
title: p.title.toLowerCase(),
|
||||
itunesId: p.itunesId,
|
||||
id: p.libraryItemId
|
||||
}
|
||||
})
|
||||
this.processing = false
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
<div :key="author.id" class="w-full py-2">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm text-white text-opacity-70 w-36 pr-2 truncate">
|
||||
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
||||
{{ index + 1 }}. <nuxt-link :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
||||
</p>
|
||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
|
||||
|
|
|
|||
|
|
@ -132,11 +132,11 @@ export default {
|
|||
methods: {
|
||||
async submitServerSetup() {
|
||||
if (!this.newRoot.username || !this.newRoot.username.trim()) {
|
||||
this.$toast.error('Must enter a root username')
|
||||
this.$toast.error(this.$strings.ToastUserRootRequireName)
|
||||
return
|
||||
}
|
||||
if (this.newRoot.password !== this.confirmPassword) {
|
||||
this.$toast.error('Password mismatch')
|
||||
this.$toast.error(this.$strings.ToastUserPasswordMismatch)
|
||||
return
|
||||
}
|
||||
if (!this.newRoot.password) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
||||
<span v-show="!streaming" class="material-symbols fill text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlayAll }}
|
||||
</ui-btn>
|
||||
|
||||
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<p v-if="mediaItemShare.playbackSession.displayAuthor" class="text-lg lg:text-xl text-slate-400 font-semibold text-center mb-1 truncate">{{ mediaItemShare.playbackSession.displayAuthor }}</p>
|
||||
|
||||
<div class="w-full pt-16">
|
||||
<player-ui ref="audioPlayer" :chapters="chapters" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
|
||||
<player-ui ref="audioPlayer" :chapters="chapters" :current-chapter="currentChapter" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -51,7 +51,8 @@ export default {
|
|||
windowHeight: 0,
|
||||
listeningTimeSinceSync: 0,
|
||||
coverRgb: null,
|
||||
coverBgIsLight: false
|
||||
coverBgIsLight: false,
|
||||
currentTime: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -60,16 +61,10 @@ export default {
|
|||
},
|
||||
coverUrl() {
|
||||
if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `http://localhost:3333/public/share/${this.mediaItemShare.slug}/cover`
|
||||
}
|
||||
return `/public/share/${this.mediaItemShare.slug}/cover`
|
||||
return `${this.$config.routerBasePath}/public/share/${this.mediaItemShare.slug}/cover`
|
||||
},
|
||||
audioTracks() {
|
||||
return (this.playbackSession.audioTracks || []).map((track) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
track.contentUrl = `${process.env.serverUrl}${track.contentUrl}`
|
||||
}
|
||||
track.relativeContentUrl = track.contentUrl
|
||||
return track
|
||||
})
|
||||
|
|
@ -83,6 +78,9 @@ export default {
|
|||
chapters() {
|
||||
return this.playbackSession.chapters || []
|
||||
},
|
||||
currentChapter() {
|
||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||
},
|
||||
coverAspectRatio() {
|
||||
const coverAspectRatio = this.playbackSession.coverAspectRatio
|
||||
return coverAspectRatio === this.$constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
|
||||
|
|
@ -128,12 +126,14 @@ export default {
|
|||
if (!this.localAudioPlayer || !this.hasLoaded) return
|
||||
const currentTime = this.localAudioPlayer.getCurrentTime()
|
||||
const duration = this.localAudioPlayer.getDuration()
|
||||
this.seek(Math.min(currentTime + 10, duration))
|
||||
const jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount') || 10
|
||||
this.seek(Math.min(currentTime + jumpForwardAmount, duration))
|
||||
},
|
||||
jumpBackward() {
|
||||
if (!this.localAudioPlayer || !this.hasLoaded) return
|
||||
const currentTime = this.localAudioPlayer.getCurrentTime()
|
||||
this.seek(Math.max(currentTime - 10, 0))
|
||||
const jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount') || 10
|
||||
this.seek(Math.max(currentTime - jumpBackwardAmount, 0))
|
||||
},
|
||||
setVolume(volume) {
|
||||
if (!this.localAudioPlayer || !this.hasLoaded) return
|
||||
|
|
@ -154,6 +154,7 @@ export default {
|
|||
|
||||
// Update UI
|
||||
this.$refs.audioPlayer.setCurrentTime(time)
|
||||
this.currentTime = time
|
||||
},
|
||||
setDuration() {
|
||||
if (!this.localAudioPlayer) return
|
||||
|
|
@ -249,6 +250,8 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('user/loadUserSettings')
|
||||
|
||||
this.resize()
|
||||
window.addEventListener('resize', this.resize)
|
||||
window.addEventListener('keydown', this.keyDown)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<div id="page-wrapper" class="page p-0 sm:p-6 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<div id="page-wrapper" class="page p-1 sm:p-6 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<div class="w-full max-w-6xl mx-auto">
|
||||
<!-- Library & folder picker -->
|
||||
<div class="flex my-6 -mx-2">
|
||||
<div class="w-1/5 px-2">
|
||||
<div class="flex flex-wrap my-6 md:-mx-2">
|
||||
<div class="w-full md:w-1/5 px-2">
|
||||
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" :label="$strings.LabelLibrary" :disabled="!!items.length" @input="libraryChanged" />
|
||||
</div>
|
||||
<div class="w-3/5 px-2">
|
||||
<div class="w-full md:w-3/5 px-2">
|
||||
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" :label="$strings.LabelFolder" />
|
||||
</div>
|
||||
<div class="w-1/5 px-2">
|
||||
<div class="w-full md:w-1/5 px-2">
|
||||
<ui-text-input-with-label :value="selectedLibraryMediaType" readonly :label="$strings.LabelMediaType" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6">
|
||||
<div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6 px-2 md:px-0">
|
||||
<label class="flex cursor-pointer pt-4">
|
||||
<ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
|
||||
<span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
|
||||
|
|
@ -33,13 +33,13 @@
|
|||
</widgets-alert>
|
||||
|
||||
<!-- Picker display -->
|
||||
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
|
||||
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}</p>
|
||||
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-4 md:px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
|
||||
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : isIOS ? $strings.LabelUploaderDragAndDropFilesOnly : $strings.LabelUploaderDragAndDrop }}</p>
|
||||
<p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p>
|
||||
<div class="w-full max-w-xl mx-auto">
|
||||
<div class="flex">
|
||||
<ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn>
|
||||
<ui-btn class="w-full mx-1" @click="openFolderPicker">{{ $strings.ButtonChooseAFolder }}</ui-btn>
|
||||
<ui-btn v-if="!isIOS" class="w-full mx-1" @click="openFolderPicker">{{ $strings.ButtonChooseAFolder }} </ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-8 text-center">
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
</p>
|
||||
|
||||
<p class="text-sm text-white text-opacity-70">
|
||||
{{ $strings.NoteUploaderFoldersWithMediaFiles }} <span v-if="selectedLibraryMediaType === 'book'">{{ $strings.NoteUploaderOnlyAudioFiles }}</span>
|
||||
<span v-if="!isIOS">{{ $strings.NoteUploaderFoldersWithMediaFiles }}</span> <span v-if="selectedLibraryMediaType === 'book'">{{ $strings.NoteUploaderOnlyAudioFiles }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -84,8 +84,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<input ref="fileInput" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
|
||||
<input ref="fileFolderInput" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
|
||||
<input ref="fileInput" type="file" multiple :accept="isIOS ? '' : inputAccept" class="hidden" @change="inputChanged" />
|
||||
<input ref="fileFolderInput" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" v-if="!isIOS" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -127,6 +127,10 @@ export default {
|
|||
})
|
||||
return extensions
|
||||
},
|
||||
isIOS() {
|
||||
const ua = window.navigator.userAgent
|
||||
return /iPad|iPhone|iPod/.test(ua) && !window.MSStream
|
||||
},
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
},
|
||||
|
|
@ -384,12 +388,6 @@ export default {
|
|||
else itemsFailed++
|
||||
this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
|
||||
}
|
||||
if (itemsUploaded) {
|
||||
this.$toast.success(`Successfully uploaded ${itemsUploaded} item${itemsUploaded > 1 ? 's' : ''}`)
|
||||
}
|
||||
if (itemsFailed) {
|
||||
this.$toast.success(`Failed to upload ${itemsFailed} item${itemsFailed > 1 ? 's' : ''}`)
|
||||
}
|
||||
this.processing = false
|
||||
this.uploadFinished = true
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue