Merge master

This commit is contained in:
advplyr 2023-03-18 17:26:11 -05:00
commit c49010b4e1
92 changed files with 2652 additions and 1192 deletions

View file

@ -64,12 +64,22 @@
<div class="flex-grow hidden sm:inline-block" />
<!-- collapse series checkbox -->
<ui-checkbox v-if="isLibraryPage && isBookLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
<!-- library filter select -->
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
<!-- library sort select -->
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
<!-- series filter select -->
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
<!-- series sort select -->
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
<!-- issues page remove all button -->
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
</template>
<!-- search page -->

View file

@ -5,8 +5,8 @@
<span class="material-icons text-2xl">arrow_back</span>
</div>
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-4 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p>{{ route.title }}</p>
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p class="leading-4">{{ route.title }}</p>
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>

View file

@ -86,6 +86,14 @@
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2xl">file_download</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
<span class="material-icons text-2xl">warning</span>
@ -149,6 +157,9 @@ export default {
isMusicLibrary() {
return this.currentLibraryMediaType === 'music'
},
isPodcastDownloadQueuePage() {
return this.$route.name === 'library-library-podcast-download-queue'
},
isPodcastSearchPage() {
return this.$route.name === 'library-library-podcast-search'
},
@ -212,4 +223,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View file

@ -11,12 +11,15 @@
</nuxt-link>
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
<span class="material-icons text-sm">person</span>
<p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
<p v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</p>
<p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</p>
<p v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</p>
<div class="flex items-center">
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</div>
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>
</div>
</div>
<div class="text-gray-400 flex items-center">
@ -129,6 +132,9 @@ export default {
isMusic() {
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false
},
isExplicit() {
return this.mediaMetadata.explicit || false
},
mediaMetadata() {
return this.media.metadata || {}
},
@ -474,4 +480,4 @@ export default {
#streamContainer {
box-shadow: 0px -6px 8px #1111113f;
}
</style>
</style>

View file

@ -28,7 +28,11 @@
</div>
</div>
<div v-else class="px-4 flex-grow">
<h1>{{ book.title }}</h1>
<h1>
<div class="flex items-center">
{{ book.title }}<widgets-explicit-indicator :explicit="book.explicit" />
</div>
</h1>
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
@ -78,4 +82,4 @@ export default {
this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : this.book.cover || null
}
}
</script>
</script>

View file

@ -0,0 +1,85 @@
<template>
<div class="flex items-center h-full px-1 overflow-hidden">
<div class="h-5 w-5 min-w-5 text-lg mr-1.5 flex items-center justify-center">
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{actionIcon}}</span>
<widgets-loading-spinner v-else />
</div>
<div class="flex-grow px-2 taskRunningCardContent">
<p class="truncate text-sm">{{ title }}</p>
<p class="truncate text-xs text-gray-300">{{ description }}</p>
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
task: {
type: Object,
default: () => {}
}
},
data() {
return {}
},
computed: {
title() {
return this.task.title || 'No Title'
},
description() {
return this.task.description || ''
},
details() {
return this.task.details || 'Unknown'
},
isFinished() {
return this.task.isFinished || false
},
isFailed() {
return this.task.isFailed || false
},
failedMessage() {
return this.task.error || ''
},
action() {
return this.task.action || ''
},
actionIcon() {
switch (this.action) {
case 'download-podcast-episode':
return 'cloud_download'
case 'encode-m4b':
return 'sync'
default:
return 'settings'
}
},
taskIconStatus() {
if (this.isFinished && this.isFailed) {
return 'text-red-500'
}
if (this.isFinished && !this.isFailed) {
return 'text-green-500'
}
return ''
}
},
methods: {
},
mounted() {}
}
</script>
<style>
.taskRunningCardContent {
width: calc(100% - 80px);
height: 75px;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>

View file

@ -7,9 +7,12 @@
<!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }}
</p>
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
<div class="flex items-center">
<span class="truncate">{{ displayTitle }}</span>
<widgets-explicit-indicator :explicit="isExplicit" />
</div>
</div>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div>
@ -102,8 +105,10 @@
</div>
<!-- Podcast Episode # -->
<div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
<div v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
</p>
</div>
<!-- Podcast Num Episodes -->
@ -193,6 +198,9 @@ export default {
isMusic() {
return this.mediaType === 'music'
},
isExplicit() {
return this.mediaMetadata.explicit || false
},
placeholderUrl() {
const config = this.$config || this.$nuxt.$config
return `${config.routerBasePath}/book_placeholder.jpg`
@ -236,7 +244,7 @@ export default {
if (this.recentEpisode.episode) {
return this.recentEpisode.episode.replace(/^#/, '')
}
return this.recentEpisode.index
return ''
},
collapsedSeries() {
// Only added to item object when collapseSeries is enabled
@ -734,7 +742,7 @@ export default {
episodeId: this.recentEpisode.id,
title: this.recentEpisode.title,
subtitle: this.mediaMetadata.title,
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
duration: this.recentEpisode.audioFile.duration || null,
coverPath: this.media.coverPath || null
}
@ -858,7 +866,7 @@ export default {
episodeId: episode.id,
title: episode.title,
subtitle: this.mediaMetadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null
})

View file

@ -73,6 +73,12 @@ export default {
},
canCreateBookmark() {
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
}
},
methods: {
@ -111,7 +117,7 @@ export default {
},
submitCreateBookmark() {
if (!this.newBookmarkTitle) {
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat)
}
var bookmark = {
title: this.newBookmarkTitle,
@ -134,4 +140,4 @@ export default {
}
}
}
</script>
</script>

View file

@ -19,13 +19,13 @@
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelStartedAt }}</div>
<div class="px-1">
{{ $formatDate(_session.startedAt, 'MMMM do, yyyy HH:mm') }}
{{ $formatDatetime(_session.startedAt, dateFormat, timeFormat) }}
</div>
</div>
<div class="flex items-center -mx-1 mb-1">
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelUpdatedAt }}</div>
<div class="px-1">
{{ $formatDate(_session.updatedAt, 'MMMM do, yyyy HH:mm') }}
{{ $formatDatetime(_session.updatedAt, dateFormat, timeFormat) }}
</div>
</div>
<div class="flex items-center -mx-1 mb-1">
@ -151,6 +151,12 @@ export default {
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
return 'Unknown'
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
}
},
methods: {
@ -186,4 +192,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View file

@ -164,6 +164,13 @@
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.explicit != null" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.explicit" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-checkbox v-model="selectedMatch.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
<p v-if="mediaMetadata.explicit != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? 'Explicit (checked)' : 'Not Explicit (unchecked)' }}</p>
</div>
</div>
<div class="flex items-center justify-end py-2">
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
@ -327,6 +334,7 @@ export default {
res.itunesPageUrl = res.pageUrl || null
res.itunesId = res.id || null
res.author = res.artistName || null
res.explicit = res.explicit || false
return res
})
}

View file

@ -59,6 +59,14 @@ export default {
newMaxNewEpisodesToDownload: 0
}
},
watch: {
libraryItem: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
}
}
},
computed: {
isProcessing: {
get() {
@ -176,4 +184,4 @@ export default {
height: calc(100% - 80px);
max-height: calc(100% - 80px);
}
</style>
</style>

View file

@ -11,8 +11,15 @@
</template>
</div>
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
</div>
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
</div>
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
<component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episode" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
<component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episodeItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
</div>
</modals-modal>
</template>
@ -21,8 +28,8 @@
export default {
data() {
return {
episodeItem: null,
processing: false,
selectedTab: 'details',
tabs: [
{
id: 'details',
@ -37,6 +44,29 @@ export default {
]
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
const availableTabIds = this.tabs.map((tab) => tab.id)
if (!availableTabIds.length) {
this.show = false
return
}
if (!availableTabIds.includes(this.selectedTab)) {
this.selectedTab = availableTabIds[0]
}
this.episodeItem = null
this.init()
this.registerListeners()
} else {
this.unregisterListeners()
}
}
}
},
computed: {
show: {
get() {
@ -46,27 +76,118 @@ export default {
this.$store.commit('globals/setShowEditPodcastEpisodeModal', val)
}
},
selectedTab: {
get() {
return this.$store.state.editPodcastModalTab
},
set(val) {
this.$store.commit('setEditPodcastModalTab', val)
}
},
libraryItem() {
return this.$store.state.selectedLibraryItem
},
episode() {
return this.$store.state.globals.selectedEpisode
},
selectedEpisodeId() {
return this.episode.id
},
title() {
if (!this.libraryItem) return ''
return this.libraryItem.media.metadata.title || 'Unknown'
return this.libraryItem?.media.metadata.title || 'Unknown'
},
tabComponentName() {
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
const _tab = this.tabs.find((t) => t.id === this.selectedTab)
return _tab ? _tab.component : ''
},
episodeTableEpisodeIds() {
return this.$store.state.episodeTableEpisodeIds || []
},
currentEpisodeIndex() {
if (!this.episodeTableEpisodeIds.length) return 0
return this.episodeTableEpisodeIds.findIndex((bid) => bid === this.selectedEpisodeId)
},
canGoPrev() {
return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex > 0
},
canGoNext() {
return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex < this.episodeTableEpisodeIds.length - 1
}
},
methods: {
async goPrevEpisode() {
if (this.currentEpisodeIndex - 1 < 0) return
const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1]
this.processing = true
const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => {
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode'
this.$toast.error(errorMsg)
return null
})
this.processing = false
if (prevEpisode) {
this.episodeItem = prevEpisode
this.$store.commit('globals/setSelectedEpisode', prevEpisode)
} else {
console.error('Episode not found', prevEpisodeId)
}
},
async goNextEpisode() {
if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return
this.processing = true
const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1]
const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => {
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
this.$toast.error(errorMsg)
return null
})
this.processing = false
if (nextEpisode) {
this.episodeItem = nextEpisode
this.$store.commit('globals/setSelectedEpisode', nextEpisode)
} else {
console.error('Episode not found', nextEpisodeId)
}
},
selectTab(tab) {
this.selectedTab = tab
if (this.selectedTab === tab) return
if (this.tabs.find((t) => t.id === tab)) {
this.selectedTab = tab
this.processing = false
}
},
init() {
this.fetchFull()
},
async fetchFull() {
try {
this.processing = true
this.episodeItem = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${this.selectedEpisodeId}`)
this.processing = false
} catch (error) {
console.error('Failed to fetch episode', this.selectedEpisodeId, error)
this.processing = false
this.show = false
}
},
hotkey(action) {
if (action === this.$hotkeys.Modal.NEXT_PAGE) {
this.goNextEpisode()
} else if (action === this.$hotkeys.Modal.PREV_PAGE) {
this.goPrevEpisode()
}
},
registerListeners() {
this.$eventBus.$on('modal-hotkey', this.hotkey)
},
unregisterListeners() {
this.$eventBus.$off('modal-hotkey', this.hotkey)
}
},
mounted() {}
mounted() {},
beforeDestroy() {
this.unregisterListeners()
}
}
</script>
@ -77,4 +198,4 @@ export default {
.tab.tab-selected {
height: 41px;
}
</style>
</style>

View file

@ -24,8 +24,15 @@
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
</div>
<div class="px-8 py-2">
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
<p class="break-words mb-1">{{ episode.title }}</p>
<div class="flex items-center font-semibold text-gray-200">
<div v-if="episode.season || episode.episode">#</div>
<div v-if="episode.season">{{ episode.season }}x</div>
<div v-if="episode.episode">{{ episode.episode }}</div>
</div>
<div class="flex items-center mb-1">
<div class="break-words">{{ episode.title }}</div>
<widgets-podcast-type-indicator :type="episode.episodeType" />
</div>
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
</div>

View file

@ -28,6 +28,17 @@
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="$strings.LabelGenres" />
</div>
</div>
<div class="flex flex-wrap">
<div class="md:w-1/4 p-2">
<ui-dropdown :label="$strings.LabelPodcastType" v-model="podcast.type" :items="podcastTypes" small />
</div>
<div class="md:w-1/4 p-2">
<ui-text-input-with-label v-model="podcast.language" :label="$strings.LabelLanguage" />
</div>
<div class="md:w-1/4 px-2 pt-7">
<ui-checkbox v-model="podcast.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div>
</div>
<div class="p-2 w-full">
<ui-textarea-with-label v-model="podcast.description" :label="$strings.LabelDescription" :rows="3" />
</div>
@ -82,7 +93,10 @@ export default {
itunesPageUrl: '',
itunesId: '',
itunesArtistId: '',
autoDownloadEpisodes: false
autoDownloadEpisodes: false,
language: '',
explicit: false,
type: ''
}
}
},
@ -140,6 +154,9 @@ export default {
selectedFolderPath() {
if (!this.selectedFolder) return ''
return this.selectedFolder.fullPath
},
podcastTypes() {
return this.$store.state.globals.podcastTypes || []
}
},
methods: {
@ -170,7 +187,9 @@ export default {
itunesPageUrl: this.podcast.itunesPageUrl,
itunesId: this.podcast.itunesId,
itunesArtistId: this.podcast.itunesArtistId,
language: this.podcast.language
language: this.podcast.language,
explicit: this.podcast.explicit,
type: this.podcast.type
},
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
}
@ -205,9 +224,11 @@ export default {
this.podcast.itunesPageUrl = this._podcastData.pageUrl || ''
this.podcast.itunesId = this._podcastData.id || ''
this.podcast.itunesArtistId = this._podcastData.artistId || ''
this.podcast.language = this._podcastData.language || ''
this.podcast.language = this._podcastData.language || this.feedMetadata.language || ''
this.podcast.autoDownloadEpisodes = false
this.podcast.type = this._podcastData.type || this.feedMetadata.type || 'episodic'
this.podcast.explicit = this._podcastData.explicit || this.feedMetadata.explicit === 'yes' || this.feedMetadata.explicit == 'true'
if (this.folderItems[0]) {
this.selectedFolderId = this.folderItems[0].value
this.folderUpdated()
@ -226,4 +247,4 @@ export default {
#episodes-scroll {
max-height: calc(80vh - 200px);
}
</style>
</style>

View file

@ -8,7 +8,7 @@
<ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
</div>
<div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" />
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
</div>
<div class="w-2/5 p-1">
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" :label="$strings.LabelPubDate" />
@ -24,7 +24,12 @@
</div>
</div>
<div class="flex items-center justify-end pt-4">
<ui-btn @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
<!-- desktop -->
<ui-btn @click="submit" class="mx-2 hidden md:block">{{ $strings.ButtonSave }}</ui-btn>
<ui-btn @click="saveAndClose" class="mx-2 hidden md:block">{{ $strings.ButtonSaveAndClose }}</ui-btn>
<!-- mobile -->
<ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn>
</div>
<div v-if="enclosureUrl" class="py-4">
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p>
@ -89,6 +94,9 @@ export default {
},
enclosureUrl() {
return this.enclosure.url
},
episodeTypes() {
return this.$store.state.globals.episodeTypes || []
}
},
methods: {
@ -122,28 +130,43 @@ export default {
}
return updatePayload
},
submit() {
const payload = this.getUpdatePayload()
if (!Object.keys(payload).length) {
return this.$toast.info('No updates were made')
async saveAndClose() {
const wasUpdated = await this.submit()
if (wasUpdated !== null) this.$emit('close')
},
async submit() {
if (this.isProcessing) {
return null
}
const updatedDetails = this.getUpdatePayload()
if (!Object.keys(updatedDetails).length) {
this.$toast.info('No changes were made')
return false
}
return this.updateDetails(updatedDetails)
},
async updateDetails(updatedDetails) {
this.isProcessing = true
this.$axios
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
.then(() => {
this.isProcessing = false
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
console.error('Failed update episode', error)
this.isProcessing = false
this.$toast.error(error?.response?.data || 'Failed to update episode')
return false
})
this.isProcessing = false
if (updateResult) {
if (updateResult) {
this.$toast.success('Podcast episode updated')
this.$emit('close')
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode'
console.error('Failed update episode', error)
this.isProcessing = false
this.$toast.error(errorMsg)
})
return true
} else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
}
}
return false
}
},
mounted() {}
}
</script>
</script>

View file

@ -14,6 +14,27 @@
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
</div>
<div v-if="currentFeed.meta" class="mt-5">
<div class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
</div>
<div>{{ currentFeed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
</div>
<div v-if="currentFeed.meta.ownerName" class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
</div>
<div>{{ currentFeed.meta.ownerName }}</div>
</div>
<div v-if="currentFeed.meta.ownerEmail" class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
</div>
<div>{{ currentFeed.meta.ownerEmail }}</div>
</div>
</div>
</div>
<div v-else class="w-full">
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderOpenRSSFeed }}</p>
@ -22,6 +43,7 @@
<ui-text-input-with-label v-model="newFeedSlug" :label="$strings.LabelRSSFeedSlug" />
<p class="text-xs text-gray-400 py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
</div>
<widgets-rss-feed-metadata-builder v-model="metadataDetails" />
<p v-if="isHttp" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsHttps }}</p>
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsPubDate }}</p>
@ -41,7 +63,12 @@ export default {
return {
processing: false,
newFeedSlug: null,
currentFeed: null
currentFeed: null,
metadataDetails: {
preventIndexing: true,
ownerName: '',
ownerEmail: ''
}
}
},
watch: {
@ -107,7 +134,8 @@ export default {
const payload = {
serverAddress: window.origin,
slug: this.newFeedSlug
slug: this.newFeedSlug,
metadataDetails: this.metadataDetails
}
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}`

View file

@ -17,7 +17,7 @@
<td>
<p class="truncate text-xs sm:text-sm md:text-base">/{{ backup.path.replace(/\\/g, '/') }}</p>
</td>
<td class="hidden sm:table-cell font-sans text-sm">{{ backup.datePretty }}</td>
<td class="hidden sm:table-cell font-sans text-sm">{{ $formatDatetime(backup.createdAt, dateFormat, timeFormat) }}</td>
<td class="hidden sm:table-cell font-mono md:text-sm text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
<td>
<div class="w-full flex flex-row items-center justify-center">
@ -46,7 +46,7 @@
<p class="text-error text-lg font-semibold">{{ $strings.MessageImportantNotice }}</p>
<p class="text-base py-1" v-html="$strings.MessageRestoreBackupWarning" />
<p class="text-lg text-center my-8">{{ $strings.MessageRestoreBackupConfirm }} {{ selectedBackup.datePretty }}?</p>
<p class="text-lg text-center my-8">{{ $strings.MessageRestoreBackupConfirm }} {{ $formatDatetime(selectedBackup.createdAt, dateFormat, timeFormat) }}?</p>
<div class="flex px-1 items-center">
<ui-btn color="primary" @click="showConfirmApply = false">{{ $strings.ButtonNevermind }}</ui-btn>
<div class="flex-grow" />
@ -71,6 +71,12 @@ export default {
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
}
},
methods: {
@ -90,7 +96,7 @@ export default {
})
},
deleteBackupClick(backup) {
if (confirm(this.$getString('MessageConfirmDeleteBackup', [backup.datePretty]))) {
if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) {
this.processing = true
this.$axios
.$delete(`/api/backups/${backup.id}`)
@ -208,4 +214,4 @@ export default {
padding-bottom: 5px;
background-color: #333;
}
</style>
</style>

View file

@ -25,13 +25,13 @@
</div>
</td>
<td class="text-xs font-mono hidden sm:table-cell">
<ui-tooltip v-if="user.lastSeen" direction="top" :text="$formatDate(user.lastSeen, 'MMMM do, yyyy HH:mm')">
<ui-tooltip v-if="user.lastSeen" direction="top" :text="$formatDatetime(user.lastSeen, dateFormat, timeFormat)">
{{ $dateDistanceFromNow(user.lastSeen) }}
</ui-tooltip>
</td>
<td class="text-xs font-mono hidden sm:table-cell">
<ui-tooltip direction="top" :text="$formatDate(user.createdAt, 'MMMM do, yyyy HH:mm')">
{{ $formatDate(user.createdAt, 'MMM d, yyyy') }}
<ui-tooltip direction="top" :text="$formatDatetime(user.createdAt, dateFormat, timeFormat)">
{{ $formatDate(user.createdAt, dateFormat) }}
</ui-tooltip>
</td>
<td class="py-0">
@ -74,6 +74,12 @@ export default {
var usermap = {}
this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u))
return usermap
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
}
},
methods: {
@ -201,4 +207,4 @@ export default {
padding-bottom: 5px;
background-color: #272727;
}
</style>
</style>

View file

@ -0,0 +1,65 @@
<template>
<div class="w-full my-2">
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center">
<p class="pr-2 md:pr-4">{{ $strings.HeaderDownloadQueue }}</p>
<div class="h-5 md:h-7 w-5 md:w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<span class="text-sm font-mono">{{ queue.length }}</span>
</div>
</div>
<transition name="slide">
<div class="w-full">
<table class="text-sm tracksTable">
<tr>
<th class="text-left px-4 min-w-48">{{ $strings.LabelPodcast }}</th>
<th class="text-left w-32 min-w-32">{{ $strings.LabelEpisode }}</th>
<th class="text-left px-4">{{ $strings.LabelEpisodeTitle }}</th>
<th class="text-left px-4 w-48">{{ $strings.LabelPubDate }}</th>
</tr>
<template v-for="downloadQueued in queue">
<tr :key="downloadQueued.id">
<td class="px-4">
<div class="flex items-center">
<nuxt-link :to="`/item/${downloadQueued.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ downloadQueued.podcastTitle }}</nuxt-link>
<widgets-explicit-indicator :explicit="downloadQueued.podcastExplicit" />
</div>
</td>
<td>
<div class="flex items-center">
<div v-if="downloadQueued.season">{{ downloadQueued.season }}x</div>
<div v-if="downloadQueued.episode">{{ downloadQueued.episode }}</div>
<widgets-podcast-type-indicator :type="downloadQueued.episodeType" />
</div>
</td>
<td class="px-4">
{{ downloadQueued.episodeDisplayTitle }}
</td>
<td class="text-xs">
<div class="flex items-center">
<p>{{ $dateDistanceFromNow(downloadQueued.publishedAt) }}</p>
</div>
</td>
</tr>
</template>
</table>
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
queue: {
type: Array,
default: () => []
},
libraryItemId: String
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -2,16 +2,17 @@
<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 cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode">
<div class="flex-grow px-2">
<p class="text-sm font-semibold">
{{ title }}
</p>
<div class="flex items-center">
<span class="text-sm font-semibold">{{ title }}</span>
<widgets-podcast-type-indicator :type="episode.episodeType" />
</div>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
<div class="flex justify-between pt-2 max-w-xl">
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
</div>
<div class="flex items-center pt-2">
@ -128,6 +129,9 @@ export default {
},
publishedAt() {
return this.episode.publishedAt
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
}
},
methods: {
@ -205,4 +209,4 @@ export default {
}
}
}
</script>
</script>

View file

@ -160,6 +160,12 @@ export default {
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
return !itemProgress || !itemProgress.isFinished
})
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
}
},
methods: {
@ -222,7 +228,7 @@ export default {
episodeId: episode.id,
title: episode.title,
subtitle: this.mediaMetadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null
}
@ -290,7 +296,7 @@ export default {
episodeId: episode.id,
title: episode.title,
subtitle: this.mediaMetadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null
})
@ -308,6 +314,8 @@ export default {
this.showPodcastRemoveModal = true
},
editEpisode(episode) {
const episodeIds = this.episodesSorted.map((e) => e.id)
this.$store.commit('setEpisodeTableEpisodeIds', episodeIds)
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)

View file

@ -68,8 +68,6 @@ export default {
}
},
mounted() {},
beforeDestroy() {
console.log('Before destroy')
}
beforeDestroy() {}
}
</script>

View file

@ -51,8 +51,8 @@ export default {
tooltip.style.zIndex = 100
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
tooltip.innerHTML = this.text
tooltip.addEventListener('mouseover', this.cancelHide);
tooltip.addEventListener('mouseleave', this.hideTooltip);
tooltip.addEventListener('mouseover', this.cancelHide)
tooltip.addEventListener('mouseleave', this.hideTooltip)
this.setTooltipPosition(tooltip)
@ -107,7 +107,7 @@ export default {
this.isShowing = false
},
cancelHide() {
if (this.hideTimeout) clearTimeout(this.hideTimeout);
if (this.hideTimeout) clearTimeout(this.hideTimeout)
},
mouseover() {
if (!this.isShowing) this.showTooltip()

View file

@ -0,0 +1,19 @@
<template>
<ui-tooltip v-if="alreadyInLibrary" :text="$strings.LabelAlreadyInYourLibrary" direction="top">
<span class="material-icons ml-1 text-success" style="font-size: 0.8rem">check_circle</span>
</ui-tooltip>
</template>
<script>
export default {
props: {
alreadyInLibrary: Boolean
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -36,6 +36,10 @@
<p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p>
</div>
</template>
<div v-if="cronExpression && isValid" class="flex items-center justify-center text-yellow-400 mt-2">
<span class="material-icons-outlined mr-2 text-xl">event</span>
<p>{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}</p>
</div>
</div>
</div>
</template>
@ -63,6 +67,14 @@ export default {
isValid: true
}
},
watch: {
value: {
immediate: true,
handler(newVal) {
this.init()
}
}
},
computed: {
minuteIsValid() {
return !(isNaN(this.selectedMinute) || this.selectedMinute === '' || this.selectedMinute < 0 || this.selectedMinute > 59)
@ -70,6 +82,11 @@ export default {
hourIsValid() {
return !(isNaN(this.selectedHour) || this.selectedHour === '' || this.selectedHour < 0 || this.selectedHour > 23)
},
nextRun() {
if (!this.cronExpression) return ''
const parsed = this.$getNextScheduledDate(this.cronExpression)
return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || ''
},
description() {
if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return ''
@ -271,6 +288,11 @@ export default {
})
},
init() {
this.selectedInterval = 'custom'
this.selectedHour = 0
this.selectedMinute = 0
this.selectedWeekdays = []
if (!this.value) return
const pieces = this.value.split(' ')
if (pieces.length !== 5) {
@ -309,4 +331,4 @@ export default {
this.init()
}
}
</script>
</script>

View file

@ -0,0 +1,19 @@
<template>
<ui-tooltip v-if="explicit" :text="$strings.LabelExplicit" direction="top">
<span class="material-icons ml-1" style="font-size: 0.8rem">explicit</span>
</ui-tooltip>
</template>
<script>
export default {
props: {
explicit: Boolean
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -1,15 +1,51 @@
<template>
<div v-if="tasksRunning" class="w-4 h-4 mx-3 relative">
<div class="flex h-full items-center justify-center">
<widgets-loading-spinner />
</div>
<div v-if="tasksRunning" class="w-4 h-4 mx-3 relative" v-click-outside="clickOutsideObj">
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full cursor-pointer" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
<div class="flex h-full items-center justify-center">
<ui-tooltip text="Tasks running" direction="bottom" class="flex items-center">
<widgets-loading-spinner />
</ui-tooltip>
</div>
</button>
<transition name="menu">
<div class="sm:w-80 w-full relative">
<div v-show="showMenu" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalTaskRunningMenu">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-if="tasksRunningOrFailed.length">
<p class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">{{ $strings.LabelTasks }}</p>
<template v-for="task in tasksRunningOrFailed">
<nuxt-link :key="task.id" v-if="actionLink(task)" :to="actionLink(task)">
<li class="text-gray-50 select-none relative hover:bg-black-400 py-1 cursor-pointer">
<cards-item-task-running-card :task="task" />
</li>
</nuxt-link>
<li v-else :key="task.id" class="text-gray-50 select-none relative hover:bg-black-400 py-1">
<cards-item-task-running-card :task="task" />
</li>
</template>
</template>
<li v-else class="py-2 px-2">
<p>{{ $strings.MessageNoTasksRunning }}</p>
</li>
</ul>
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
data() {
return {}
return {
clickOutsideObj: {
handler: this.clickedOutside,
events: ['mousedown'],
isActive: true
},
showMenu: false,
disabled: false
}
},
computed: {
tasks() {
@ -17,9 +53,37 @@ export default {
},
tasksRunning() {
return this.tasks.some((t) => !t.isFinished)
},
tasksRunningOrFailed() {
// return just the tasks that are running or failed in the last 1 minute
return this.tasks.filter((t) => !t.isFinished || (t.isFailed && t.finishedAt > new Date().getTime() - 1000 * 60)) || []
}
},
methods: {
clickShowMenu() {
if (this.disabled) return
this.showMenu = !this.showMenu
},
clickedOutside() {
this.showMenu = false
},
actionLink(task) {
switch (task.action) {
case 'download-podcast-episode':
return `/library/${task.data.libraryId}/podcast/download-queue`
case 'encode-m4b':
return `/audiobook/${task.data.libraryItemId}/manage?tool=m4b`
default:
return ''
}
}
},
methods: {},
mounted() {}
}
</script>
</script>
<style>
.globalTaskRunningMenu {
max-height: 80vh;
}
</style>

View file

@ -39,6 +39,11 @@
</div>
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/4 px-1">
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" />
</div>
</div>
</form>
</div>
</template>
@ -65,7 +70,8 @@ export default {
itunesId: null,
itunesArtistId: null,
explicit: false,
language: null
language: null,
type: null
},
newTags: []
}
@ -93,6 +99,9 @@ export default {
},
filterData() {
return this.$store.state.libraries.filterData || {}
},
podcastTypes() {
return this.$store.state.globals.podcastTypes || []
}
},
methods: {
@ -219,6 +228,7 @@ export default {
this.details.itunesArtistId = this.mediaMetadata.itunesArtistId || ''
this.details.language = this.mediaMetadata.language || ''
this.details.explicit = !!this.mediaMetadata.explicit
this.details.type = this.mediaMetadata.type || 'episodic'
this.newTags = [...(this.media.tags || [])]
},
@ -228,4 +238,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View file

@ -0,0 +1,31 @@
<template>
<div>
<template v-if="type == 'bonus'">
<ui-tooltip text="Bonus" direction="top">
<span class="material-icons ml-1" style="font-size: 0.8rem">local_play</span>
</ui-tooltip>
</template>
<template v-if="type == 'trailer'">
<ui-tooltip text="Trailer" direction="top">
<span class="material-icons ml-1" style="font-size: 0.8rem">local_movies</span>
</ui-tooltip>
</template>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
default: 'full'
}
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -0,0 +1,90 @@
<template>
<div class="w-full py-2">
<div class="flex -mb-px">
<div class="w-1/2 h-6 rounded-tl-md relative border border-black-200 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = false">
<p class="text-sm">{{ $strings.HeaderRSSFeedGeneral }}</p>
</div>
<div class="w-1/2 h-6 rounded-tr-md relative border border-black-200 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = true">
<p class="text-sm">{{ $strings.HeaderAdvanced }}</p>
</div>
</div>
<div class="px-2 py-4 md:p-4 border border-black-200 rounded-b-md mr-px" style="min-height: 200px">
<template v-if="!showAdvancedView">
<div class="flex-grow pt-2 mb-2">
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
</div>
</template>
<template v-else>
<div class="flex-grow pt-2 mb-2">
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
</div>
<div class="w-full relative mb-1">
<ui-text-input-with-label v-model="ownerName" :label="$strings.LabelRSSFeedCustomOwnerName" />
</div>
<div class="w-full relative mb-1">
<ui-text-input-with-label v-model="ownerEmail" :label="$strings.LabelRSSFeedCustomOwnerEmail" />
</div>
</template>
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
default: () => {
return {
preventIndexing: true,
ownerName: '',
ownerEmail: ''
}
}
}
},
data() {
return {
showAdvancedView: false
}
},
watch: {},
computed: {
preventIndexing: {
get() {
return this.value.preventIndexing
},
set(value) {
this.$emit('input', {
...this.value,
preventIndexing: value
})
}
},
ownerName: {
get() {
return this.value.ownerName
},
set(value) {
this.$emit('input', {
...this.value,
ownerName: value
})
}
},
ownerEmail: {
get() {
return this.value.ownerEmail
},
set(value) {
this.$emit('input', {
...this.value,
ownerEmail: value
})
}
}
},
methods: {},
mounted() {}
}
</script>