mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-26 08:12:25 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			332 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			332 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|   <div>
 | |
|     <app-settings-content :header-text="$strings.HeaderEmailSettings" :description="''">
 | |
|       <template #header-items>
 | |
|         <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
 | |
|           <a href="https://www.audiobookshelf.org/guides/send_to_ereader" target="_blank" class="inline-flex">
 | |
|             <span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
 | |
|           </a>
 | |
|         </ui-tooltip>
 | |
|       </template>
 | |
| 
 | |
|       <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">
 | |
|           <div class="w-full md:w-1/2 px-1">
 | |
|             <!-- secure toggle -->
 | |
|             <div class="flex items-center">
 | |
|               <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-symbols text-lg pl-1">info</span>
 | |
|                 </div>
 | |
|               </ui-tooltip>
 | |
|             </div>
 | |
|           </div>
 | |
|           <div class="w-full md:w-1/2 px-1">
 | |
|             <!-- reject unauthorized toggle -->
 | |
|             <div class="flex items-center">
 | |
|               <ui-toggle-switch labeledBy="email-settings-reject-unauthorized" v-model="newSettings.rejectUnauthorized" :disabled="savingSettings" />
 | |
|               <ui-tooltip :text="$strings.LabelEmailSettingsRejectUnauthorizedHelp">
 | |
|                 <div class="pl-4 flex items-center">
 | |
|                   <span id="email-settings-reject-unauthorized">{{ $strings.LabelEmailSettingsRejectUnauthorized }}</span>
 | |
|                   <span class="material-symbols text-lg pl-1">info</span>
 | |
|                 </div>
 | |
|               </ui-tooltip>
 | |
|             </div>
 | |
|           </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="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 class="w-full md:w-1/2 px-1">
 | |
|             <ui-text-input-with-label ref="testInput" v-model="newSettings.testAddress" :disabled="savingSettings" :label="$strings.LabelEmailSettingsTestAddress" />
 | |
|           </div>
 | |
|         </div>
 | |
| 
 | |
|         <div class="flex items-center justify-between pt-4">
 | |
|           <ui-btn v-if="hasUpdates" :disabled="savingSettings" type="button" @click="resetChanges">{{ $strings.ButtonReset }}</ui-btn>
 | |
|           <ui-btn v-else :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/25 flex items-center justify-center">
 | |
|         <ui-loading-indicator />
 | |
|       </div>
 | |
|     </app-settings-content>
 | |
| 
 | |
|     <app-settings-content :header-text="$strings.HeaderEreaderDevices" :description="$strings.MessageEreaderDevices">
 | |
|       <template #header-items>
 | |
|         <div class="grow" />
 | |
| 
 | |
|         <ui-btn color="bg-primary" small @click="addNewDeviceClick">{{ $strings.ButtonAddDevice }}</ui-btn>
 | |
|       </template>
 | |
| 
 | |
|       <table v-if="existingEReaderDevices.length" class="tracksTable mt-4">
 | |
|         <tr>
 | |
|           <th class="text-left">{{ $strings.LabelName }}</th>
 | |
|           <th class="text-left">{{ $strings.LabelEmail }}</th>
 | |
|           <th class="text-left">{{ $strings.LabelAccessibleBy }}</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="text-left">
 | |
|             <p class="text-sm md:text-base text-gray-100">{{ getAccessibleBy(device) }}</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-if="!loading" class="text-center py-4">
 | |
|         <p class="text-lg text-gray-100">{{ $strings.MessageNoDevices }}</p>
 | |
|       </div>
 | |
|     </app-settings-content>
 | |
| 
 | |
|     <modals-emails-e-reader-device-modal v-model="showEReaderDeviceModal" :users="users" :existing-devices="existingEReaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" :loadUsers="loadUsers" />
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <script>
 | |
