Merge master

This commit is contained in:
advplyr 2022-05-28 13:58:52 -05:00
commit a5dacd7821
47 changed files with 3086 additions and 598 deletions

View file

@ -33,8 +33,8 @@ export default {
showMenu: false,
items: [
{
text: 'Current',
value: 'index'
text: 'Pub Date',
value: 'publishedAt'
},
{
text: 'Title',
@ -47,10 +47,6 @@ export default {
{
text: 'Episode',
value: 'episode'
},
{
text: 'Pub Date',
value: 'publishedAt'
}
]
}

View file

@ -0,0 +1,150 @@
<template>
<modals-modal v-model="show" name="listening-session-modal" :width="700" :height="'unset'">
<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">Session {{ _session.id }}</p>
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
<div class="flex items-center">
<p class="text-base text-gray-200">{{ _session.displayTitle }}</p>
<p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">by {{ _session.displayAuthor }}</p>
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
<div class="flex flex-wrap mb-4">
<div class="w-full md:w-2/3">
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2">Details</p>
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Started At</div>
<div class="px-1">
{{ $formatDate(_session.startedAt, 'MMMM do, yyyy HH:mm') }}
</div>
</div>
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Updated At</div>
<div class="px-1">
{{ $formatDate(_session.updatedAt, 'MMMM do, yyyy HH:mm') }}
</div>
</div>
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Listened for</div>
<div class="px-1">
{{ $elapsedPrettyExtended(_session.timeListening) }}
</div>
</div>
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Start Time</div>
<div class="px-1">
{{ $secondsToTimestamp(_session.startTime) }}
</div>
</div>
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Last Time</div>
<div class="px-1">
{{ $secondsToTimestamp(_session.currentTime) }}
</div>
</div>
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Item</p>
<div v-if="_session.libraryId" class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Library Id</div>
<div class="px-1">
{{ _session.libraryId }}
</div>
</div>
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Library Item Id</div>
<div class="px-1">
{{ _session.libraryItemId }}
</div>
</div>
<div v-if="_session.episodeId" class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Episode Id</div>
<div class="px-1">
{{ _session.episodeId }}
</div>
</div>
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Media Type</div>
<div class="px-1">
{{ _session.mediaType }}
</div>
</div>
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">Duration</div>
<div class="px-1">
{{ $elapsedPretty(_session.duration) }}
</div>
</div>
</div>
<div class="w-full md:w-1/3">
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">User</p>
<p class="mb-1">{{ _session.userId }}</p>
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Media Player</p>
<p class="mb-1">{{ playMethodName }}</p>
<p class="mb-1">{{ _session.mediaPlayer }}</p>
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Device</p>
<p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p>
<p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p>
<p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p>
<p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p>
<p v-if="deviceInfo.sdkVersion" class="mb-1">SDK Version: {{ deviceInfo.sdkVersion }}</p>
<p v-if="deviceInfo.deviceType" class="mb-1">Type: {{ deviceInfo.deviceType }}</p>
</div>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
session: {
type: Object,
default: () => {}
}
},
data() {
return {}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
_session() {
return this.session || {}
},
deviceInfo() {
return this._session.deviceInfo || {}
},
osDisplayName() {
if (!this.deviceInfo.osName) return null
return `${this.deviceInfo.osName} ${this.deviceInfo.osVersion}`
},
clientDisplayName() {
if (!this.deviceInfo.manufacturer || !this.deviceInfo.model) return null
return `${this.deviceInfo.manufacturer} ${this.deviceInfo.model}`
},
playMethodName() {
const playMethod = this._session.playMethod
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'
else if (playMethod === this.$constants.PlayMethod.TRANSCODE) return 'Transcode'
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
return 'Unknown'
}
},
methods: {},
mounted() {}
}
</script>

View file

@ -6,12 +6,12 @@
</div>
</template>
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
<form @submit.prevent="submitForm">
<form v-if="author" @submit.prevent="submitForm">
<div class="flex">
<div class="w-40 p-2">
<div class="w-full h-45 relative">
<covers-author-image :author="author" />
<div v-show="!processing" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
<div v-show="!processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
</div>
</div>

View file

@ -5,7 +5,7 @@
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
<div class="flex flex-wrap">
<div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.season" label="Season" />
@ -25,8 +25,8 @@
<div class="w-full p-1">
<ui-textarea-with-label v-model="newEpisode.subtitle" label="Subtitle" :rows="3" />
</div>
<div class="w-full p-1">
<ui-textarea-with-label v-model="newEpisode.description" label="Description" :rows="8" />
<div class="w-full p-1 default-style">
<ui-rich-text-editor v-if="show" label="Description" v-model="newEpisode.description" />
</div>
</div>
<div class="flex justify-end pt-4">

View file

@ -0,0 +1,90 @@
<template>
<modals-modal v-model="show" name="podcast-episode-remove-modal" :width="500" :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">{{ title }}</p>
</div>
</template>
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
<div class="mb-4">
<p class="text-lg text-gray-200 mb-4">
Are you sure you want to remove episode<br /><span class="text-base">{{ episodeTitle }}</span
>?
</p>
<p class="text-xs font-semibold text-warning text-opacity-90">Note: This does not delete the audio file unless toggling "Hard delete file"</p>
</div>
<div class="flex justify-between items-center pt-4">
<ui-checkbox v-model="hardDeleteFile" label="Hard delete file" check-color="error" checkbox-bg="bg" small label-class="text-base text-gray-200 pl-3" />
<ui-btn @click="submit">{{ hardDeleteFile ? 'Delete episode' : 'Remove episode' }}</ui-btn>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
libraryItem: {
type: Object,
default: () => {}
},
episode: {
type: Object,
default: () => {}
}
},
data() {
return {
hardDeleteFile: false,
processing: false
}
},
watch: {
value(newVal) {
if (newVal) this.hardDeleteFile = false
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
title() {
return 'Remove Episode'
},
episodeId() {
return this.episode ? this.episode.id : null
},
episodeTitle() {
return this.episode ? this.episode.title : null
}
},
methods: {
submit() {
this.processing = true
var queryString = this.hardDeleteFile ? '?hard=1' : ''
this.$axios
.$delete(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}${queryString}`)
.then(() => {
this.processing = false
this.$toast.success('Podcast episode removed')
this.show = false
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed remove episode'
console.error('Failed update episode', error)
this.processing = false
this.$toast.error(errorMsg)
})
}
},
mounted() {}
}
</script>

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

View file

@ -1,21 +1,18 @@
<template>
<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-show="userCanUpdate" class="w-12 min-w-12 max-w-16 h-full">
<div class="flex h-full items-center justify-center">
<span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span>
</div>
</div>
<div v-if="episode" class="flex items-center h-24 cursor-pointer" @click="$emit('view', episode)">
<div class="flex-grow px-2">
<p class="text-sm font-semibold">
{{ title }}
</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="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>
<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-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
@ -49,8 +46,7 @@ export default {
episode: {
type: Object,
default: () => {}
},
isDragging: Boolean
}
},
data() {
return {
@ -59,15 +55,6 @@ export default {
isHovering: false
}
},
watch: {
isDragging: {
handler(newVal) {
if (newVal) {
this.isHovering = false
}
}
}
},
computed: {
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
@ -81,10 +68,11 @@ export default {
title() {
return this.episode.title || ''
},
subtitle() {
return this.episode.subtitle || ''
},
description() {
if (this.episode.subtitle) return this.episode.subtitle
var desc = this.episode.description || ''
return desc
return this.episode.description || ''
},
duration() {
return this.$secondsToTimestamp(this.episode.duration)
@ -117,7 +105,7 @@ export default {
},
methods: {
mouseover() {
if (this.isDragging) return
// if (this.isDragging) return
this.isHovering = true
},
mouseleave() {
@ -154,22 +142,7 @@ export default {
})
},
removeClick() {
if (confirm(`Are you sure you want to remove episode ${this.title}?\nNote: Does not delete from file system`)) {
this.processingRemove = true
this.$axios
.$delete(`/api/items/${this.libraryItemId}/episode/${this.episode.id}`)
.then((updatedPodcast) => {
console.log(`Episode removed from podcast`, updatedPodcast)
this.$toast.success('Episode removed from podcast')
this.processingRemove = false
})
.catch((error) => {
console.error('Failed to remove episode from podcast', error)
this.$toast.error('Failed to remove episode from podcast')
this.processingRemove = false
})
}
this.$emit('remove', this.episode)
}
}
}

View file

@ -3,29 +3,19 @@
<div class="flex items-center mb-4">
<p class="text-lg mb-0 font-semibold">Episodes</p>
<div class="flex-grow" />
<controls-episode-sort-select v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" @change="changeSort" />
<div v-if="userCanUpdate" class="w-12">
<ui-icon-btn v-if="orderChanged" :loading="savingOrder" icon="save" bg-color="primary" class="ml-auto" @click="saveOrder" />
</div>
<controls-episode-sort-select v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
</div>
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
<draggable v-model="episodesCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
<transition-group type="transition" :name="!drag ? 'episode' : null">
<template v-for="episode in episodesCopy">
<tables-podcast-episode-table-row :key="episode.id" :is-dragging="drag" :episode="episode" :library-item-id="libraryItem.id" class="item" :class="drag ? '' : 'episode'" @edit="editEpisode" />
</template>
</transition-group>
</draggable>
<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" @view="viewEpisode" />
</template>
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" :library-item="libraryItem" :episode="selectedEpisode" />
</div>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: {
draggable
},
props: {
libraryItem: {
type: Object,
@ -34,30 +24,19 @@ export default {
},
data() {
return {
sortKey: 'index',
sortDesc: true,
drag: false,
episodesCopy: [],
orderChanged: false,
savingOrder: false
sortKey: 'publishedAt',
sortDesc: true,
selectedEpisode: null,
showPodcastRemoveModal: false
}
},
watch: {
libraryItem: {
handler(newVal) {
this.init()
}
libraryItem() {
this.init()
}
},
computed: {
dragOptions() {
return {
animation: 200,
group: 'description',
ghostClass: 'ghost',
disabled: !this.userCanUpdate
}
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},
@ -69,64 +48,33 @@ export default {
},
episodes() {
return this.media.episodes || []
}
},
methods: {
changeSort() {
this.episodesCopy.sort((a, b) => {
},
episodesSorted() {
return this.episodesCopy.sort((a, b) => {
if (this.sortDesc) {
return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
}
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
})
this.orderChanged = this.checkHasOrderChanged()
},
checkHasOrderChanged() {
for (let i = 0; i < this.episodesCopy.length; i++) {
var epc = this.episodesCopy[i]
var ep = this.episodes[i]
if (epc.index != ep.index) {
return true
}
}
return false
}
},
methods: {
removeEpisode(episode) {
this.selectedEpisode = episode
this.showPodcastRemoveModal = true
},
editEpisode(episode) {
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
},
draggableUpdate() {
this.orderChanged = this.checkHasOrderChanged()
},
async saveOrder() {
if (!this.userCanUpdate) return
this.savingOrder = true
var episodesUpdate = {
episodes: this.episodesCopy.map((b) => b.id)
}
await this.$axios
.$patch(`/api/items/${this.libraryItem.id}/episodes`, episodesUpdate)
.then((podcast) => {
console.log('Podcast updated', podcast)
this.$toast.success('Saved episode order')
this.orderChanged = false
})
.catch((error) => {
console.error('Failed to update podcast', error)
this.$toast.error('Failed to save podcast episode order')
})
this.savingOrder = false
viewEpisode(episode) {
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowViewPodcastEpisodeModal', true)
},
init() {
this.episodesCopy = this.episodes.map((ep) => {
return {
...ep
}
})
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
}
},
mounted() {

View file

@ -32,6 +32,7 @@ export default {
default: ''
},
paddingX: Number,
paddingY: Number,
small: Boolean,
loading: Boolean,
disabled: Boolean
@ -48,14 +49,17 @@ export default {
if (this.small) {
list.push('text-sm')
if (this.paddingX === undefined) list.push('px-4')
list.push('py-1')
if (this.paddingY === undefined) list.push('py-1')
} else {
if (this.paddingX === undefined) list.push('px-8')
list.push('py-2')
if (this.paddingY === undefined) list.push('py-2')
}
if (this.paddingX !== undefined) {
list.push(`px-${this.paddingX}`)
}
if (this.paddingY !== undefined) {
list.push(`py-${this.paddingY}`)
}
if (this.disabled) {
list.push('cursor-not-allowed')
}

View file

@ -0,0 +1,75 @@
<template>
<div>
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
{{ label }}
</p>
<ui-vue-trix v-model="content" :config="config" :disabled-editor="disabled" @trix-file-accept="trixFileAccept" />
</div>
</template>
<script>
export default {
props: {
value: String,
label: String,
disabled: Boolean
},
data() {
return {}
},
computed: {
content: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
config() {
return {
toolbar: {
getDefaultHTML: () => ` <div class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="#{lang.bold}" tabindex="-1">#{lang.bold}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="#{lang.italic}" tabindex="-1">#{lang.italic}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="#{lang.strike}" tabindex="-1">#{lang.strike}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="#{lang.link}" tabindex="-1">#{lang.link}</button>
</span>
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="#{lang.bullets}" tabindex="-1">#{lang.bullets}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="#{lang.numbers}" tabindex="-1">#{lang.numbers}</button>
</span>
<span class="trix-button-group-spacer"></span>
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="#{lang.undo}" tabindex="-1">#{lang.undo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="#{lang.redo}" tabindex="-1">#{lang.redo}</button>
</span>
</div>
<div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="#{lang.urlPlaceholder}" aria-label="#{lang.url}" required data-trix-input>
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" value="#{lang.link}" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="#{lang.unlink}" data-trix-method="removeAttribute">
</div>
</div>
</div>
</div>`
}
}
}
},
methods: {
trixFileAccept(e) {
e.preventDefault()
}
},
mounted() {},
beforeDestroy() {
console.log('Before destroy')
}
}
</script>

View file

@ -0,0 +1,284 @@
<template>
<div>
<trix-editor :contenteditable="!disabledEditor" :class="['trix-content']" ref="trix" :input="computedId" :placeholder="placeholder" @trix-change="handleContentChange" @trix-initialize="handleInitialize" @trix-focus="processTrixFocus" @trix-blur="processTrixBlur" />
<input type="hidden" :name="inputName" :id="computedId" :value="editorContent" />
</div>
</template>
<script>
/*
ORIGINAL SOURCE: https://github.com/hanhdt/vue-trix
modified for audiobookshelf
*/
import Trix from 'trix'
import '@/assets/trix.css'
export default {
name: 'vue-trix',
model: {
prop: 'srcContent',
event: 'update'
},
props: {
/**
* This prop will put the editor in read-only mode
*/
disabledEditor: {
type: Boolean,
required: false,
default() {
return false
}
},
/**
* This is referenced `id` of the hidden input field defined.
* It is optional and will be a random string by default.
*/
inputId: {
type: String,
required: false,
default() {
return ''
}
},
/**
* This is referenced `name` of the hidden input field defined,
* default value is `content`.
*/
inputName: {
type: String,
required: false,
default() {
return 'content'
}
},
/**
* The placeholder attribute specifies a short hint
* that describes the expected value of a editor.
*/
placeholder: {
type: String,
required: false,
default() {
return ''
}
},
/**
* The source content is associcated to v-model directive.
*/
srcContent: {
type: String,
required: false,
default() {
return ''
}
},
/**
* The boolean attribute allows saving editor state into browser's localStorage
* (optional, default is `false`).
*/
localStorage: {
type: Boolean,
required: false,
default() {
return false
}
},
/**
* Focuses cursor in the editor when attached to the DOM
* (optional, default is `false`).
*/
autofocus: {
type: Boolean,
required: false,
default() {
return false
}
},
/**
* Object to override default editor configuration
*/
config: {
type: Object,
required: false,
default() {
return {}
}
}
},
data() {
return {
editorContent: this.srcContent,
isActived: null
}
},
watch: {
editorContent: {
handler: 'emitEditorState'
},
initialContent: {
handler: 'handleInitialContentChange'
},
isDisabled: {
handler: 'decorateDisabledEditor'
},
config: {
handler: 'overrideConfig',
immediate: true,
deep: true
}
},
computed: {
/**
* Compute a random id of hidden input
* when it haven't been specified.
*/
generateId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
var r = (Math.random() * 16) | 0
var v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
},
computedId() {
return this.inputId || this.generateId
},
initialContent() {
return this.srcContent
},
isDisabled() {
return this.disabledEditor
}
},
methods: {
processTrixFocus(event) {
if (this.$refs.trix) {
this.isActived = true
this.$emit('trix-focus', this.$refs.trix.editor, event)
}
},
processTrixBlur(event) {
if (this.$refs.trix) {
this.isActived = false
this.$emit('trix-blur', this.$refs.trix.editor, event)
}
},
handleContentChange(event) {
this.editorContent = event.srcElement ? event.srcElement.value : event.target.value
this.$emit('input', this.editorContent)
},
handleInitialize(event) {
/**
* If autofocus is true, manually set focus to
* beginning of content (consistent with Trix behavior)
*/
if (this.autofocus) {
this.$refs.trix.editor.setSelectedRange(0)
}
this.$emit('trix-initialize', this.emitInitialize)
},
handleInitialContentChange(newContent, oldContent) {
newContent = newContent === undefined ? '' : newContent
if (this.$refs.trix.editor && this.$refs.trix.editor.innerHTML !== newContent) {
/* Update editor's content when initial content changed */
this.editorContent = newContent
/**
* If user are typing, then don't reload the editor,
* hence keep cursor's position after typing.
*/
if (!this.isActived) {
this.reloadEditorContent(this.editorContent)
}
}
},
emitEditorState(value) {
/**
* If localStorage is enabled,
* then save editor's content into storage
*/
if (this.localStorage) {
localStorage.setItem(this.storageId('VueTrix'), JSON.stringify(this.$refs.trix.editor))
}
this.$emit('update', this.editorContent)
},
storageId(component) {
if (this.inputId) {
return `${component}.${this.inputId}.content`
} else {
return `${component}.content`
}
},
reloadEditorContent(newContent) {
// Reload HTML content
this.$refs.trix.editor.loadHTML(newContent)
// Move cursor to end of new content updated
this.$refs.trix.editor.setSelectedRange(this.getContentEndPosition())
},
getContentEndPosition() {
return this.$refs.trix.editor.getDocument().toString().length - 1
},
decorateDisabledEditor(editorState) {
/** Disable toolbar and editor by pointer events styling */
if (editorState) {
this.$refs.trix.toolbarElement.style['pointer-events'] = 'none'
this.$refs.trix.contentEditable = false
this.$refs.trix.style['background'] = '#e9ecef'
} else {
this.$refs.trix.toolbarElement.style['pointer-events'] = 'unset'
this.$refs.trix.style['pointer-events'] = 'unset'
this.$refs.trix.style['background'] = 'transparent'
}
},
overrideConfig(config) {
Trix.config = this.deepMerge(Trix.config, config)
},
deepMerge(target, override) {
// deep merge the object into the target object
for (let prop in override) {
if (override.hasOwnProperty(prop)) {
if (Object.prototype.toString.call(override[prop]) === '[object Object]') {
// if the property is a nested object
target[prop] = this.deepMerge(target[prop], override[prop])
} else {
// for regular property
target[prop] = override[prop]
}
}
}
return target
}
},
mounted() {
/** Override editor configuration */
this.overrideConfig(this.config)
/** Check if editor read-only mode is required */
this.decorateDisabledEditor(this.disabledEditor)
this.$nextTick(() => {
/**
* If localStorage is enabled,
* then load editor's content from the beginning.
*/
if (this.localStorage) {
const savedValue = localStorage.getItem(this.storageId('VueTrix'))
if (savedValue && !this.srcContent) {
this.$refs.trix.editor.loadJSON(JSON.parse(savedValue))
}
}
})
}
}
</script>
<style lang="css" module>
.trix_container {
max-width: 100%;
height: auto;
}
.trix_container .trix-button-group {
background-color: white;
}
.trix_container .trix-content {
background-color: white;
}
</style>