Skip to content

Commit 0a0f342

Browse files
committed
feat: video overwrite queue on play
1 parent d787882 commit 0a0f342

7 files changed

Lines changed: 319 additions & 34 deletions

File tree

src/App.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@
4444
<Book :size="20" />
4545
</template>
4646
</NcAppNavigationItem>
47-
<NcAppNavigationItem name="Videos" :to="{ path: '/videos' }">
47+
<NcAppNavigationItem
48+
name="Videos"
49+
:to="{ path: '/videos' }"
50+
:active="isPrefixRoute('/videos')">
4851
<template #icon>
4952
<Filmstrip :size="20" />
5053
</template>

src/components/media/MiniPlayer.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export default defineComponent({
5252
5353
const closePlayer = () => {
5454
playback.pause()
55+
playback.unregisterExternalPlayer()
5556
disposePlayer()
5657
}
5758
@@ -64,22 +65,37 @@ export default defineComponent({
6465
const wasPlaying = !player.value.paused()
6566
const currentTime = player.value.currentTime()
6667
68+
console.log('Mini player appearing, wasPlaying:', wasPlaying, 'currentTime:', currentTime)
69+
6770
// Move the video.js player to the mini player element
6871
const playerEl = player.value.el()
6972
if (miniVideoElement.value.parentNode && playerEl) {
7073
miniVideoElement.value.parentNode.replaceChild(playerEl, miniVideoElement.value)
7174
miniVideoElement.value = playerEl.querySelector('video')
7275
76+
// Register the player with playback for MediaControls integration
77+
playback.registerExternalPlayer({
78+
play: () => player.value!.play(),
79+
pause: () => player.value!.pause(),
80+
paused: () => player.value!.paused(),
81+
})
82+
7383
// Restore playing state after move - always call play if it was playing
7484
if (wasPlaying) {
7585
// Wait for DOM to settle
7686
await nextTick()
7787
setTimeout(() => {
7888
if (player.value) {
7989
player.value.currentTime(currentTime)
80-
player.value.play().catch((err) => console.warn('Failed to resume play in mini player:', err))
90+
// The play event listener will handle updating the playing state
91+
player.value.play()
92+
.then(() => console.log('Video resumed in mini player'))
93+
.catch((err) => console.warn('Failed to resume play in mini player:', err))
8194
}
8295
}, 50)
96+
} else {
97+
// Update playing state to match current player state
98+
playback.updatePlayingState(!player.value.paused())
8399
}
84100
}
85101
}

src/components/media/QueuePopover.vue

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@
6565
</NcActionButton>
6666
</template>
6767
</RadioStationListItem>
68+
<VideoListItem
69+
v-else-if="media.type == 'video'"
70+
:key="'video-' + media.id"
71+
:video="media as unknown as Video"
72+
@play="onPlay(media)"
73+
disable-play-next
74+
disable-add-to-queue>
75+
<template #actions-end>
76+
<NcActionButton @click.stop="onRemove(media)">
77+
<template #icon>
78+
<Delete :size="20" />
79+
</template>
80+
Remove from Queue
81+
</NcActionButton>
82+
</template>
83+
</VideoListItem>
6884
</ul>
6985
</div>
7086
<p v-else class="empty-message">The queue is empty.</p>
@@ -79,10 +95,11 @@
7995
import TrackListItem from '@/components/media/TrackListItem.vue'
8096
import PodcastEpisodeListItem from '@/components/media/PodcastEpisodeListItem.vue'
8197
import RadioStationListItem from '@/components/media/RadioStationListItem.vue'
98+
import VideoListItem from '@/components/media/VideoListItem.vue'
8299
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
83100
import Delete from '@icons/Delete.vue'
84101
import playback, { toPlayable, type Playable } from '@/composables/usePlayback'
85-
import type { Track, PodcastEpisode, RadioStation } from '@/models/media'
102+
import type { Track, PodcastEpisode, RadioStation, Video } from '@/models/media'
86103
87104
export default defineComponent({
88105
name: 'QueuePopover',
@@ -91,6 +108,7 @@
91108
TrackListItem,
92109
PodcastEpisodeListItem,
93110
RadioStationListItem,
111+
VideoListItem,
94112
NcActionButton,
95113
Delete,
96114
},

src/components/media/VideoGalleryItem.vue

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@
66
<div class="duration-overlay" v-if="video.duration">
77
{{ formatDuration(video.duration) }}
88
</div>
9+
10+
<NcActions class="actions-button" @click.stop="null">
11+
<NcActionButton @click.stop="handleClick">
12+
<template #icon>
13+
<Play :size="20" />
14+
</template>
15+
Play
16+
</NcActionButton>
17+
18+
<NcActionButton @click.stop="onPlayNext">
19+
<template #icon>
20+
<SkipNext :size="20" />
21+
</template>
22+
Play Next
23+
</NcActionButton>
24+
25+
<NcActionButton @click.stop="onAddToQueue">
26+
<template #icon>
27+
<PlaylistPlus :size="20" />
28+
</template>
29+
Add to Queue
30+
</NcActionButton>
31+
</NcActions>
932
</div>
1033
<div class="metadata">
1134
<div class="title">{{ video.title || 'Untitled' }}</div>
@@ -23,8 +46,14 @@
2346
import { defineComponent, type PropType } from 'vue'
2447
import { useRouter } from 'vue-router'
2548
import type { Video } from '@/models/media'
49+
import playback from '@/composables/usePlayback'
2650
51+
import NcActions from '@nextcloud/vue/components/NcActions'
52+
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
2753
import VideoIcon from '@icons/Video.vue'
54+
import Play from '@icons/Play.vue'
55+
import SkipNext from '@icons/SkipNext.vue'
56+
import PlaylistPlus from '@icons/PlaylistPlus.vue'
2857
2958
export default defineComponent({
3059
name: 'VideoGalleryItem',
@@ -40,6 +69,11 @@
4069
},
4170
components: {
4271
VideoIcon,
72+
NcActions,
73+
NcActionButton,
74+
Play,
75+
SkipNext,
76+
PlaylistPlus,
4377
},
4478
setup(props) {
4579
const router = useRouter()
@@ -61,9 +95,19 @@
6195
router.push(`/videos/${props.video.id}`)
6296
}
6397
98+
const onPlayNext = () => {
99+
playback.addAsNext({ type: 'video', ...props.video })
100+
}
101+
102+
const onAddToQueue = () => {
103+
playback.addToQueue({ type: 'video', ...props.video })
104+
}
105+
64106
return {
65107
formatDuration,
66108
handleClick,
109+
onPlayNext,
110+
onAddToQueue,
67111
width: props.width,
68112
}
69113
},
@@ -78,6 +122,7 @@
78122
flex-direction: column;
79123
align-items: start;
80124
transition: background 0.15s;
125+
position: relative;
81126
82127
&,
83128
& * {
@@ -120,6 +165,19 @@
120165
font-size: 0.75rem;
121166
font-weight: 500;
122167
}
168+
169+
.actions-button {
170+
position: absolute;
171+
top: 0.5rem;
172+
right: 0.5rem;
173+
transition: opacity 0.3s ease-in-out;
174+
opacity: 0;
175+
z-index: 10;
176+
}
177+
}
178+
179+
&:hover .actions-button {
180+
opacity: 1;
123181
}
124182
125183
.metadata {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<template>
2+
<NcListItem
3+
:active="isActive"
4+
:name="video.title || 'Untitled'"
5+
@click.prevent="onPlay"
6+
:bold="false">
7+
<template #icon>
8+
<img
9+
v-if="video.thumbnail"
10+
:src="video.thumbnail"
11+
alt="Thumbnail"
12+
class="thumbnail"
13+
width="44"
14+
height="44" />
15+
<Filmstrip v-else :size="44" />
16+
</template>
17+
18+
<template #subname>
19+
<span v-if="video.year || video.genre">
20+
<span v-if="video.year">{{ video.year }}</span>
21+
<span v-if="video.year && video.genre"> • </span>
22+
<span v-if="video.genre">{{ video.genre }}</span>
23+
</span>
24+
<span v-else-if="video.duration">{{ formatDuration(video.duration) }}</span>
25+
</template>
26+
27+
<template #actions>
28+
<slot name="actions-start" />
29+
30+
<NcActionButton v-if="!disablePlay" @click.stop="onPlay">
31+
<template #icon>
32+
<Play :size="20" />
33+
</template>
34+
Play
35+
</NcActionButton>
36+
37+
<NcActionButton v-if="!disablePlayNext" @click.stop="onPlayNext">
38+
<template #icon>
39+
<SkipNext :size="20" />
40+
</template>
41+
Play Next
42+
</NcActionButton>
43+
44+
<NcActionButton v-if="!disableAddToQueue" @click.stop="onAddToQueue">
45+
<template #icon>
46+
<PlaylistPlus :size="20" />
47+
</template>
48+
Add to Queue
49+
</NcActionButton>
50+
51+
<slot name="actions-end" />
52+
</template>
53+
</NcListItem>
54+
</template>
55+
56+
<script lang="ts">
57+
import { defineComponent, computed, type PropType } from 'vue'
58+
import { type Video } from '@/models/media'
59+
import playback from '@/composables/usePlayback'
60+
61+
import NcListItem from '@nextcloud/vue/components/NcListItem'
62+
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
63+
64+
import Filmstrip from '@icons/Filmstrip.vue'
65+
import Play from '@icons/Play.vue'
66+
import SkipNext from '@icons/SkipNext.vue'
67+
import PlaylistPlus from '@icons/PlaylistPlus.vue'
68+
69+
export default defineComponent({
70+
name: 'VideoListItem',
71+
props: {
72+
video: {
73+
type: Object as PropType<Video>,
74+
required: true,
75+
},
76+
disablePlay: {
77+
type: Boolean,
78+
default: false,
79+
},
80+
disablePlayNext: {
81+
type: Boolean,
82+
default: false,
83+
},
84+
disableAddToQueue: {
85+
type: Boolean,
86+
default: false,
87+
},
88+
},
89+
components: {
90+
NcActionButton,
91+
NcListItem,
92+
Filmstrip,
93+
Play,
94+
SkipNext,
95+
PlaylistPlus,
96+
},
97+
emits: ['play'],
98+
setup(props, { emit }) {
99+
const { currentMedia, addToQueue, addAsNext } = playback
100+
101+
const isActive = computed(() =>
102+
props.video.id === currentMedia.value?.id && currentMedia.value?.type === 'video'
103+
)
104+
105+
const onPlay = () => emit('play', props.video)
106+
const onPlayNext = () => addAsNext({ type: 'video', ...props.video })
107+
const onAddToQueue = () => addToQueue({ type: 'video', ...props.video })
108+
109+
const formatDuration = (seconds: number): string => {
110+
const hours = Math.floor(seconds / 3600)
111+
const minutes = Math.floor((seconds % 3600) / 60)
112+
const secs = seconds % 60
113+
114+
if (hours > 0) {
115+
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
116+
}
117+
return `${minutes}:${secs.toString().padStart(2, '0')}`
118+
}
119+
120+
return {
121+
isActive,
122+
onPlay,
123+
onPlayNext,
124+
onAddToQueue,
125+
formatDuration,
126+
}
127+
},
128+
})
129+
</script>
130+
131+
<style scoped lang="scss">
132+
.thumbnail {
133+
border-radius: 4px;
134+
object-fit: cover;
135+
}
136+
</style>

0 commit comments

Comments
 (0)