| export default {
 | |
|   asyncData({ store, redirect }) {
 | |
|     if (!store.getters['user/getIsAdminOrUp']) {
 | |
|       redirect('/')
 | |
|     }
 | |
|   },
 | |
|   data() {
 | |
|     return {
 | |
|       users: [],
 | |
|       loading: false,
 | |
|       savingSettings: false,
 | |
|       sendingTest: false,
 | |
|       deletingDeviceName: null,
 | |
|       settings: null,
 | |
|       newSettings: {
 | |
|         host: null,
 | |
|         port: 465,
 | |
|         secure: true,
 | |
|         rejectUnauthorized: true,
 | |
|         user: null,
 | |
|         pass: null,
 | |
|         testAddress: 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: {
 | |
|     resetChanges() {
 | |
|       this.newSettings = {
 | |
|         ...this.settings
 | |
|       }
 | |
|     },
 | |
|     async loadUsers() {
 | |
|       if (this.users.length) return
 | |
|       this.users = await this.$axios
 | |
|         .$get('/api/users')
 | |
|         .then((res) => {
 | |
|           return res.users.sort((a, b) => {
 | |
|             return a.createdAt - b.createdAt
 | |
|           })
 | |
|         })
 | |
|         .catch((error) => {
 | |
|           console.error('Failed', error)
 | |
|           this.$toast.error(this.$strings.ToastFailedToLoadData)
 | |
|           return []
 | |
|         })
 | |
|     },
 | |
|     getAccessibleBy(device) {
 | |
|       const user = device.availabilityOption
 | |
|       if (user === 'userOrUp') return 'Users (excluding Guests)'
 | |
|       if (user === 'guestOrUp') return 'Users (including Guests)'
 | |
|       if (user === 'specificUsers') {
 | |
|         return device.users.map((id) => this.users.find((u) => u.id === id)?.username).join(', ')
 | |
|       }
 | |
|       return 'Admins Only'
 | |
|     },
 | |
|     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.existingEReaderDevices.filter((d) => d.name !== device.name)
 | |
|       }
 | |
|       this.deletingDeviceName = device.name
 | |
|       this.$axios
 | |
|         .$post(`/api/emails/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.settings.ereaderDevices = ereaderDevices
 | |
|       this.newSettings.ereaderDevices = ereaderDevices.map((d) => ({ ...d }))
 | |
| 
 | |
|       // Load users if a device has availability set to specific users
 | |
|       if (ereaderDevices.some((device) => device.availabilityOption === 'specificUsers')) {
 | |
|         this.loadUsers()
 | |
|       }
 | |
|     },
 | |
|     addNewDeviceClick() {
 | |
|       this.selectedEReaderDevice = null
 | |
|       this.showEReaderDeviceModal = true
 | |
|     },
 | |
|     sendTestClick() {
 | |
|       this.sendingTest = true
 | |
|       this.$axios
 | |
|         .$post('/api/emails/test')
 | |
|         .then(() => {
 | |
|           this.$toast.success(this.$strings.ToastDeviceTestEmailSuccess)
 | |
|         })
 | |
|         .catch((error) => {
 | |
|           console.error('Failed to send test email', error)
 | |
|           const errorMsg = error.response.data || this.$strings.ToastDeviceTestEmailFailed
 | |
|           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,
 | |
|         rejectUnauthorized: this.newSettings.rejectUnauthorized,
 | |
|         user: this.newSettings.user,
 | |
|         pass: this.newSettings.pass,
 | |
|         testAddress: this.newSettings.testAddress,
 | |
|         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(this.$strings.ToastEmailSettingsUpdateSuccess)
 | |
|         })
 | |
|         .catch((error) => {
 | |
|           console.error('Failed to update email settings', error)
 | |
|           this.$toast.error(this.$strings.ToastFailedToUpdate)
 | |
|         })
 | |
|         .finally(() => {
 | |
|           this.savingSettings = false
 | |
|         })
 | |
|     },
 | |
|     init() {
 | |
|       this.loading = true
 | |
| 
 | |
|       this.$axios
 | |
|         .$get(`/api/emails/settings`)
 | |
|         .then(async (data) => {
 | |
|           // Load users if a device has availability set to specific users
 | |
|           if (data.settings.ereaderDevices.some((device) => device.availabilityOption === 'specificUsers')) {
 | |
|             await this.loadUsers()
 | |
|           }
 | |
| 
 | |
|           this.settings = data.settings
 | |
|           this.newSettings = {
 | |
|             ...this.settings
 | |
|           }
 | |
|         })
 | |
|         .catch((error) => {
 | |
|           console.error('Failed to get email settings', error)
 | |
|           this.$toast.error(this.$strings.ToastFailedToLoadData)
 | |
|         })
 | |
|         .finally(() => {
 | |
|           this.loading = false
 | |
|         })
 | |
|     }
 | |
|   },
 | |
|   mounted() {
 | |
|     this.init()
 | |
|   },
 | |
|   beforeDestroy() {}
 | |
| }
 | |
| </script>
 |