mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-04 10:14:36 +02:00
Add:Email smtp config & send ebooks to devices #1474
This commit is contained in:
parent
15aaf2863c
commit
05ce9c6eda
40 changed files with 1077 additions and 99 deletions
|
@ -145,7 +145,7 @@ export default {
|
|||
feed: this.rssFeed
|
||||
})
|
||||
},
|
||||
contextMenuAction(action) {
|
||||
contextMenuAction({ action }) {
|
||||
if (action === 'delete') {
|
||||
this.removeClick()
|
||||
} else if (action === 'create-playlist') {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="configContent" :class="`page-${currentPage}`">
|
||||
<div v-show="isMobilePortrait" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2 cursor-pointer" @click.stop.prevent="toggleShowMore">
|
||||
<span class="material-icons text-2xl cursor-pointer">arrow_forward</span>
|
||||
<p class="pl-3 capitalize">{{ $strings.HeaderSettings }}</p>
|
||||
<p class="pl-3 capitalize">{{ currentPage }}</p>
|
||||
</div>
|
||||
<nuxt-child />
|
||||
</div>
|
||||
|
@ -55,6 +55,7 @@ export default {
|
|||
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
||||
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||
else if (pageName === 'email') return this.$strings.HeaderEmail
|
||||
}
|
||||
return this.$strings.HeaderSettings
|
||||
}
|
||||
|
@ -79,14 +80,6 @@ export default {
|
|||
width: 900px;
|
||||
max-width: calc(100% - 176px);
|
||||
}
|
||||
.configContent.page-library-stats {
|
||||
width: 1200px;
|
||||
}
|
||||
@media (max-width: 1550px) {
|
||||
.configContent.page-library-stats {
|
||||
margin-left: 176px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1240px) {
|
||||
.configContent {
|
||||
margin-left: 176px;
|
||||
|
@ -98,8 +91,5 @@ export default {
|
|||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.configContent.page-library-stats {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
244
client/pages/config/email.vue
Normal file
244
client/pages/config/email.vue
Normal file
|
@ -0,0 +1,244 @@
|
|||
<template>
|
||||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderEmailSettings" :description="''">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="flex items-center -mx-1 mb-2">
|
||||
<div class="w-full md:w-3/4 px-1">
|
||||
<ui-text-input-with-label ref="hostInput" v-model="newSettings.host" :disabled="savingSettings" :label="$strings.LabelHost" />
|
||||
</div>
|
||||
<div class="w-full md:w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="portInput" v-model="newSettings.port" type="number" :disabled="savingSettings" :label="$strings.LabelPort" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-2 py-3">
|
||||
<ui-toggle-switch labeledBy="email-settings-secure" v-model="newSettings.secure" :disabled="savingSettings" />
|
||||
<ui-tooltip :text="$strings.LabelEmailSettingsSecureHelp">
|
||||
<div class="pl-4 flex items-center">
|
||||
<span id="email-settings-secure">{{ $strings.LabelEmailSettingsSecure }}</span>
|
||||
<span class="material-icons text-lg pl-1">info_outlined</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center -mx-1 mb-2">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="userInput" v-model="newSettings.user" :disabled="savingSettings" :label="$strings.LabelUsername" />
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="passInput" v-model="newSettings.pass" type="password" :disabled="savingSettings" :label="$strings.LabelPassword" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center -mx-1 mb-2">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="fromInput" v-model="newSettings.fromAddress" :disabled="savingSettings" :label="$strings.LabelEmailSettingsFromAddress" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-4">
|
||||
<ui-btn :loading="sendingTest" :disabled="savingSettings || !newSettings.host" type="button" @click="sendTestClick">{{ $strings.ButtonTest }}</ui-btn>
|
||||
<ui-btn :loading="savingSettings" :disabled="!hasUpdates" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-show="loading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-25 flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
<app-settings-content :header-text="$strings.HeaderEReaderDevices" showAddButton :description="''" @clicked="addNewDeviceClick">
|
||||
<table v-if="existingEReaderDevices.length" class="tracksTable my-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 existingEReaderDevices" :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" class="mx-1" @click="editDeviceClick(device)" />
|
||||
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name" @click="deleteDeviceClick(device)" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div v-else class="text-center py-4">
|
||||
<p class="text-lg text-gray-100">No Devices</p>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
<modals-emails-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="existingEReaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
savingSettings: false,
|
||||
sendingTest: false,
|
||||
deletingDeviceName: null,
|
||||
settings: null,
|
||||
newSettings: {
|
||||
host: null,
|
||||
port: 465,
|
||||
secure: true,
|
||||
user: null,
|
||||
pass: null,
|
||||
fromAddress: null
|
||||
},
|
||||
newEReaderDevice: {
|
||||
name: '',
|
||||
email: ''
|
||||
},
|
||||
selectedEReaderDevice: null,
|
||||
showEReaderDeviceModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasUpdates() {
|
||||
if (!this.settings) return true
|
||||
for (const key in this.newSettings) {
|
||||
if (key === 'ereaderDevices') continue
|
||||
if (this.newSettings[key] !== this.settings[key]) return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
existingEReaderDevices() {
|
||||
return this.settings?.ereaderDevices || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editDeviceClick(device) {
|
||||
this.selectedEReaderDevice = device
|
||||
this.showEReaderDeviceModal = true
|
||||
},
|
||||
deleteDeviceClick(device) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to delete e-reader device "${device.name}"?`,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteDevice(device)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
deleteDevice(device) {
|
||||
const payload = {
|
||||
ereaderDevices: this.existingEReaderDevices.filter((d) => d.name !== device.name)
|
||||
}
|
||||
this.deletingDeviceName = device.name
|
||||
this.$axios
|
||||
.$patch(`/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')
|
||||
})
|
||||
.finally(() => {
|
||||
this.deletingDeviceName = null
|
||||
})
|
||||
},
|
||||
ereaderDevicesUpdated(ereaderDevices) {
|
||||
this.settings.ereaderDevices = ereaderDevices
|
||||
this.newSettings.ereaderDevices = ereaderDevices.map((d) => ({ ...d }))
|
||||
},
|
||||
addNewDeviceClick() {
|
||||
this.selectedEReaderDevice = null
|
||||
this.showEReaderDeviceModal = true
|
||||
},
|
||||
sendTestClick() {
|
||||
this.sendingTest = true
|
||||
this.$axios
|
||||
.$post('/api/emails/test')
|
||||
.then(() => {
|
||||
this.$toast.success('Test Email Sent')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to send test email', error)
|
||||
const errorMsg = error.response.data || 'Failed to send test email'
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
this.sendingTest = false
|
||||
})
|
||||
},
|
||||
validateForm() {
|
||||
for (const ref of [this.$refs.hostInput, this.$refs.portInput, this.$refs.userInput, this.$refs.passInput, this.$refs.fromInput]) {
|
||||
if (ref?.blur) ref.blur()
|
||||
}
|
||||
|
||||
if (this.newSettings.port) {
|
||||
this.newSettings.port = Number(this.newSettings.port)
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.validateForm()) return
|
||||
|
||||
const updatePayload = {
|
||||
host: this.newSettings.host,
|
||||
port: this.newSettings.port,
|
||||
secure: this.newSettings.secure,
|
||||
user: this.newSettings.user,
|
||||
pass: this.newSettings.pass,
|
||||
fromAddress: this.newSettings.fromAddress
|
||||
}
|
||||
this.savingSettings = true
|
||||
this.$axios
|
||||
.$patch('/api/emails/settings', updatePayload)
|
||||
.then((data) => {
|
||||
this.settings = data.settings
|
||||
this.newSettings = {
|
||||
...data.settings
|
||||
}
|
||||
this.$toast.success('Email settings updated')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update email settings', error)
|
||||
this.$toast.error('Failed to update email settings')
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingSettings = false
|
||||
})
|
||||
},
|
||||
init() {
|
||||
this.loading = true
|
||||
|
||||
this.$axios
|
||||
.$get(`/api/emails/settings`)
|
||||
.then((data) => {
|
||||
this.settings = data.settings
|
||||
this.newSettings = {
|
||||
...this.settings
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get email settings', error)
|
||||
this.$toast.error('Failed to load email settings')
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
|
@ -431,6 +431,19 @@ export default {
|
|||
})
|
||||
}
|
||||
|
||||
if (this.ebookFile && this.$store.state.libraries.ereaderDevices?.length) {
|
||||
items.push({
|
||||
text: this.$strings.LabelSendEbookToDevice,
|
||||
subitems: this.$store.state.libraries.ereaderDevices.map((d) => {
|
||||
return {
|
||||
text: d.name,
|
||||
action: 'sendToDevice',
|
||||
data: d.name
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (this.userCanDelete) {
|
||||
items.push({
|
||||
text: this.$strings.ButtonDelete,
|
||||
|
@ -704,7 +717,35 @@ export default {
|
|||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
contextMenuAction(action) {
|
||||
sendToDevice(deviceName) {
|
||||
const payload = {
|
||||
message: this.$getString('MessageConfirmSendEbookToDevice', [this.ebookFile.ebookFormat, this.title, deviceName]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
const payload = {
|
||||
libraryItemId: this.libraryItemId,
|
||||
deviceName
|
||||
}
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$post(`/api/emails/send-ebook-to-device`, payload)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$getString('ToastSendEbookToDeviceSuccess', [deviceName]))
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to send e-book to device', error)
|
||||
this.$toast.error(this.$strings.ToastSendEbookToDeviceFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
contextMenuAction({ action, data }) {
|
||||
if (action === 'collections') {
|
||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||
this.$store.commit('globals/setShowCollectionsModal', true)
|
||||
|
@ -719,6 +760,8 @@ export default {
|
|||
this.downloadLibraryItem()
|
||||
} else if (action === 'delete') {
|
||||
this.deleteLibraryItem()
|
||||
} else if (action === 'sendToDevice') {
|
||||
this.sendToDevice(data)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -107,7 +107,7 @@ export default {
|
|||
const payload = {
|
||||
newRoot: { ...this.newRoot }
|
||||
}
|
||||
var success = await this.$axios
|
||||
const success = await this.$axios
|
||||
.$post('/init', payload)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
|
@ -124,9 +124,10 @@ export default {
|
|||
|
||||
location.reload()
|
||||
},
|
||||
setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
|
||||
setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) {
|
||||
this.$store.commit('setServerSettings', serverSettings)
|
||||
this.$store.commit('setSource', Source)
|
||||
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
|
||||
this.$setServerLanguageCode(serverSettings.language)
|
||||
|
||||
if (serverSettings.chromecastEnabled) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue