mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-09 13:50:42 +02:00
Merge 51081f71b0
into 32da0f1224
This commit is contained in:
commit
40d4ae93d0
@ -16,6 +16,9 @@
|
|||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-primary/40'">
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/calendar`" class="grow h-full flex justify-center items-center" :class="isCalendarPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||||
|
<p class="text-sm">{{ $strings.ButtonCalendar }}</p>
|
||||||
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary/80' : 'bg-primary/40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||||
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@ -65,7 +68,7 @@
|
|||||||
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
<!-- library & collections page -->
|
<!-- library & collections page -->
|
||||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome && !isAuthorsPage">
|
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && page !== 'calendar' && !isHome && !isAuthorsPage">
|
||||||
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||||
|
|
||||||
<div class="grow hidden sm:inline-block" />
|
<div class="grow hidden sm:inline-block" />
|
||||||
@ -247,6 +250,9 @@ export default {
|
|||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.currentLibraryMediaType === 'podcast'
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
isCalendarPage() {
|
||||||
|
return this.page === 'calendar'
|
||||||
|
},
|
||||||
isLibraryPage() {
|
isLibraryPage() {
|
||||||
return this.page === ''
|
return this.page === ''
|
||||||
},
|
},
|
||||||
@ -268,6 +274,9 @@ export default {
|
|||||||
isPodcastLatestPage() {
|
isPodcastLatestPage() {
|
||||||
return this.$route.name === 'library-library-podcast-latest'
|
return this.$route.name === 'library-library-podcast-latest'
|
||||||
},
|
},
|
||||||
|
isCalendarPage() {
|
||||||
|
return this.$route.name === 'library-library-calendar'
|
||||||
|
},
|
||||||
isPodcastDownloadQueuePage() {
|
isPodcastDownloadQueuePage() {
|
||||||
return this.$route.name === 'library-library-podcast-download-queue'
|
return this.$route.name === 'library-library-podcast-download-queue'
|
||||||
},
|
},
|
||||||
|
@ -22,6 +22,12 @@
|
|||||||
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/calendar`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isCalendarPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-symbols text-2xl"></span>
|
||||||
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCalendar }}</p>
|
||||||
|
<div v-show="isCalendarPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary/80' : 'bg-bg/60'">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary/80' : 'bg-bg/60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||||
@ -173,6 +179,9 @@ export default {
|
|||||||
isPodcastLatestPage() {
|
isPodcastLatestPage() {
|
||||||
return this.$route.name === 'library-library-podcast-latest'
|
return this.$route.name === 'library-library-podcast-latest'
|
||||||
},
|
},
|
||||||
|
isCalendarPage() {
|
||||||
|
return this.$route.name === 'library-library-calendar'
|
||||||
|
},
|
||||||
homePage() {
|
homePage() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
|
@ -140,6 +140,10 @@
|
|||||||
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ timeExample }}</p>
|
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ timeExample }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<ui-dropdown :label="$strings.LabelCalendarFirstDayOfWeek" v-model="newServerSettings.calendarFirstDayOfWeek" :items="firstDayOfWeekOptions" small class="max-w-52" @input="(val) => updateSettingsKey('calendarFirstDayOfWeek', val)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
|
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
|
||||||
</div>
|
</div>
|
||||||
@ -271,6 +275,12 @@ export default {
|
|||||||
timeExample() {
|
timeExample() {
|
||||||
const date = new Date(2014, 2, 25, 17, 30, 0)
|
const date = new Date(2014, 2, 25, 17, 30, 0)
|
||||||
return this.$formatJsTime(date, this.newServerSettings.timeFormat)
|
return this.$formatJsTime(date, this.newServerSettings.timeFormat)
|
||||||
|
},
|
||||||
|
firstDayOfWeekOptions() {
|
||||||
|
return this.$store.state.globals.firstDayOfWeekOptions.map((option) => ({
|
||||||
|
text: this.$strings[option.labelKey] || option.text,
|
||||||
|
value: option.value
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -354,6 +364,10 @@ export default {
|
|||||||
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
||||||
this.scannerEnableWatcher = !this.newServerSettings.scannerDisableWatcher
|
this.scannerEnableWatcher = !this.newServerSettings.scannerDisableWatcher
|
||||||
|
|
||||||
|
if (this.newServerSettings.calendarFirstDayOfWeek === undefined) {
|
||||||
|
this.newServerSettings.calendarFirstDayOfWeek = 0
|
||||||
|
}
|
||||||
|
|
||||||
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
||||||
this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL
|
this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL
|
||||||
},
|
},
|
||||||
|
384
client/pages/library/_library/calendar.vue
Normal file
384
client/pages/library/_library/calendar.vue
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page" :class="libraryItemIdStreaming ? 'streaming' : ''">
|
||||||
|
<app-book-shelf-toolbar page="calendar" />
|
||||||
|
|
||||||
|
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
||||||
|
<div class="w-full max-w-6xl mx-auto py-4">
|
||||||
|
<!-- Mobile Layout (stacked) -->
|
||||||
|
<div class="block md:hidden mb-4 px-2 relative z-50">
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<p class="text-xl font-semibold">{{ $strings.HeaderCalendar }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center space-x-2 mb-3">
|
||||||
|
<ui-btn small @click="navigateMonth(-1)" :disabled="processing" class="h-9 px-2">
|
||||||
|
<span class="material-symbols text-lg">chevron_left</span>
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2 relative z-50">
|
||||||
|
<ui-dropdown v-model="selectedMonth" :items="monthOptions" small class="w-28" @input="onMonthYearChange" :disabled="processing" />
|
||||||
|
<ui-dropdown v-model="selectedYear" :items="yearOptions" small class="w-20" @input="onMonthYearChange" :disabled="processing" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ui-btn small @click="navigateMonth(1)" :disabled="processing" class="h-9 px-2">
|
||||||
|
<span class="material-symbols text-lg">chevron_right</span>
|
||||||
|
</ui-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<ui-btn small @click="goToToday" :disabled="processing || isCurrentMonth" class="h-9">
|
||||||
|
{{ $strings.ButtonToday }}
|
||||||
|
</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Layout (single row) -->
|
||||||
|
<div class="hidden md:flex items-center justify-between mb-4 px-4 md:px-0 relative z-50">
|
||||||
|
<p class="text-xl font-semibold">{{ $strings.HeaderCalendar }}</p>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<ui-btn small @click="navigateMonth(-1)" :disabled="processing" class="h-9 px-2">
|
||||||
|
<span class="material-symbols text-lg">chevron_left</span>
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2 min-w-64 relative z-50">
|
||||||
|
<ui-dropdown v-model="selectedMonth" :items="monthOptions" small class="w-32" @input="onMonthYearChange" :disabled="processing" />
|
||||||
|
<ui-dropdown v-model="selectedYear" :items="yearOptions" small class="w-20" @input="onMonthYearChange" :disabled="processing" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ui-btn small @click="navigateMonth(1)" :disabled="processing" class="h-9 px-2">
|
||||||
|
<span class="material-symbols text-lg">chevron_right</span>
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
|
<ui-btn small @click="goToToday" :disabled="processing || isCurrentMonth" class="ml-2 h-9">
|
||||||
|
{{ $strings.ButtonToday }}
|
||||||
|
</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="processing" class="flex justify-center py-8">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="bg-primary/25 rounded-lg overflow-hidden">
|
||||||
|
<div class="grid grid-cols-7 bg-primary/40">
|
||||||
|
<div v-for="day in weekDays" :key="day" class="p-2 sm:p-4 text-center font-semibold text-gray-200 border-r border-black/20 last:border-r-0">
|
||||||
|
<span class="text-sm sm:text-base">{{ day }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-7">
|
||||||
|
<div
|
||||||
|
v-for="day in calendarDays"
|
||||||
|
:key="`${day.date}-${day.isCurrentMonth}`"
|
||||||
|
class="min-h-24 sm:min-h-32 border-r border-b border-black/20 last:border-r-0 relative"
|
||||||
|
:class="{
|
||||||
|
'bg-black/20': !day.isCurrentMonth,
|
||||||
|
'bg-blue-600/30': day.isCurrentMonth && day.isToday,
|
||||||
|
'bg-transparent': day.isCurrentMonth && !day.isToday && !day.isWeekend,
|
||||||
|
'bg-slate-700/50': day.isCurrentMonth && !day.isToday && day.isWeekend
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div v-if="day.isToday" class="absolute inset-0 border-2 border-blue-500 pointer-events-none" style="z-index: 5"></div>
|
||||||
|
<div v-if="day.isToday" class="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full" style="z-index: 6"></div>
|
||||||
|
<div class="p-1 sm:p-2 relative" style="z-index: 1">
|
||||||
|
<span
|
||||||
|
class="text-xs sm:text-sm font-medium"
|
||||||
|
:class="{
|
||||||
|
'text-gray-500': !day.isCurrentMonth,
|
||||||
|
'text-blue-200 font-semibold': day.isCurrentMonth && day.isToday,
|
||||||
|
'text-gray-200': day.isCurrentMonth && !day.isToday
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ day.dayNumber }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-1 sm:px-2 pb-1 sm:pb-2 space-y-1 relative" style="z-index: 1">
|
||||||
|
<div v-for="episode in day.episodes" :key="episode.id" @click="clickEpisode(episode)" class="p-1 rounded text-xs cursor-pointer hover:bg-white/10 transition-colors" :class="getItemColorClass(episode)">
|
||||||
|
<div class="font-medium truncate text-xs">
|
||||||
|
{{ episode.podcastTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-300 truncate text-xs">
|
||||||
|
<span v-if="episode.season || episode.episode" class="mr-1">
|
||||||
|
<span v-if="episode.season">S{{ episode.season }}</span
|
||||||
|
><span v-if="episode.episode">E{{ episode.episode }}</span>
|
||||||
|
</span>
|
||||||
|
{{ episode.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ params, redirect, store }) {
|
||||||
|
var libraryId = params.library
|
||||||
|
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||||
|
if (!libraryData) {
|
||||||
|
return redirect('/oops?message=Library not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const library = libraryData.library
|
||||||
|
if (library.mediaType === 'book') {
|
||||||
|
return redirect(`/library/${libraryId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
libraryId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentDate: new Date(),
|
||||||
|
episodes: [],
|
||||||
|
processing: false,
|
||||||
|
openingItem: false,
|
||||||
|
selectedMonth: new Date().getMonth(),
|
||||||
|
selectedYear: new Date().getFullYear(),
|
||||||
|
itemColors: [
|
||||||
|
'bg-blue-600/80',
|
||||||
|
'bg-sky-600/80',
|
||||||
|
'bg-cyan-600/80',
|
||||||
|
'bg-indigo-600/80',
|
||||||
|
'bg-green-600/80',
|
||||||
|
'bg-emerald-600/80',
|
||||||
|
'bg-teal-600/80',
|
||||||
|
'bg-lime-600/80',
|
||||||
|
'bg-purple-600/80',
|
||||||
|
'bg-violet-600/80',
|
||||||
|
'bg-fuchsia-600/80',
|
||||||
|
'bg-pink-600/80',
|
||||||
|
'bg-red-600/80',
|
||||||
|
'bg-rose-600/80',
|
||||||
|
'bg-orange-600/80',
|
||||||
|
'bg-amber-600/80',
|
||||||
|
'bg-yellow-600/80',
|
||||||
|
'bg-yellow-500/80',
|
||||||
|
'bg-slate-600/80',
|
||||||
|
'bg-gray-600/80',
|
||||||
|
'bg-zinc-600/80',
|
||||||
|
'bg-stone-600/80',
|
||||||
|
'bg-blue-500/80',
|
||||||
|
'bg-green-500/80',
|
||||||
|
'bg-purple-500/80',
|
||||||
|
'bg-red-500/80',
|
||||||
|
'bg-orange-500/80',
|
||||||
|
'bg-pink-500/80',
|
||||||
|
'bg-indigo-500/80',
|
||||||
|
'bg-teal-500/80'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
libraryItemIdStreaming() {
|
||||||
|
return this.$store.getters['getLibraryItemIdStreaming']
|
||||||
|
},
|
||||||
|
firstDayOfWeek() {
|
||||||
|
return this.$store.state.serverSettings?.calendarFirstDayOfWeek || 0
|
||||||
|
},
|
||||||
|
monthOptions() {
|
||||||
|
const monthNames = [
|
||||||
|
this.$strings.LabelCalendarJanuary,
|
||||||
|
this.$strings.LabelCalendarFebruary,
|
||||||
|
this.$strings.LabelCalendarMarch,
|
||||||
|
this.$strings.LabelCalendarApril,
|
||||||
|
this.$strings.LabelCalendarMay,
|
||||||
|
this.$strings.LabelCalendarJune,
|
||||||
|
this.$strings.LabelCalendarJuly,
|
||||||
|
this.$strings.LabelCalendarAugust,
|
||||||
|
this.$strings.LabelCalendarSeptember,
|
||||||
|
this.$strings.LabelCalendarOctober,
|
||||||
|
this.$strings.LabelCalendarNovember,
|
||||||
|
this.$strings.LabelCalendarDecember
|
||||||
|
]
|
||||||
|
|
||||||
|
return monthNames.map((name, index) => ({
|
||||||
|
text: name,
|
||||||
|
value: index
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
yearOptions() {
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const startYear = currentYear - 10
|
||||||
|
const endYear = currentYear + 2
|
||||||
|
|
||||||
|
const years = []
|
||||||
|
for (let year = endYear; year >= startYear; year--) {
|
||||||
|
years.push({
|
||||||
|
text: year.toString(),
|
||||||
|
value: year
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return years
|
||||||
|
},
|
||||||
|
weekDays() {
|
||||||
|
const allDays = [this.$strings.LabelCalendarSundayShort, this.$strings.LabelCalendarMondayShort, this.$strings.LabelCalendarTuesdayShort, this.$strings.LabelCalendarWednesdayShort, this.$strings.LabelCalendarThursdayShort, this.$strings.LabelCalendarFridayShort, this.$strings.LabelCalendarSaturdayShort]
|
||||||
|
|
||||||
|
if (this.firstDayOfWeek === 1) {
|
||||||
|
return [...allDays.slice(1), allDays[0]]
|
||||||
|
}
|
||||||
|
return allDays
|
||||||
|
},
|
||||||
|
isCurrentMonth() {
|
||||||
|
const today = new Date()
|
||||||
|
return this.currentDate.getFullYear() === today.getFullYear() && this.currentDate.getMonth() === today.getMonth()
|
||||||
|
},
|
||||||
|
calendarDays() {
|
||||||
|
const year = this.currentDate.getFullYear()
|
||||||
|
const month = this.currentDate.getMonth()
|
||||||
|
|
||||||
|
const firstDay = new Date(year, month, 1)
|
||||||
|
const firstCalendarDay = new Date(firstDay)
|
||||||
|
|
||||||
|
let daysToSubtract = firstDay.getDay() - this.firstDayOfWeek
|
||||||
|
if (daysToSubtract < 0) {
|
||||||
|
daysToSubtract += 7
|
||||||
|
}
|
||||||
|
firstCalendarDay.setDate(firstCalendarDay.getDate() - daysToSubtract)
|
||||||
|
|
||||||
|
const days = []
|
||||||
|
const today = new Date()
|
||||||
|
const currentDay = new Date(firstCalendarDay)
|
||||||
|
|
||||||
|
for (let i = 0; i < 42; i++) {
|
||||||
|
const dayKey = this.formatDateKey(currentDay)
|
||||||
|
const dayEpisodes = this.episodes.filter((episode) => {
|
||||||
|
if (!episode.publishedAt) return false
|
||||||
|
const episodeDate = new Date(episode.publishedAt)
|
||||||
|
return this.formatDateKey(episodeDate) === dayKey
|
||||||
|
})
|
||||||
|
|
||||||
|
days.push({
|
||||||
|
date: new Date(currentDay),
|
||||||
|
dayNumber: currentDay.getDate(),
|
||||||
|
isCurrentMonth: currentDay.getMonth() === month,
|
||||||
|
isToday: this.isSameDay(currentDay, today),
|
||||||
|
isWeekend: this.isWeekendDay(currentDay),
|
||||||
|
episodes: dayEpisodes
|
||||||
|
})
|
||||||
|
|
||||||
|
currentDay.setDate(currentDay.getDate() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentDate: {
|
||||||
|
handler(newDate) {
|
||||||
|
this.selectedMonth = newDate.getMonth()
|
||||||
|
this.selectedYear = newDate.getFullYear()
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async navigateMonth(direction) {
|
||||||
|
const newDate = new Date(this.currentDate)
|
||||||
|
newDate.setMonth(newDate.getMonth() + direction)
|
||||||
|
this.currentDate = newDate
|
||||||
|
await this.loadMonthEpisodes()
|
||||||
|
},
|
||||||
|
async goToToday() {
|
||||||
|
if (this.isCurrentMonth) return
|
||||||
|
this.currentDate = new Date()
|
||||||
|
await this.loadMonthEpisodes()
|
||||||
|
},
|
||||||
|
async onMonthYearChange() {
|
||||||
|
const newDate = new Date(this.selectedYear, this.selectedMonth, 1)
|
||||||
|
this.currentDate = newDate
|
||||||
|
await this.loadMonthEpisodes()
|
||||||
|
},
|
||||||
|
async loadMonthEpisodes() {
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const year = this.currentDate.getFullYear()
|
||||||
|
const month = this.currentDate.getMonth()
|
||||||
|
const startOfMonth = new Date(year, month, 1)
|
||||||
|
const endOfMonth = new Date(year, month + 1, 0)
|
||||||
|
|
||||||
|
const startDate = new Date(startOfMonth)
|
||||||
|
startDate.setDate(startDate.getDate() - 7)
|
||||||
|
const endDate = new Date(endOfMonth)
|
||||||
|
endDate.setDate(endDate.getDate() + 7)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const episodePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episodes-calendar`, {
|
||||||
|
params: {
|
||||||
|
start: startDate.toISOString(),
|
||||||
|
end: endDate.toISOString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.episodes = episodePayload.episodes || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get calendar episodes', error)
|
||||||
|
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||||
|
this.episodes = []
|
||||||
|
} finally {
|
||||||
|
this.processing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async clickEpisode(episode) {
|
||||||
|
if (this.openingItem) return
|
||||||
|
this.openingItem = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fullLibraryItem = await this.$axios.$get(`/api/items/${episode.libraryItemId}`)
|
||||||
|
this.$store.commit('setSelectedLibraryItem', fullLibraryItem)
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
|
this.$store.commit('globals/setShowViewPodcastEpisodeModal', true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get library item', error)
|
||||||
|
this.$toast.error('Failed to get library item')
|
||||||
|
} finally {
|
||||||
|
this.openingItem = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDateKey(date) {
|
||||||
|
return date.toISOString().split('T')[0]
|
||||||
|
},
|
||||||
|
isSameDay(date1, date2) {
|
||||||
|
return this.formatDateKey(date1) === this.formatDateKey(date2)
|
||||||
|
},
|
||||||
|
isWeekendDay(date) {
|
||||||
|
const dayOfWeek = date.getDay()
|
||||||
|
return dayOfWeek === 0 || dayOfWeek === 6
|
||||||
|
},
|
||||||
|
getItemColorClass(episode) {
|
||||||
|
const podcastId = episode.podcastId || episode.libraryItemId
|
||||||
|
const hash = this.itemGroupingHash(podcastId)
|
||||||
|
return this.itemColors[hash % this.itemColors.length]
|
||||||
|
},
|
||||||
|
itemGroupingHash(str) {
|
||||||
|
let hash = 0
|
||||||
|
if (!str) return hash
|
||||||
|
|
||||||
|
const stringToHash = str.toString()
|
||||||
|
for (let i = 0; i < stringToHash.length; i++) {
|
||||||
|
const char = stringToHash.charCodeAt(i)
|
||||||
|
hash = (hash << 5) - hash + char
|
||||||
|
hash = hash & hash
|
||||||
|
}
|
||||||
|
return Math.abs(hash)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadMonthEpisodes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
}
|
||||||
|
</style>
|
@ -71,6 +71,18 @@ export const state = () => ({
|
|||||||
value: 'HH:mm'
|
value: 'HH:mm'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
firstDayOfWeekOptions: [
|
||||||
|
{
|
||||||
|
text: 'Sunday',
|
||||||
|
value: 0,
|
||||||
|
labelKey: 'LabelCalendarFirstDayOfWeekSunday'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Monday',
|
||||||
|
value: 1,
|
||||||
|
labelKey: 'LabelCalendarFirstDayOfWeekMonday'
|
||||||
|
}
|
||||||
|
],
|
||||||
podcastTypes: [
|
podcastTypes: [
|
||||||
{ text: 'Episodic', value: 'episodic', descriptionKey: 'LabelEpisodic' },
|
{ text: 'Episodic', value: 'episodic', descriptionKey: 'LabelEpisodic' },
|
||||||
{ text: 'Serial', value: 'serial', descriptionKey: 'LabelSerial' }
|
{ text: 'Serial', value: 'serial', descriptionKey: 'LabelSerial' }
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"ButtonBatchEditPopulateFromExisting": "Populate from existing",
|
"ButtonBatchEditPopulateFromExisting": "Populate from existing",
|
||||||
"ButtonBatchEditPopulateMapDetails": "Populate map details",
|
"ButtonBatchEditPopulateMapDetails": "Populate map details",
|
||||||
"ButtonBrowseForFolder": "Browse for Folder",
|
"ButtonBrowseForFolder": "Browse for Folder",
|
||||||
|
"ButtonCalendar": "Calendar",
|
||||||
"ButtonCancel": "Cancel",
|
"ButtonCancel": "Cancel",
|
||||||
"ButtonCancelEncode": "Cancel Encode",
|
"ButtonCancelEncode": "Cancel Encode",
|
||||||
"ButtonChangeRootPassword": "Change Root Password",
|
"ButtonChangeRootPassword": "Change Root Password",
|
||||||
@ -106,6 +107,7 @@
|
|||||||
"ButtonStats": "Stats",
|
"ButtonStats": "Stats",
|
||||||
"ButtonSubmit": "Submit",
|
"ButtonSubmit": "Submit",
|
||||||
"ButtonTest": "Test",
|
"ButtonTest": "Test",
|
||||||
|
"ButtonToday": "Today",
|
||||||
"ButtonUnlinkOpenId": "Unlink OpenID",
|
"ButtonUnlinkOpenId": "Unlink OpenID",
|
||||||
"ButtonUpload": "Upload",
|
"ButtonUpload": "Upload",
|
||||||
"ButtonUploadBackup": "Upload Backup",
|
"ButtonUploadBackup": "Upload Backup",
|
||||||
@ -127,6 +129,7 @@
|
|||||||
"HeaderAudiobookTools": "Audiobook File Management Tools",
|
"HeaderAudiobookTools": "Audiobook File Management Tools",
|
||||||
"HeaderAuthentication": "Authentication",
|
"HeaderAuthentication": "Authentication",
|
||||||
"HeaderBackups": "Backups",
|
"HeaderBackups": "Backups",
|
||||||
|
"HeaderCalendar": "Calendar",
|
||||||
"HeaderChangePassword": "Change Password",
|
"HeaderChangePassword": "Change Password",
|
||||||
"HeaderChapters": "Chapters",
|
"HeaderChapters": "Chapters",
|
||||||
"HeaderChooseAFolder": "Choose a Folder",
|
"HeaderChooseAFolder": "Choose a Folder",
|
||||||
@ -274,6 +277,35 @@
|
|||||||
"LabelBooks": "Books",
|
"LabelBooks": "Books",
|
||||||
"LabelButtonText": "Button Text",
|
"LabelButtonText": "Button Text",
|
||||||
"LabelByAuthor": "by {0}",
|
"LabelByAuthor": "by {0}",
|
||||||
|
"LabelCalendarApril": "April",
|
||||||
|
"LabelCalendarAugust": "August",
|
||||||
|
"LabelCalendarDecember": "December",
|
||||||
|
"LabelCalendarFebruary": "February",
|
||||||
|
"LabelCalendarFirstDayOfWeek": "First day of week",
|
||||||
|
"LabelCalendarFirstDayOfWeekMonday": "Monday",
|
||||||
|
"LabelCalendarFirstDayOfWeekSunday": "Sunday",
|
||||||
|
"LabelCalendarFriday": "Friday",
|
||||||
|
"LabelCalendarFridayShort": "Fri",
|
||||||
|
"LabelCalendarJanuary": "January",
|
||||||
|
"LabelCalendarJuly": "July",
|
||||||
|
"LabelCalendarJune": "June",
|
||||||
|
"LabelCalendarMarch": "March",
|
||||||
|
"LabelCalendarMay": "May",
|
||||||
|
"LabelCalendarMonday": "Monday",
|
||||||
|
"LabelCalendarMondayShort": "Mon",
|
||||||
|
"LabelCalendarNovember": "November",
|
||||||
|
"LabelCalendarOctober": "October",
|
||||||
|
"LabelCalendarSaturday": "Saturday",
|
||||||
|
"LabelCalendarSaturdayShort": "Sat",
|
||||||
|
"LabelCalendarSeptember": "September",
|
||||||
|
"LabelCalendarSunday": "Sunday",
|
||||||
|
"LabelCalendarSundayShort": "Sun",
|
||||||
|
"LabelCalendarThursday": "Thursday",
|
||||||
|
"LabelCalendarThursdayShort": "Thu",
|
||||||
|
"LabelCalendarTuesday": "Tuesday",
|
||||||
|
"LabelCalendarTuesdayShort": "Tue",
|
||||||
|
"LabelCalendarWednesday": "Wednesday",
|
||||||
|
"LabelCalendarWednesdayShort": "Wed",
|
||||||
"LabelChangePassword": "Change Password",
|
"LabelChangePassword": "Change Password",
|
||||||
"LabelChannels": "Channels",
|
"LabelChannels": "Channels",
|
||||||
"LabelChapterCount": "{0} Chapters",
|
"LabelChapterCount": "{0} Chapters",
|
||||||
|
@ -385,6 +385,7 @@ class Server {
|
|||||||
'/library/:library/series/:id?',
|
'/library/:library/series/:id?',
|
||||||
'/library/:library/podcast/search',
|
'/library/:library/podcast/search',
|
||||||
'/library/:library/podcast/latest',
|
'/library/:library/podcast/latest',
|
||||||
|
'/library/:library/podcast/calendar',
|
||||||
'/library/:library/podcast/download-queue',
|
'/library/:library/podcast/download-queue',
|
||||||
'/config/users/:id',
|
'/config/users/:id',
|
||||||
'/config/users/:id/sessions',
|
'/config/users/:id/sessions',
|
||||||
|
@ -1460,6 +1460,86 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/libraries/:id/episodes-calendar
|
||||||
|
* Get podcast episodes for calendar view within date range
|
||||||
|
*
|
||||||
|
* @param {LibraryControllerRequest} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
|
async getEpisodesCalendar(req, res) {
|
||||||
|
const { start, end, limit = 1000 } = req.query
|
||||||
|
|
||||||
|
if (!start || !end) {
|
||||||
|
return res.status(400).send('Start and end date parameters are required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDate = new Date(start)
|
||||||
|
const endDate = new Date(end)
|
||||||
|
|
||||||
|
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
||||||
|
return res.status(400).send('Invalid date format')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.library.mediaType !== 'podcast') {
|
||||||
|
return res.status(400).send('Library is not a podcast library')
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryItems = await Database.libraryItemModel.findAll({
|
||||||
|
where: { libraryId: req.library.id },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.podcastModel,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.podcastEpisodeModel,
|
||||||
|
where: {
|
||||||
|
publishedAt: {
|
||||||
|
[Sequelize.Op.between]: [startDate, endDate]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const episodes = []
|
||||||
|
for (const libraryItem of libraryItems) {
|
||||||
|
for (const episode of libraryItem.media.podcastEpisodes) {
|
||||||
|
episodes.push({
|
||||||
|
id: episode.id,
|
||||||
|
title: episode.title,
|
||||||
|
subtitle: episode.subtitle,
|
||||||
|
description: episode.description,
|
||||||
|
season: episode.season,
|
||||||
|
episode: episode.episode,
|
||||||
|
episodeType: episode.episodeType,
|
||||||
|
publishedAt: episode.publishedAt,
|
||||||
|
duration: episode.audioFile?.duration || 0,
|
||||||
|
audioFile: episode.audioFile,
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
podcastId: libraryItem.media.id,
|
||||||
|
podcastTitle: libraryItem.media.title,
|
||||||
|
podcastAuthor: libraryItem.media.author,
|
||||||
|
podcastExplicit: libraryItem.media.explicit,
|
||||||
|
coverPath: libraryItem.media.coverPath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
episodes.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt))
|
||||||
|
|
||||||
|
const limitedEpisodes = episodes.slice(0, parseInt(limit))
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
episodes: limitedEpisodes,
|
||||||
|
total: episodes.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {RequestWithUser} req
|
||||||
|
@ -53,6 +53,7 @@ class ServerSettings {
|
|||||||
this.dateFormat = 'MM/dd/yyyy'
|
this.dateFormat = 'MM/dd/yyyy'
|
||||||
this.timeFormat = 'HH:mm'
|
this.timeFormat = 'HH:mm'
|
||||||
this.language = 'en-us'
|
this.language = 'en-us'
|
||||||
|
this.calendarFirstDayOfWeek = 0
|
||||||
|
|
||||||
this.logLevel = Logger.logLevel
|
this.logLevel = Logger.logLevel
|
||||||
|
|
||||||
@ -120,6 +121,7 @@ class ServerSettings {
|
|||||||
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
|
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
|
||||||
this.timeFormat = settings.timeFormat || 'HH:mm'
|
this.timeFormat = settings.timeFormat || 'HH:mm'
|
||||||
this.language = settings.language || 'en-us'
|
this.language = settings.language || 'en-us'
|
||||||
|
this.calendarFirstDayOfWeek = settings.calendarFirstDayOfWeek !== undefined ? Number(settings.calendarFirstDayOfWeek) : 0
|
||||||
this.logLevel = settings.logLevel || Logger.logLevel
|
this.logLevel = settings.logLevel || Logger.logLevel
|
||||||
this.version = settings.version || null
|
this.version = settings.version || null
|
||||||
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
|
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
|
||||||
@ -231,6 +233,7 @@ class ServerSettings {
|
|||||||
dateFormat: this.dateFormat,
|
dateFormat: this.dateFormat,
|
||||||
timeFormat: this.timeFormat,
|
timeFormat: this.timeFormat,
|
||||||
language: this.language,
|
language: this.language,
|
||||||
|
calendarFirstDayOfWeek: this.calendarFirstDayOfWeek,
|
||||||
logLevel: this.logLevel,
|
logLevel: this.logLevel,
|
||||||
version: this.version,
|
version: this.version,
|
||||||
buildNumber: this.buildNumber,
|
buildNumber: this.buildNumber,
|
||||||
|
@ -75,6 +75,7 @@ class ApiRouter {
|
|||||||
this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this))
|
this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this))
|
||||||
this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
|
this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
|
||||||
this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this))
|
this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this))
|
||||||
|
this.router.get('/libraries/:id/episodes-calendar', LibraryController.middleware, LibraryController.getEpisodesCalendar.bind(this))
|
||||||
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/series/:seriesId', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
|
this.router.get('/libraries/:id/series/:seriesId', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
||||||
|
Loading…
Reference in New Issue
Block a user