Merge remote-tracking branch 'remotes/upstream/master'

# Conflicts:
#	client/components/cards/LazyBookCard.vue
#	client/components/cards/LazySeriesCard.vue
This commit is contained in:
Toni Barth 2024-05-27 12:37:19 +02:00
commit 557fc6dc64
198 changed files with 8256 additions and 1566 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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
}

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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">,&nbsp;</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>

View file

@ -108,4 +108,4 @@ export default {
this.$root.socket.off('author_removed', this.authorRemoved)
}
}
</script>
</script>

View file

@ -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>

View file

@ -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)">

View file

@ -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>

View file

@ -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" />

View file

@ -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">