mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Add:Full podcast episode description parsed and viewable in modal #492
This commit is contained in:
parent
c4bfa266b0
commit
a394f38fe9
@ -1,6 +1,7 @@
|
|||||||
@import './fonts.css';
|
@import './fonts.css';
|
||||||
@import './transitions.css';
|
@import './transitions.css';
|
||||||
@import './draggable.css';
|
@import './draggable.css';
|
||||||
|
@import './defaultStyles.css';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bookshelf-texture-img: url(/textures/wood_default.jpg);
|
--bookshelf-texture-img: url(/textures/wood_default.jpg);
|
||||||
|
44
client/assets/defaultStyles.css
Normal file
44
client/assets/defaultStyles.css
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
This is for setting regular html styles for places where embedding HTML will be
|
||||||
|
like podcast episode descriptions. Otherwise TailwindCSS will have stripped all default markup.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
.default-style p {
|
||||||
|
display: block;
|
||||||
|
margin-block-start: 1em;
|
||||||
|
margin-block-end: 1em;
|
||||||
|
margin-inline-start: 0px;
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-style a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #5985ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-style ul {
|
||||||
|
display: block;
|
||||||
|
list-style: circle;
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-block-start: 1em;
|
||||||
|
margin-block-end: 1em;
|
||||||
|
margin-inline-start: 0px;
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
padding-inline-start: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-style li {
|
||||||
|
display: list-item;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-style li::marker {
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-transform: none;
|
||||||
|
text-indent: 0px !important;
|
||||||
|
text-align: start !important;
|
||||||
|
text-align-last: start !important;
|
||||||
|
}
|
75
client/components/modals/podcast/ViewEpisode.vue
Normal file
75
client/components/modals/podcast/ViewEpisode.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="podcast-episode-view-modal" :width="800" :height="'unset'" :processing="processing">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="font-book text-3xl text-white truncate">Episode</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="wrapper" class="p-4 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
||||||
|
<div class="flex mb-4">
|
||||||
|
<div class="w-12 h-12">
|
||||||
|
<covers-book-cover :library-item="libraryItem" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2">
|
||||||
|
<p class="text-base mb-1">{{ podcastTitle }}</p>
|
||||||
|
<p class="text-xs text-gray-300">{{ podcastAuthor }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-semibold mb-6">{{ title }}</p>
|
||||||
|
<div v-if="description" class="default-style" v-html="description" />
|
||||||
|
<p v-else class="mb-2">No description</p>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.globals.showViewPodcastEpisodeModal
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('globals/setShowViewPodcastEpisodeModal', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
libraryItem() {
|
||||||
|
return this.$store.state.selectedLibraryItem
|
||||||
|
},
|
||||||
|
episode() {
|
||||||
|
return this.$store.state.globals.selectedEpisode || {}
|
||||||
|
},
|
||||||
|
episodeId() {
|
||||||
|
return this.episode.id
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.episode.title || 'No Episode Title'
|
||||||
|
},
|
||||||
|
description() {
|
||||||
|
return this.episode.description || ''
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
|
podcastTitle() {
|
||||||
|
return this.mediaMetadata.title
|
||||||
|
},
|
||||||
|
podcastAuthor() {
|
||||||
|
return this.mediaMetadata.author
|
||||||
|
},
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div v-if="episode" class="flex items-center h-24">
|
<div v-if="episode" class="flex items-center h-24 cursor-pointer" @click="$emit('view', episode)">
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<p class="text-sm font-semibold">
|
<p class="text-sm font-semibold">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ description }}</p>
|
|
||||||
|
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
|
||||||
|
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
<div 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" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click="playClick">
|
<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="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
|
||||||
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
@ -66,10 +68,11 @@ export default {
|
|||||||
title() {
|
title() {
|
||||||
return this.episode.title || ''
|
return this.episode.title || ''
|
||||||
},
|
},
|
||||||
|
subtitle() {
|
||||||
|
return this.episode.subtitle || ''
|
||||||
|
},
|
||||||
description() {
|
description() {
|
||||||
if (this.episode.subtitle) return this.episode.subtitle
|
return this.episode.description || ''
|
||||||
var desc = this.episode.description || ''
|
|
||||||
return desc
|
|
||||||
},
|
},
|
||||||
duration() {
|
duration() {
|
||||||
return this.$secondsToTimestamp(this.episode.duration)
|
return this.$secondsToTimestamp(this.episode.duration)
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
|
||||||
<template v-for="episode in episodesSorted">
|
<template v-for="episode in episodesSorted">
|
||||||
<tables-podcast-episode-table-row :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" class="item" @remove="removeEpisode" @edit="editEpisode" />
|
<tables-podcast-episode-table-row :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" class="item" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" :library-item="libraryItem" :episode="selectedEpisode" />
|
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" :library-item="libraryItem" :episode="selectedEpisode" />
|
||||||
@ -68,6 +68,11 @@ export default {
|
|||||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
},
|
},
|
||||||
|
viewEpisode(episode) {
|
||||||
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
|
this.$store.commit('globals/setShowViewPodcastEpisodeModal', true)
|
||||||
|
},
|
||||||
init() {
|
init() {
|
||||||
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
<modals-edit-collection-modal />
|
<modals-edit-collection-modal />
|
||||||
<modals-bookshelf-texture-modal />
|
<modals-bookshelf-texture-modal />
|
||||||
<modals-podcast-edit-episode />
|
<modals-podcast-edit-episode />
|
||||||
|
<modals-podcast-view-episode />
|
||||||
<modals-authors-edit-modal />
|
<modals-authors-edit-modal />
|
||||||
<readers-reader />
|
<readers-reader />
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@ export const state = () => ({
|
|||||||
showUserCollectionsModal: false,
|
showUserCollectionsModal: false,
|
||||||
showEditCollectionModal: false,
|
showEditCollectionModal: false,
|
||||||
showEditPodcastEpisode: false,
|
showEditPodcastEpisode: false,
|
||||||
|
showViewPodcastEpisodeModal: false,
|
||||||
showEditAuthorModal: false,
|
showEditAuthorModal: false,
|
||||||
selectedEpisode: null,
|
selectedEpisode: null,
|
||||||
selectedCollection: null,
|
selectedCollection: null,
|
||||||
@ -53,6 +54,9 @@ export const mutations = {
|
|||||||
setShowEditPodcastEpisodeModal(state, val) {
|
setShowEditPodcastEpisodeModal(state, val) {
|
||||||
state.showEditPodcastEpisode = val
|
state.showEditPodcastEpisode = val
|
||||||
},
|
},
|
||||||
|
setShowViewPodcastEpisodeModal(state, val) {
|
||||||
|
state.showViewPodcastEpisodeModal = val
|
||||||
|
},
|
||||||
setEditCollection(state, collection) {
|
setEditCollection(state, collection) {
|
||||||
state.selectedCollection = collection
|
state.selectedCollection = collection
|
||||||
state.showEditCollectionModal = true
|
state.showEditCollectionModal = true
|
||||||
|
@ -3,7 +3,7 @@ const sanitizeHtml = require('../libs/sanitizeHtml')
|
|||||||
function sanitize(html) {
|
function sanitize(html) {
|
||||||
const sanitizerOptions = {
|
const sanitizerOptions = {
|
||||||
allowedTags: [
|
allowedTags: [
|
||||||
'p', 'ol', 'ul', 'a', 'strong', 'em'
|
'p', 'ol', 'ul', 'li', 'a', 'strong', 'em'
|
||||||
],
|
],
|
||||||
disallowedTagsMode: 'discard',
|
disallowedTagsMode: 'discard',
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
|
@ -81,9 +81,16 @@ function extractEpisodeData(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Full description with html
|
||||||
|
if (item['content:encoded']) {
|
||||||
|
const rawDescription = (extractFirstArrayItem(item, 'content:encoded') || '').trim()
|
||||||
|
episode.description = htmlSanitizer.sanitize(rawDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supposed to be the plaintext description but not always followed
|
||||||
if (item['description']) {
|
if (item['description']) {
|
||||||
const rawDescription = extractFirstArrayItem(item, 'description') || ''
|
const rawDescription = extractFirstArrayItem(item, 'description') || ''
|
||||||
episode.description = htmlSanitizer.sanitize(rawDescription)
|
if (!episode.description) episode.description = htmlSanitizer.sanitize(rawDescription)
|
||||||
episode.descriptionPlain = htmlSanitizer.stripAllTags(rawDescription)
|
episode.descriptionPlain = htmlSanitizer.stripAllTags(rawDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user