mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-24 20:59:38 +00:00
Merge branch 'advplyr:master' into ffmpeg-progress
This commit is contained in:
commit
7faf42d892
44 changed files with 1146 additions and 541 deletions
|
|
@ -167,8 +167,19 @@ export default {
|
|||
this.loaded = true
|
||||
},
|
||||
async fetchCategories() {
|
||||
// Sets the limit for the number of items to be displayed based on the viewport width.
|
||||
const viewportWidth = window.innerWidth
|
||||
let limit
|
||||
if (viewportWidth >= 3240) {
|
||||
limit = 15
|
||||
} else if (viewportWidth >= 2880 && viewportWidth < 3240) {
|
||||
limit = 12
|
||||
}
|
||||
|
||||
const limitQuery = limit ? `&limit=${limit}` : ''
|
||||
|
||||
const categories = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share`)
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share${limitQuery}`)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
|
|
|
|||
|
|
@ -114,9 +114,9 @@ export default {
|
|||
|
||||
if (this.currentLibraryId) {
|
||||
configRoutes.push({
|
||||
id: 'config-library-stats',
|
||||
id: 'library-stats',
|
||||
title: this.$strings.HeaderLibraryStats,
|
||||
path: '/config/library-stats'
|
||||
path: `/library/${this.currentLibraryId}/stats`
|
||||
})
|
||||
configRoutes.push({
|
||||
id: 'config-stats',
|
||||
|
|
@ -182,4 +182,4 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -35,11 +35,13 @@
|
|||
<player-ui
|
||||
ref="audioPlayer"
|
||||
:chapters="chapters"
|
||||
:current-chapter="currentChapter"
|
||||
:paused="!isPlaying"
|
||||
:loading="playerLoading"
|
||||
:bookmarks="bookmarks"
|
||||
:sleep-timer-set="sleepTimerSet"
|
||||
:sleep-timer-remaining="sleepTimerRemaining"
|
||||
:sleep-timer-type="sleepTimerType"
|
||||
:is-podcast="isPodcast"
|
||||
@playPause="playPause"
|
||||
@jumpForward="jumpForward"
|
||||
|
|
@ -51,13 +53,16 @@
|
|||
@showBookmarks="showBookmarks"
|
||||
@showSleepTimer="showSleepTimerModal = true"
|
||||
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
||||
@showPlayerSettings="showPlayerSettingsModal = true"
|
||||
/>
|
||||
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||
|
||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||
|
||||
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" />
|
||||
|
||||
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -76,9 +81,10 @@ export default {
|
|||
currentTime: 0,
|
||||
showSleepTimerModal: false,
|
||||
showPlayerQueueItemsModal: false,
|
||||
showPlayerSettingsModal: false,
|
||||
sleepTimerSet: false,
|
||||
sleepTimerTime: 0,
|
||||
sleepTimerRemaining: 0,
|
||||
sleepTimerType: null,
|
||||
sleepTimer: null,
|
||||
displayTitle: null,
|
||||
currentPlaybackRate: 1,
|
||||
|
|
@ -145,6 +151,9 @@ export default {
|
|||
if (this.streamEpisode) return this.streamEpisode.chapters || []
|
||||
return this.media.chapters || []
|
||||
},
|
||||
currentChapter() {
|
||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||
},
|
||||
title() {
|
||||
if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle
|
||||
return this.mediaMetadata.title || 'No Title'
|
||||
|
|
@ -204,14 +213,18 @@ export default {
|
|||
this.$store.commit('setIsPlaying', isPlaying)
|
||||
this.updateMediaSessionPlaybackState()
|
||||
},
|
||||
setSleepTimer(seconds) {
|
||||
setSleepTimer(time) {
|
||||
this.sleepTimerSet = true
|
||||
this.sleepTimerTime = seconds
|
||||
this.sleepTimerRemaining = seconds
|
||||
this.runSleepTimer()
|
||||
this.showSleepTimerModal = false
|
||||
|
||||
this.sleepTimerType = time.timerType
|
||||
if (this.sleepTimerType === this.$constants.SleepTimerTypes.COUNTDOWN) {
|
||||
this.runSleepTimer(time)
|
||||
}
|
||||
},
|
||||
runSleepTimer() {
|
||||
runSleepTimer(time) {
|
||||
this.sleepTimerRemaining = time.seconds
|
||||
|
||||
var lastTick = Date.now()
|
||||
clearInterval(this.sleepTimer)
|
||||
this.sleepTimer = setInterval(() => {
|
||||
|
|
@ -220,12 +233,23 @@ export default {
|
|||
this.sleepTimerRemaining -= elapsed / 1000
|
||||
|
||||
if (this.sleepTimerRemaining <= 0) {
|
||||
this.clearSleepTimer()
|
||||
this.playerHandler.pause()
|
||||
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
||||
this.sleepTimerEnd()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
checkChapterEnd(time) {
|
||||
if (!this.currentChapter) return
|
||||
const chapterEndTime = this.currentChapter.end
|
||||
const tolerance = 0.75
|
||||
if (time >= chapterEndTime - tolerance) {
|
||||
this.sleepTimerEnd()
|
||||
}
|
||||
},
|
||||
sleepTimerEnd() {
|
||||
this.clearSleepTimer()
|
||||
this.playerHandler.pause()
|
||||
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
||||
},
|
||||
cancelSleepTimer() {
|
||||
this.showSleepTimerModal = false
|
||||
this.clearSleepTimer()
|
||||
|
|
@ -235,6 +259,7 @@ export default {
|
|||
this.sleepTimerRemaining = 0
|
||||
this.sleepTimer = null
|
||||
this.sleepTimerSet = false
|
||||
this.sleepTimerType = null
|
||||
},
|
||||
incrementSleepTimer(amount) {
|
||||
if (!this.sleepTimerSet) return
|
||||
|
|
@ -275,6 +300,10 @@ export default {
|
|||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.setCurrentTime(time)
|
||||
}
|
||||
|
||||
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
||||
this.checkChapterEnd(time)
|
||||
}
|
||||
},
|
||||
setDuration(duration) {
|
||||
this.totalDuration = duration
|
||||
|
|
|
|||
|
|
@ -79,6 +79,14 @@
|
|||
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" 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="isStatsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-symbols text-2xl">monitoring</span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
|
||||
|
||||
<div v-show="isStatsPage" 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/search`" 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="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="abs-icons icon-podcast text-xl"></span>
|
||||
|
||||
|
|
@ -103,7 +111,7 @@
|
|||
<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'">
|
||||
<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-symbols text-2xl">warning</span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
||||
|
|
@ -194,6 +202,9 @@ export default {
|
|||
isPlaylistsPage() {
|
||||
return this.paramId === 'playlists'
|
||||
},
|
||||
isStatsPage() {
|
||||
return this.$route.name === 'library-library-stats'
|
||||
},
|
||||
libraryBookshelfPage() {
|
||||
return this.$route.name === 'library-library-bookshelf-id'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -81,16 +81,16 @@ export default {
|
|||
return this.store.getters['user/getSizeMultiplier']
|
||||
},
|
||||
seriesId() {
|
||||
return this.series ? this.series.id : ''
|
||||
return this.series?.id || ''
|
||||
},
|
||||
title() {
|
||||
return this.series ? this.series.name : ''
|
||||
return this.series?.name || ''
|
||||
},
|
||||
nameIgnorePrefix() {
|
||||
return this.series ? this.series.nameIgnorePrefix : ''
|
||||
return this.series?.nameIgnorePrefix || ''
|
||||
},
|
||||
displayTitle() {
|
||||
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
|
||||
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title || '\u00A0'
|
||||
return this.title || '\u00A0'
|
||||
},
|
||||
displaySortLine() {
|
||||
|
|
@ -110,13 +110,13 @@ export default {
|
|||
}
|
||||
},
|
||||
books() {
|
||||
return this.series ? this.series.books || [] : []
|
||||
return this.series?.books || []
|
||||
},
|
||||
addedAt() {
|
||||
return this.series ? this.series.addedAt : 0
|
||||
return this.series?.addedAt || 0
|
||||
},
|
||||
totalDuration() {
|
||||
return this.series ? this.series.totalDuration : 0
|
||||
return this.series?.totalDuration || 0
|
||||
},
|
||||
seriesBookProgress() {
|
||||
return this.books
|
||||
|
|
@ -161,7 +161,7 @@ export default {
|
|||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||
},
|
||||
rssFeed() {
|
||||
return this.series ? this.series.rssFeed : null
|
||||
return this.series?.rssFeed
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
70
client/components/modals/PlayerSettingsModal.vue
Normal file
70
client/components/modals/PlayerSettingsModal.vue
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<modals-modal v-model="show" name="player-settings" :width="500" :height="'unset'">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-4" style="max-height: 80vh; min-height: 40vh">
|
||||
<h3 class="text-xl font-semibold mb-8">{{ $strings.HeaderPlayerSettings }}</h3>
|
||||
<div class="flex items-center mb-4">
|
||||
<ui-toggle-switch v-model="useChapterTrack" @input="setUseChapterTrack" />
|
||||
<div class="pl-4">
|
||||
<span>{{ $strings.LabelUseChapterTrack }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<ui-select-input v-model="jumpForwardAmount" :label="$strings.LabelJumpForwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpForwardAmount" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ui-select-input v-model="jumpBackwardAmount" :label="$strings.LabelJumpBackwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpBackwardAmount" />
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
useChapterTrack: false,
|
||||
jumpValues: [
|
||||
{ text: this.$getString('LabelTimeDurationXSeconds', ['10']), value: 10 },
|
||||
{ text: this.$getString('LabelTimeDurationXSeconds', ['15']), value: 15 },
|
||||
{ text: this.$getString('LabelTimeDurationXSeconds', ['30']), value: 30 },
|
||||
{ text: this.$getString('LabelTimeDurationXSeconds', ['60']), value: 60 },
|
||||
{ text: this.$getString('LabelTimeDurationXMinutes', ['2']), value: 120 },
|
||||
{ text: this.$getString('LabelTimeDurationXMinutes', ['5']), value: 300 }
|
||||
],
|
||||
jumpForwardAmount: 10,
|
||||
jumpBackwardAmount: 10
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setUseChapterTrack() {
|
||||
this.$store.dispatch('user/updateUserSettings', { useChapterTrack: this.useChapterTrack })
|
||||
},
|
||||
setJumpForwardAmount(val) {
|
||||
this.jumpForwardAmount = val
|
||||
this.$store.dispatch('user/updateUserSettings', { jumpForwardAmount: val })
|
||||
},
|
||||
setJumpBackwardAmount(val) {
|
||||
this.jumpBackwardAmount = val
|
||||
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
|
||||
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
||||
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -6,34 +6,36 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="!timerSet" class="w-full">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div class="w-full">
|
||||
<template v-for="time in sleepTimes">
|
||||
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-bg relative" @click="setTime(time.seconds)">
|
||||
<p class="text-xl text-center">{{ time.text }}</p>
|
||||
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-primary/25 relative" @click="setTime(time)">
|
||||
<p class="text-lg text-center">{{ time.text }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
||||
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" placeholder="Time in minutes" class="w-48" />
|
||||
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" :placeholder="$strings.LabelTimeInMinutes" class="w-48" />
|
||||
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else class="w-full p-4">
|
||||
<div class="mb-4 flex items-center justify-center">
|
||||
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center mr-4" @click="decrement(30 * 60)">
|
||||
<div v-if="timerSet" class="w-full p-4">
|
||||
<div class="mb-4 h-px w-full bg-white/10" />
|
||||
|
||||
<div v-if="timerType === $constants.SleepTimerTypes.COUNTDOWN" class="mb-4 flex items-center justify-center space-x-4">
|
||||
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center h-9" @click="decrement(30 * 60)">
|
||||
<span class="material-symbols text-lg">remove</span>
|
||||
<span class="pl-1 text-base font-mono">30m</span>
|
||||
<span class="pl-1 text-sm">30m</span>
|
||||
</ui-btn>
|
||||
|
||||
<ui-icon-btn icon="remove" @click="decrement(60 * 5)" />
|
||||
<ui-icon-btn icon="remove" class="min-w-9" @click="decrement(60 * 5)" />
|
||||
|
||||
<p class="mx-6 text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
|
||||
<p class="text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
|
||||
|
||||
<ui-icon-btn icon="add" @click="increment(60 * 5)" />
|
||||
<ui-icon-btn icon="add" class="min-w-9" @click="increment(60 * 5)" />
|
||||
|
||||
<ui-btn :padding-x="2" small class="flex items-center ml-4" @click="increment(30 * 60)">
|
||||
<ui-btn :padding-x="2" small class="flex items-center h-9" @click="increment(30 * 60)">
|
||||
<span class="material-symbols text-lg">add</span>
|
||||
<span class="pl-1 text-base font-mono">30m</span>
|
||||
<span class="pl-1 text-sm">30m</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
<ui-btn class="w-full" @click="$emit('cancel')">{{ $strings.ButtonCancel }}</ui-btn>
|
||||
|
|
@ -47,52 +49,13 @@ export default {
|
|||
props: {
|
||||
value: Boolean,
|
||||
timerSet: Boolean,
|
||||
timerTime: Number,
|
||||
remaining: Number
|
||||
timerType: String,
|
||||
remaining: Number,
|
||||
hasChapters: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
customTime: null,
|
||||
sleepTimes: [
|
||||
{
|
||||
seconds: 60 * 5,
|
||||
text: '5 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 15,
|
||||
text: '15 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 20,
|
||||
text: '20 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 30,
|
||||
text: '30 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 45,
|
||||
text: '45 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 60,
|
||||
text: '60 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 90,
|
||||
text: '90 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 120,
|
||||
text: '2 hours'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
}
|
||||
customTime: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -103,6 +66,54 @@ export default {
|
|||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
sleepTimes() {
|
||||
const times = [
|
||||
{
|
||||
seconds: 60 * 5,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['5']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 15,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['15']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 20,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['20']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 30,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['30']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 45,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['45']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 60,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['60']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 90,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['90']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 120,
|
||||
text: this.$getString('LabelTimeDurationXHours', ['2']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
}
|
||||
]
|
||||
if (this.hasChapters) {
|
||||
times.push({ seconds: -1, text: this.$strings.LabelEndOfChapter, timerType: this.$constants.SleepTimerTypes.CHAPTER })
|
||||
}
|
||||
return times
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -113,10 +124,14 @@ export default {
|
|||
}
|
||||
|
||||
const timeInSeconds = Math.round(Number(this.customTime) * 60)
|
||||
this.setTime(timeInSeconds)
|
||||
const time = {
|
||||
seconds: timeInSeconds,
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
}
|
||||
this.setTime(time)
|
||||
},
|
||||
setTime(seconds) {
|
||||
this.$emit('set', seconds)
|
||||
setTime(time) {
|
||||
this.$emit('set', time)
|
||||
},
|
||||
increment(amount) {
|
||||
this.$emit('increment', amount)
|
||||
|
|
@ -130,4 +145,4 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -16,11 +16,18 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-lg font-semibold mb-2">{{ $strings.HeaderPodcastsToAdd }}</p>
|
||||
<p class="text-lg font-semibold mb-1">{{ $strings.HeaderPodcastsToAdd }}</p>
|
||||
<p class="text-sm text-gray-300 mb-4">{{ $strings.MessageOpmlPreviewNote }}</p>
|
||||
|
||||
<div class="w-full overflow-y-auto" style="max-height: 50vh">
|
||||
<template v-for="(feed, index) in feedMetadata">
|
||||
<cards-podcast-feed-summary-card :key="index" :feed="feed" :library-folder-path="selectedFolderPath" class="my-1" />
|
||||
<template v-for="(feed, index) in feeds">
|
||||
<div :key="index" class="py-1 flex items-center">
|
||||
<p class="text-lg font-semibold">{{ index + 1 }}.</p>
|
||||
<div class="pl-2">
|
||||
<p v-if="feed.title" class="text-sm font-semibold">{{ feed.title }}</p>
|
||||
<p class="text-xs text-gray-400">{{ feed.feedUrl }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -45,9 +52,7 @@ export default {
|
|||
return {
|
||||
processing: false,
|
||||
selectedFolderId: null,
|
||||
fullPath: null,
|
||||
autoDownloadEpisodes: false,
|
||||
feedMetadata: []
|
||||
autoDownloadEpisodes: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -96,73 +101,36 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
toFeedMetadata(feed) {
|
||||
const metadata = feed.metadata
|
||||
return {
|
||||
title: metadata.title,
|
||||
author: metadata.author,
|
||||
description: metadata.description,
|
||||
releaseDate: '',
|
||||
genres: [...metadata.categories],
|
||||
feedUrl: metadata.feedUrl,
|
||||
imageUrl: metadata.image,
|
||||
itunesPageUrl: '',
|
||||
itunesId: '',
|
||||
itunesArtistId: '',
|
||||
language: '',
|
||||
numEpisodes: feed.numEpisodes
|
||||
}
|
||||
},
|
||||
init() {
|
||||
this.feedMetadata = this.feeds.map(this.toFeedMetadata)
|
||||
|
||||
if (this.folderItems[0]) {
|
||||
this.selectedFolderId = this.folderItems[0].value
|
||||
}
|
||||
},
|
||||
async submit() {
|
||||
this.processing = true
|
||||
const newFeedPayloads = this.feedMetadata.map((metadata) => {
|
||||
return {
|
||||
path: `${this.selectedFolderPath}/${this.$sanitizeFilename(metadata.title)}`,
|
||||
folderId: this.selectedFolderId,
|
||||
libraryId: this.currentLibrary.id,
|
||||
media: {
|
||||
metadata: {
|
||||
...metadata
|
||||
},
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log('New feed payloads', newFeedPayloads)
|
||||
|
||||
for (const podcastPayload of newFeedPayloads) {
|
||||
await this.$axios
|
||||
.$post('/api/podcasts', podcastPayload)
|
||||
.then(() => {
|
||||
this.$toast.success(`${podcastPayload.media.metadata.title}: ${this.$strings.ToastPodcastCreateSuccess}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastPodcastCreateFailed
|
||||
console.error('Failed to create podcast', podcastPayload, error)
|
||||
this.$toast.error(`${podcastPayload.media.metadata.title}: ${errorMsg}`)
|
||||
})
|
||||
const payload = {
|
||||
feeds: this.feeds.map((f) => f.feedUrl),
|
||||
folderId: this.selectedFolderId,
|
||||
libraryId: this.currentLibrary.id,
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes
|
||||
}
|
||||
this.processing = false
|
||||
this.show = false
|
||||
this.$axios
|
||||
.$post('/api/podcasts/opml/create', payload)
|
||||
.then(() => {
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMsg = error.response?.data || this.$strings.ToastPodcastCreateFailed
|
||||
console.error('Failed to create podcast', payload, error)
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#podcast-wrapper {
|
||||
min-height: 400px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
#episodes-scroll {
|
||||
max-height: calc(80vh - 200px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -132,7 +132,7 @@ export default {
|
|||
this.searchedTitle = this.episodeTitle
|
||||
this.isProcessing = true
|
||||
this.$axios
|
||||
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${this.$encodeUriPath(this.episodeTitle)}`)
|
||||
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${encodeURIComponent(this.episodeTitle)}`)
|
||||
.then((results) => {
|
||||
this.episodesFound = results.episodes.map((ep) => ep.episode)
|
||||
console.log('Episodes found', this.episodesFound)
|
||||
|
|
@ -153,4 +153,4 @@ export default {
|
|||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -7,17 +7,17 @@
|
|||
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonJumpBackward">
|
||||
<button :aria-label="$strings.ButtonJumpBackward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">replay_10</span>
|
||||
<ui-tooltip direction="top" :text="jumpBackwardText">
|
||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||
</button>
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonJumpForward">
|
||||
<button :aria-label="$strings.ButtonJumpForward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">forward_10</span>
|
||||
<ui-tooltip direction="top" :text="jumpForwardText">
|
||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8">
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||
<span class="material-symbols">autorenew</span>
|
||||
<span class="material-symbols text-2xl">autorenew</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex-grow" />
|
||||
|
|
@ -56,6 +56,12 @@ export default {
|
|||
set(val) {
|
||||
this.$emit('update:playbackRate', val)
|
||||
}
|
||||
},
|
||||
jumpForwardText() {
|
||||
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
|
||||
},
|
||||
jumpBackwardText() {
|
||||
return this.getJumpText('jumpBackwardAmount', this.$strings.ButtonJumpBackward)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -83,8 +89,22 @@ export default {
|
|||
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
||||
console.error('Failed to update settings', err)
|
||||
})
|
||||
},
|
||||
getJumpText(setting, prefix) {
|
||||
const amount = this.$store.getters['user/getUserSetting'](setting)
|
||||
if (!amount) return prefix
|
||||
|
||||
let formattedTime = ''
|
||||
if (amount <= 60) {
|
||||
formattedTime = this.$getString('LabelTimeDurationXSeconds', [amount])
|
||||
} else {
|
||||
const minutes = Math.floor(amount / 60)
|
||||
formattedTime = this.$getString('LabelTimeDurationXMinutes', [minutes])
|
||||
}
|
||||
|
||||
return `${prefix} - ${formattedTime}`
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
|
||||
<div v-else class="flex items-center">
|
||||
<span class="material-symbols text-lg text-warning">snooze</span>
|
||||
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
||||
<p class="text-sm sm:text-lg text-warning font-mono font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
|
|
@ -36,9 +36,9 @@
|
|||
</button>
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
||||
<button :aria-label="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack" class="text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
||||
<span class="material-symbols text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
|
||||
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
|
||||
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerSettings')">
|
||||
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
|
@ -72,12 +72,14 @@ export default {
|
|||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
currentChapter: Object,
|
||||
bookmarks: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
sleepTimerSet: Boolean,
|
||||
sleepTimerRemaining: Number,
|
||||
sleepTimerType: String,
|
||||
isPodcast: Boolean,
|
||||
hideBookmarks: Boolean,
|
||||
hideSleepTimer: Boolean
|
||||
|
|
@ -90,27 +92,34 @@ export default {
|
|||
seekLoading: false,
|
||||
showChaptersModal: false,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
useChapterTrack: false
|
||||
duration: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
playbackRate() {
|
||||
this.updateTimestamp()
|
||||
},
|
||||
useChapterTrack() {
|
||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||
this.updateTimestamp()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sleepTimerRemainingString() {
|
||||
var rounded = Math.round(this.sleepTimerRemaining)
|
||||
if (rounded < 90) {
|
||||
return `${rounded}s`
|
||||
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER) {
|
||||
return 'EoC'
|
||||
} else {
|
||||
var rounded = Math.round(this.sleepTimerRemaining)
|
||||
if (rounded < 90) {
|
||||
return `${rounded}s`
|
||||
}
|
||||
var minutesRounded = Math.round(rounded / 60)
|
||||
if (minutesRounded <= 90) {
|
||||
return `${minutesRounded}m`
|
||||
}
|
||||
var hoursRounded = Math.round(minutesRounded / 60)
|
||||
return `${hoursRounded}h`
|
||||
}
|
||||
var minutesRounded = Math.round(rounded / 60)
|
||||
if (minutesRounded < 90) {
|
||||
return `${minutesRounded}m`
|
||||
}
|
||||
var hoursRounded = Math.round(minutesRounded / 60)
|
||||
return `${hoursRounded}h`
|
||||
},
|
||||
token() {
|
||||
return this.$store.getters['user/getToken']
|
||||
|
|
@ -135,9 +144,6 @@ export default {
|
|||
if (!duration) return 0
|
||||
return Math.round((100 * time) / duration)
|
||||
},
|
||||
currentChapter() {
|
||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||
},
|
||||
currentChapterName() {
|
||||
return this.currentChapter ? this.currentChapter.title : ''
|
||||
},
|
||||
|
|
@ -162,6 +168,10 @@ export default {
|
|||
},
|
||||
playerQueueItems() {
|
||||
return this.$store.state.playerQueueItems || []
|
||||
},
|
||||
useChapterTrack() {
|
||||
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
||||
return this.chapters.length ? _useChapterTrack : false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -310,9 +320,6 @@ export default {
|
|||
init() {
|
||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||
|
||||
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
||||
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
|
||||
|
||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||
this.setPlaybackRate(this.playbackRate)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
||||
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-symbols" :class="iconClass">more_vert</span>
|
||||
<span class="material-symbols text-2xl" :class="iconClass">more_vert</span>
|
||||
</button>
|
||||
<div v-else class="h-full w-full flex items-center justify-center">
|
||||
<widgets-loading-spinner />
|
||||
|
|
@ -116,4 +116,4 @@ export default {
|
|||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
151
client/components/ui/SelectInput.vue
Normal file
151
client/components/ui/SelectInput.vue
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div class="relative w-full">
|
||||
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||
<button ref="buttonWrapper" type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<span class="flex items-center">
|
||||
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
||||
<span v-if="selectedSubtext">: </span>
|
||||
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
|
||||
</span>
|
||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<span class="material-symbols text-2xl">expand_more</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<transition name="menu">
|
||||
<ul ref="menu" v-show="showMenu" class="absolute z-60 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox" :style="{ maxHeight: menuMaxHeight }" v-click-outside="clickOutsideObj">
|
||||
<template v-for="item in itemsToShow">
|
||||
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click.stop.prevent="clickedOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
||||
<span v-if="item.subtext">: </span>
|
||||
<span v-if="item.subtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ item.subtext }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: [String, Number],
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
disabled: Boolean,
|
||||
small: Boolean,
|
||||
menuMaxHeight: {
|
||||
type: String,
|
||||
default: '224px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
clickOutsideObj: {
|
||||
handler: this.clickedOutside,
|
||||
events: ['click'],
|
||||
isActive: true
|
||||
},
|
||||
menu: null,
|
||||
showMenu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selected: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
itemsToShow() {
|
||||
return this.items.map((i) => {
|
||||
if (typeof i === 'string' || typeof i === 'number') {
|
||||
return {
|
||||
text: i,
|
||||
value: i
|
||||
}
|
||||
}
|
||||
return i
|
||||
})
|
||||
},
|
||||
selectedItem() {
|
||||
return this.itemsToShow.find((i) => i.value === this.selected)
|
||||
},
|
||||
selectedText() {
|
||||
return this.selectedItem ? this.selectedItem.text : ''
|
||||
},
|
||||
selectedSubtext() {
|
||||
return this.selectedItem ? this.selectedItem.subtext : ''
|
||||
},
|
||||
buttonClass() {
|
||||
var classes = []
|
||||
if (this.small) classes.push('h-9')
|
||||
else classes.push('h-10')
|
||||
|
||||
if (this.disabled) classes.push('cursor-not-allowed border-gray-600 bg-primary bg-opacity-70 border-opacity-70 text-gray-400')
|
||||
else classes.push('cursor-pointer border-gray-600 bg-primary text-gray-100')
|
||||
|
||||
return classes.join(' ')
|
||||
},
|
||||
longLabel() {
|
||||
let result = ''
|
||||
if (this.label) result += this.label + ': '
|
||||
if (this.selectedText) result += this.selectedText
|
||||
if (this.selectedSubtext) result += ' ' + this.selectedSubtext
|
||||
return result
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
recalcMenuPos() {
|
||||
if (!this.menu || !this.$refs.buttonWrapper) return
|
||||
const boundingBox = this.$refs.buttonWrapper.getBoundingClientRect()
|
||||
this.menu.style.top = boundingBox.y + boundingBox.height - 4 + 'px'
|
||||
this.menu.style.left = boundingBox.x + 'px'
|
||||
this.menu.style.width = boundingBox.width + 'px'
|
||||
},
|
||||
unmountMountMenu() {
|
||||
if (!this.$refs.menu || !this.$refs.buttonWrapper) return
|
||||
this.menu = this.$refs.menu
|
||||
this.menu.remove()
|
||||
},
|
||||
clickShowMenu() {
|
||||
if (this.disabled) return
|
||||
if (!this.showMenu) this.handleShowMenu()
|
||||
else this.handleCloseMenu()
|
||||
},
|
||||
handleShowMenu() {
|
||||
if (!this.menu) {
|
||||
this.unmountMountMenu()
|
||||
}
|
||||
document.body.appendChild(this.menu)
|
||||
this.recalcMenuPos()
|
||||
this.showMenu = true
|
||||
},
|
||||
handleCloseMenu() {
|
||||
this.showMenu = false
|
||||
if (this.menu) this.menu.remove()
|
||||
},
|
||||
clickedOutside() {
|
||||
this.handleCloseMenu()
|
||||
},
|
||||
clickedOption(itemValue) {
|
||||
this.selected = itemValue
|
||||
this.handleCloseMenu()
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
beforeDestroy() {
|
||||
if (this.menu) this.menu.remove()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue