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

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