mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-05 23:49:41 +00:00
Merge remote-tracking branch 'remotes/upstream/master'
# Conflicts: # client/components/cards/LazyBookCard.vue # client/components/cards/LazySeriesCard.vue
This commit is contained in:
commit
557fc6dc64
198 changed files with 8256 additions and 1566 deletions
|
|
@ -18,7 +18,7 @@
|
|||
<div class="w-12 hidden lg:block" />
|
||||
<p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
|
||||
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" :label="$strings.LabelShowSeconds" class="mx-2" />
|
||||
<div class="w-32 hidden lg:block" />
|
||||
</div>
|
||||
<div class="flex items-center mb-3 py-1 -mx-1">
|
||||
|
|
@ -639,4 +639,4 @@ export default {
|
|||
this.destroyAudioEl()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">{{ $strings.LabelDescription }}</p>
|
||||
<p ref="description" id="author-description" class="text-white max-w-3xl text-base whitespace-pre-wrap" :class="{ 'show-full': showFullDescription }">{{ author.description }}</p>
|
||||
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">
|
||||
{{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
|
||||
{{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -140,4 +140,4 @@ export default {
|
|||
-webkit-line-clamp: unset;
|
||||
max-height: 999rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -58,29 +58,53 @@
|
|||
|
||||
<ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" />
|
||||
|
||||
<ui-dropdown v-if="openIdSigningAlgorithmsSupportedByIssuer.length" v-model="newAuthSettings.authOpenIDTokenSigningAlgorithm" :items="openIdSigningAlgorithmsSupportedByIssuer" :label="'Signing Algorithm'" :disabled="savingSettings" class="mb-2" />
|
||||
<ui-text-input-with-label v-else ref="openidTokenSigningAlgorithm" v-model="newAuthSettings.authOpenIDTokenSigningAlgorithm" :disabled="savingSettings" :label="'Signing Algorithm'" class="mb-2" />
|
||||
|
||||
<ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" />
|
||||
<p class="pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" />
|
||||
<p class="sm:pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" />
|
||||
|
||||
<ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" />
|
||||
|
||||
<div class="flex items-center pt-1 mb-2">
|
||||
<div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
|
||||
<div class="w-44">
|
||||
<ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" :label="$strings.LabelMatchExistingUsersBy" :disabled="savingSettings" />
|
||||
</div>
|
||||
<p class="pl-4 text-sm text-gray-300 mt-5">{{ $strings.LabelMatchExistingUsersByDescription }}</p>
|
||||
<p class="sm:pl-4 text-sm text-gray-300 mt-2 sm:mt-5">{{ $strings.LabelMatchExistingUsersByDescription }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-4 px-1">
|
||||
<div class="flex items-center py-4 px-1 w-full">
|
||||
<ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" />
|
||||
<p id="auto-redirect-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoLaunch }}</p>
|
||||
<p class="pl-4 text-sm text-gray-300" v-html="$strings.LabelAutoLaunchDescription" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-4 px-1">
|
||||
<div class="flex items-center py-4 px-1 w-full">
|
||||
<ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" />
|
||||
<p id="auto-register-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoRegister }}</p>
|
||||
<p class="pl-4 text-sm text-gray-300">{{ $strings.LabelAutoRegisterDescription }}</p>
|
||||
</div>
|
||||
|
||||
<p class="pt-6 mb-4 px-1">{{ $strings.LabelOpenIDClaims }}</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row mb-4">
|
||||
<div class="w-44 min-w-44">
|
||||
<ui-text-input-with-label ref="openidGroupClaim" v-model="newAuthSettings.authOpenIDGroupClaim" :disabled="savingSettings" :placeholder="'groups'" :label="'Group Claim'" />
|
||||
</div>
|
||||
<p class="sm:pl-4 pt-2 sm:pt-0 text-sm text-gray-300" v-html="$strings.LabelOpenIDGroupClaimDescription"></p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row mb-4">
|
||||
<div class="w-44 min-w-44">
|
||||
<ui-text-input-with-label ref="openidAdvancedPermsClaim" v-model="newAuthSettings.authOpenIDAdvancedPermsClaim" :disabled="savingSettings" :placeholder="'abspermissions'" :label="'Advanced Permission Claim'" />
|
||||
</div>
|
||||
<div class="sm:pl-4 pt-2 sm:pt-0 text-sm text-gray-300">
|
||||
<p v-html="$strings.LabelOpenIDAdvancedPermsClaimDescription"></p>
|
||||
<pre class="text-pre-wrap mt-2"
|
||||
>{{ newAuthSettings.authOpenIDSamplePermissions }}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
|
@ -117,6 +141,7 @@ export default {
|
|||
enableOpenIDAuth: false,
|
||||
showCustomLoginMessage: false,
|
||||
savingSettings: false,
|
||||
openIdSigningAlgorithmsSupportedByIssuer: [],
|
||||
newAuthSettings: {}
|
||||
}
|
||||
},
|
||||
|
|
@ -157,6 +182,22 @@ export default {
|
|||
this.newAuthSettings.authOpenIDIssuerURL = this.newAuthSettings.authOpenIDIssuerURL.replace('/.well-known/openid-configuration', '')
|
||||
}
|
||||
|
||||
const setSupportedSigningAlgorithms = (algorithms) => {
|
||||
if (!algorithms?.length || !Array.isArray(algorithms)) {
|
||||
console.warn('Invalid id_token_signing_alg_values_supported from openid-configuration', algorithms)
|
||||
this.openIdSigningAlgorithmsSupportedByIssuer = []
|
||||
return
|
||||
}
|
||||
this.openIdSigningAlgorithmsSupportedByIssuer = algorithms
|
||||
|
||||
// If a signing algorithm is already selected, then keep it, when it is still supported.
|
||||
// But if it is not supported, then select one of the supported ones.
|
||||
let currentAlgorithm = this.newAuthSettings.authOpenIDTokenSigningAlgorithm
|
||||
if (!algorithms.includes(currentAlgorithm)) {
|
||||
this.newAuthSettings.authOpenIDTokenSigningAlgorithm = algorithms[0]
|
||||
}
|
||||
}
|
||||
|
||||
this.$axios
|
||||
.$get(`/auth/openid/config?issuer=${issuerUrl}`)
|
||||
.then((data) => {
|
||||
|
|
@ -166,6 +207,7 @@ export default {
|
|||
if (data.userinfo_endpoint) this.newAuthSettings.authOpenIDUserInfoURL = data.userinfo_endpoint
|
||||
if (data.end_session_endpoint) this.newAuthSettings.authOpenIDLogoutURL = data.end_session_endpoint
|
||||
if (data.jwks_uri) this.newAuthSettings.authOpenIDJwksURL = data.jwks_uri
|
||||
if (data.id_token_signing_alg_values_supported) setSupportedSigningAlgorithms(data.id_token_signing_alg_values_supported)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to receive data', error)
|
||||
|
|
@ -203,6 +245,10 @@ export default {
|
|||
this.$toast.error('Client Secret required')
|
||||
isValid = false
|
||||
}
|
||||
if (!this.newAuthSettings.authOpenIDTokenSigningAlgorithm) {
|
||||
this.$toast.error('Signing Algorithm required')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
function isValidRedirectURI(uri) {
|
||||
// Check for somestring://someother/string
|
||||
|
|
@ -222,6 +268,22 @@ export default {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
function isValidClaim(claim) {
|
||||
if (claim === '') return true
|
||||
|
||||
const pattern = new RegExp('^[a-zA-Z][a-zA-Z0-9_-]*$', 'i')
|
||||
return pattern.test(claim)
|
||||
}
|
||||
if (!isValidClaim(this.newAuthSettings.authOpenIDGroupClaim)) {
|
||||
this.$toast.error('Group Claim: Invalid claim name')
|
||||
isValid = false
|
||||
}
|
||||
if (!isValidClaim(this.newAuthSettings.authOpenIDAdvancedPermsClaim)) {
|
||||
this.$toast.error('Advanced Permission Claim: Invalid claim name')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
return isValid
|
||||
},
|
||||
async saveSettings() {
|
||||
|
|
@ -248,14 +310,14 @@ export default {
|
|||
.then((data) => {
|
||||
this.$store.commit('setServerSettings', data.serverSettings)
|
||||
if (data.updated) {
|
||||
this.$toast.success('Server settings updated')
|
||||
this.$toast.success(this.$strings.ToastServerSettingsUpdateSuccess)
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.$toast.error('Failed to update server settings')
|
||||
this.$toast.error(this.$strings.ToastServerSettingsUpdateFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingSettings = false
|
||||
|
|
@ -285,4 +347,4 @@ export default {
|
|||
padding: 2px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
<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-icons 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">
|
||||
|
|
@ -51,7 +59,7 @@
|
|||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
<app-settings-content :header-text="$strings.HeaderEreaderDevices" :description="''">
|
||||
<app-settings-content :header-text="$strings.HeaderEreaderDevices" :description="$strings.MessageEreaderDevices">
|
||||
<template #header-items>
|
||||
<div class="flex-grow" />
|
||||
|
||||
|
|
@ -62,6 +70,7 @@
|
|||
<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">
|
||||
|
|
@ -71,6 +80,9 @@
|
|||
<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)" />
|
||||
|
|
@ -79,12 +91,12 @@
|
|||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div v-else class="text-center py-4">
|
||||
<div v-else-if="!loading" class="text-center py-4">
|
||||
<p class="text-lg text-gray-100">No Devices</p>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
<modals-emails-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="existingEReaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
|
||||
<modals-emails-e-reader-device-modal v-model="showEReaderDeviceModal" :users="users" :existing-devices="existingEReaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" :loadUsers="loadUsers" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -97,6 +109,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
users: [],
|
||||
loading: false,
|
||||
savingSettings: false,
|
||||
sendingTest: false,
|
||||
|
|
@ -138,6 +151,30 @@ export default {
|
|||
...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
|
||||
|
|
@ -176,6 +213,11 @@ export default {
|
|||
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
|
||||
|
|
@ -243,7 +285,12 @@ export default {
|
|||
|
||||
this.$axios
|
||||
.$get(`/api/emails/settings`)
|
||||
.then((data) => {
|
||||
.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
|
||||
|
|
@ -251,7 +298,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get email settings', error)
|
||||
this.$toast.error('Failed to load email settings')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
|
|
@ -263,4 +310,4 @@ export default {
|
|||
},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -199,16 +199,15 @@
|
|||
|
||||
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
||||
|
||||
<!-- confirm cache purge dialog -->
|
||||
<prompt-dialog v-model="showConfirmPurgeCache" :width="675">
|
||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
||||
<p class="text-error font-semibold">Important Notice!</p>
|
||||
<p class="my-2 text-center">Purge cache will delete the entire directory at <span class="font-mono">/metadata/cache</span>.</p>
|
||||
|
||||
<p class="text-center mb-8">Are you sure you want to remove the cache directory?</p>
|
||||
<p class="text-error font-semibold">{{ $strings.MessageImportantNotice }}</p>
|
||||
<p class="my-8 text-center" v-html="$strings.MessageConfirmPurgeCache" />
|
||||
<div class="flex px-1 items-center">
|
||||
<ui-btn color="primary" @click="showConfirmPurgeCache = false">Nevermind</ui-btn>
|
||||
<ui-btn color="primary" @click="showConfirmPurgeCache = false">{{ $strings.ButtonNevermind }}</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" @click="confirmPurge">Yes, Purge!</ui-btn>
|
||||
<ui-btn color="success" @click="confirmPurge">{{ $strings.ButtonYes }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</prompt-dialog>
|
||||
|
|
@ -275,7 +274,7 @@ export default {
|
|||
updateSortingPrefixes() {
|
||||
const prefixes = [...new Set(this.newServerSettings.sortingPrefixes.map((prefix) => prefix.trim().toLowerCase()) || [])]
|
||||
if (!prefixes.length) {
|
||||
this.$toast.error('Must have at least 1 prefix')
|
||||
this.$toast.error(this.$strings.ToastSortingPrefixesEmptyError)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -283,7 +282,7 @@ export default {
|
|||
this.$axios
|
||||
.$patch(`/api/sorting-prefixes`, { sortingPrefixes: prefixes })
|
||||
.then((data) => {
|
||||
this.$toast.success(`Sorting prefixes updated. ${data.rowsUpdated} rows`)
|
||||
this.$toast.success(this.$getString('ToastSortingPrefixesUpdateSuccess', [data.rowsUpdated]))
|
||||
if (data.serverSettings) {
|
||||
this.$store.commit('setServerSettings', data.serverSettings)
|
||||
}
|
||||
|
|
@ -291,7 +290,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update prefixes', error)
|
||||
this.$toast.error('Failed to update sorting prefixes')
|
||||
this.$toast.error(this.$strings.ToastSortingPrefixesUpdateFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingPrefixes = false
|
||||
|
|
@ -329,7 +328,7 @@ export default {
|
|||
.dispatch('updateServerSettings', payload)
|
||||
.then(() => {
|
||||
this.updatingServerSettings = false
|
||||
this.$toast.success('Server settings updated')
|
||||
this.$toast.success(this.$strings.ToastServerSettingsUpdateSuccess)
|
||||
|
||||
if (payload.language) {
|
||||
// Updating language after save allows for re-rendering
|
||||
|
|
@ -339,7 +338,7 @@ export default {
|
|||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.updatingServerSettings = false
|
||||
this.$toast.error('Failed to update server settings')
|
||||
this.$toast.error(this.$strings.ToastServerSettingsUpdateFailed)
|
||||
})
|
||||
},
|
||||
initServerSettings() {
|
||||
|
|
@ -359,11 +358,11 @@ export default {
|
|||
await this.$axios
|
||||
.$post('/api/cache/purge')
|
||||
.then(() => {
|
||||
this.$toast.success('Cache Purged!')
|
||||
this.$toast.success(this.$strings.ToastCachePurgeSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to purge cache', error)
|
||||
this.$toast.error('Failed to purge cache')
|
||||
this.$toast.error(this.$strings.ToastCachePurgeFailed)
|
||||
})
|
||||
this.isPurgingCache = false
|
||||
},
|
||||
|
|
@ -384,11 +383,11 @@ export default {
|
|||
await this.$axios
|
||||
.$post('/api/cache/items/purge')
|
||||
.then(() => {
|
||||
this.$toast.success('Items Cache Purged!')
|
||||
this.$toast.success(this.$strings.ToastCachePurgeSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to purge items cache', error)
|
||||
this.$toast.error('Failed to purge items cache')
|
||||
this.$toast.error(this.$strings.ToastCachePurgeFailed)
|
||||
})
|
||||
this.isPurgingCache = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error('Failed to load custom metadata providers')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderLogs">
|
||||
<app-settings-content :header-text="$strings.HeaderLogs" :description="$strings.MessageLogsDescription">
|
||||
<template #header-items>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/server_logs" target="_blank" class="inline-flex">
|
||||
<span class="material-icons text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
</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" />
|
||||
|
||||
|
|
@ -139,7 +147,7 @@ export default {
|
|||
async loadLoggerData() {
|
||||
const loggerData = await this.$axios.$get('/api/logger-data').catch((error) => {
|
||||
console.error('Failed to load logger data', error)
|
||||
this.$toast.error('Failed to load logger data')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
})
|
||||
|
||||
this.loadedLogs = loggerData?.currentDailyLogs || []
|
||||
|
|
@ -183,4 +191,4 @@ export default {
|
|||
.logmessage {
|
||||
width: calc(100% - 208px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export default {
|
|||
this.loading = true
|
||||
const notificationResponse = await this.$axios.$get('/api/notifications').catch((error) => {
|
||||
console.error('Failed to get notification settings', error)
|
||||
this.$toast.error('Failed to load notification settings')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return null
|
||||
})
|
||||
this.loading = false
|
||||
|
|
@ -172,4 +172,4 @@ export default {
|
|||
this.$root.socket.off('notifications_updated', this.notificationsUpdated)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export default {
|
|||
return null
|
||||
})
|
||||
if (!data) {
|
||||
this.$toast.error('Failed to load RSS feeds')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return
|
||||
}
|
||||
this.feeds = data.feeds
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@
|
|||
<td class="hidden md:table-cell w-26 min-w-26">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell w-32 min-w-32">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
<td class="hidden sm:table-cell max-w-32 min-w-32">
|
||||
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
|
|
@ -127,8 +127,8 @@
|
|||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
<td class="hidden sm:table-cell max-w-32 min-w-32">
|
||||
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
|
|
@ -394,6 +394,7 @@ export default {
|
|||
getDeviceInfoString(deviceInfo) {
|
||||
if (!deviceInfo) return ''
|
||||
var lines = []
|
||||
if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`)
|
||||
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
|
||||
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
|
||||
|
||||
|
|
@ -425,7 +426,7 @@ export default {
|
|||
})
|
||||
this.loading = false
|
||||
if (!data) {
|
||||
this.$toast.error('Failed to load listening sessions')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -446,7 +447,7 @@ export default {
|
|||
return null
|
||||
})
|
||||
if (!data) {
|
||||
this.$toast.error('Failed to load open sessions')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@
|
|||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
<td class="hidden sm:table-cell min-w-32 max-w-32">
|
||||
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
|
|
@ -193,6 +193,7 @@ export default {
|
|||
getDeviceInfoString(deviceInfo) {
|
||||
if (!deviceInfo) return ''
|
||||
var lines = []
|
||||
if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`)
|
||||
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
|
||||
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
|
||||
|
||||
|
|
@ -213,7 +214,7 @@ export default {
|
|||
return null
|
||||
})
|
||||
if (!data) {
|
||||
this.$toast.error('Failed to load listening sessions')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,17 +27,20 @@
|
|||
<h1 class="text-2xl md:text-3xl font-semibold">
|
||||
<div class="flex items-center">
|
||||
{{ title }}
|
||||
<widgets-explicit-indicator :explicit="isExplicit" />
|
||||
<widgets-explicit-indicator v-if="isExplicit" />
|
||||
<widgets-abridged-indicator v-if="isAbridged" />
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
||||
|
||||
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ _series.text }}</nuxt-link>
|
||||
<template v-for="(_series, index) in seriesList">
|
||||
<nuxt-link :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7">{{ _series.text }}</nuxt-link
|
||||
><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">by {{ podcastAuthor || 'Unknown' }}</p>
|
||||
<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>
|
||||
|
|
@ -125,9 +128,9 @@
|
|||
</div>
|
||||
|
||||
<div class="my-4 w-full">
|
||||
<p ref="description" id="item-description" class="text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }">{{ description }}</p>
|
||||
<p ref="description" id="item-description" dir="auto" class="text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }">{{ description }}</p>
|
||||
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">
|
||||
{{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
|
||||
{{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -282,7 +285,7 @@ export default {
|
|||
return this.mediaMetadata.subtitle
|
||||
},
|
||||
podcastAuthor() {
|
||||
return this.mediaMetadata.author || ''
|
||||
return this.mediaMetadata.author || 'Unknown'
|
||||
},
|
||||
authors() {
|
||||
return this.mediaMetadata.authors || []
|
||||
|
|
@ -804,4 +807,4 @@ export default {
|
|||
-webkit-line-clamp: unset;
|
||||
max-height: 999rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -108,4 +108,4 @@ export default {
|
|||
this.$root.socket.off('author_removed', this.authorRemoved)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<div class="flex-grow px-2">
|
||||
<div class="flex items-center">
|
||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcastTitle }}</nuxt-link>
|
||||
<widgets-explicit-indicator :explicit="episode.podcastExplicit" />
|
||||
<widgets-explicit-indicator v-if="episode.podcastExplicit" />
|
||||
</div>
|
||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||
</div>
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
<div class="hidden md:block">
|
||||
<div class="flex items-center">
|
||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcastTitle }}</nuxt-link>
|
||||
<widgets-explicit-indicator :explicit="episode.podcastExplicit" />
|
||||
<widgets-explicit-indicator v-if="episode.podcastExplicit" />
|
||||
</div>
|
||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<div class="flex" @click.stop>
|
||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||
</div>
|
||||
<widgets-explicit-indicator :explicit="episode.podcast.metadata.explicit" />
|
||||
<widgets-explicit-indicator v-if="episode.podcast.metadata.explicit" />
|
||||
</div>
|
||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||
</div>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
<div class="flex" @click.stop>
|
||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||
</div>
|
||||
<widgets-explicit-indicator :explicit="episode.podcast.metadata.explicit" />
|
||||
<widgets-explicit-indicator v-if="episode.podcast.metadata.explicit" />
|
||||
</div>
|
||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||
</div>
|
||||
|
|
@ -40,12 +40,12 @@
|
|||
<div v-if="episode.episode">{{ episode.episode }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-2">
|
||||
<div dir="auto" class="flex items-center mb-2">
|
||||
<div class="font-semibold text-sm md:text-base">{{ episode.title }}</div>
|
||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" />
|
||||
<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)">
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@
|
|||
<div class="flex-grow pl-4 max-w-2xl">
|
||||
<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 :explicit="podcast.explicit" />
|
||||
<widgets-explicit-indicator v-if="podcast.explicit" />
|
||||
<widgets-already-in-library-indicator :already-in-library="podcast.alreadyInLibrary" />
|
||||
</div>
|
||||
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p>
|
||||
<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>
|
||||
<p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} {{ $strings.HeaderEpisodes }}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@
|
|||
<form @submit.prevent="submitServerSetup">
|
||||
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
|
||||
<ui-text-input-with-label v-model.trim="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
<ui-text-input-with-label v-model.trim="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
<ui-text-input-with-label v-model.trim="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
|
||||
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
|
||||
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||
</ui-btn>
|
||||
|
||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
||||
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||
|
||||
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
||||
<ui-icon-btn icon="delete" class="mx-0.5" @click="removeClick" />
|
||||
</div>
|
||||
|
||||
<div class="my-8 max-w-2xl">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue