Add global mini-player and improve configuration
- Add global activeRoom store for persistent WebSocket connection - Add MiniPlayer component for playback controls across pages - Add chunked S3 streaming with 64KB chunks and Range support - Add queue item removal button - Move DB credentials to environment variables - Update .env.example with DB configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,19 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<Header />
|
||||
<main class="main-content">
|
||||
<main class="main-content" :class="{ 'has-mini-player': activeRoomStore.isInRoom }">
|
||||
<router-view />
|
||||
</main>
|
||||
<MiniPlayer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Header from './components/common/Header.vue'
|
||||
import MiniPlayer from './components/player/MiniPlayer.vue'
|
||||
import { useActiveRoomStore } from './stores/activeRoom'
|
||||
|
||||
const activeRoomStore = useActiveRoomStore()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -25,4 +30,8 @@ import Header from './components/common/Header.vue'
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-content.has-mini-player {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h3>Чат</h3>
|
||||
<div class="messages" ref="messagesRef">
|
||||
<ChatMessage
|
||||
v-for="msg in messages"
|
||||
v-for="msg in allMessages"
|
||||
:key="msg.id"
|
||||
:message="msg"
|
||||
/>
|
||||
@@ -13,7 +13,7 @@
|
||||
type="text"
|
||||
v-model="newMessage"
|
||||
placeholder="Написать сообщение..."
|
||||
:disabled="!ws.connected"
|
||||
:disabled="!activeRoomStore.connected"
|
||||
/>
|
||||
<button type="submit" class="btn-primary" :disabled="!newMessage.trim()">
|
||||
Отправить
|
||||
@@ -23,53 +23,43 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import api from '../../composables/useApi'
|
||||
import { useActiveRoomStore } from '../../stores/activeRoom'
|
||||
import ChatMessage from './ChatMessage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
roomId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
ws: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const messages = ref([])
|
||||
const activeRoomStore = useActiveRoomStore()
|
||||
|
||||
const historyMessages = ref([])
|
||||
const newMessage = ref('')
|
||||
const messagesRef = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await api.get(`/api/rooms/${props.roomId}/messages`)
|
||||
messages.value = response.data
|
||||
scrollToBottom()
|
||||
// Combine history + new messages
|
||||
const allMessages = computed(() => {
|
||||
return [...historyMessages.value, ...activeRoomStore.chatMessages]
|
||||
})
|
||||
|
||||
// Listen for new messages from WebSocket
|
||||
watch(() => props.ws, (wsObj) => {
|
||||
if (wsObj?.messages) {
|
||||
watch(wsObj.messages, (msgs) => {
|
||||
const lastMsg = msgs[msgs.length - 1]
|
||||
if (lastMsg?.type === 'chat_message') {
|
||||
messages.value.push({
|
||||
id: lastMsg.id,
|
||||
user_id: lastMsg.user_id,
|
||||
username: lastMsg.username,
|
||||
text: lastMsg.text,
|
||||
created_at: lastMsg.created_at
|
||||
})
|
||||
nextTick(scrollToBottom)
|
||||
}
|
||||
}, { deep: true })
|
||||
}
|
||||
}, { immediate: true })
|
||||
onMounted(async () => {
|
||||
const response = await api.get(`/api/rooms/${props.roomId}/messages`)
|
||||
historyMessages.value = response.data
|
||||
nextTick(scrollToBottom)
|
||||
})
|
||||
|
||||
// Auto-scroll when new messages arrive
|
||||
watch(() => activeRoomStore.chatMessages.length, () => {
|
||||
nextTick(scrollToBottom)
|
||||
})
|
||||
|
||||
function sendMessage() {
|
||||
if (!newMessage.value.trim()) return
|
||||
props.ws.sendChatMessage(newMessage.value)
|
||||
activeRoomStore.sendChatMessage(newMessage.value)
|
||||
newMessage.value = ''
|
||||
}
|
||||
|
||||
|
||||
211
frontend/src/components/player/MiniPlayer.vue
Normal file
211
frontend/src/components/player/MiniPlayer.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="mini-player" v-if="activeRoomStore.isInRoom">
|
||||
<div class="mini-player-info" @click="goToRoom">
|
||||
<div class="room-name">{{ activeRoomStore.roomName }}</div>
|
||||
<div class="track-info" v-if="currentTrack">
|
||||
{{ currentTrack.title }} - {{ currentTrack.artist }}
|
||||
</div>
|
||||
<div class="track-info" v-else>Нет трека</div>
|
||||
</div>
|
||||
|
||||
<div class="mini-player-controls">
|
||||
<button class="control-btn" @click="handlePrev">
|
||||
<span>⏮</span>
|
||||
</button>
|
||||
<button class="control-btn play-btn" @click="togglePlay">
|
||||
<span>{{ playerStore.isPlaying ? '⏸' : '▶' }}</span>
|
||||
</button>
|
||||
<button class="control-btn" @click="handleNext">
|
||||
<span>⏭</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mini-player-progress">
|
||||
<div class="progress-bar" @click="handleSeek">
|
||||
<div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
|
||||
</div>
|
||||
<span class="time">{{ formatTime(playerStore.position) }} / {{ formatTime(playerStore.duration) }}</span>
|
||||
</div>
|
||||
|
||||
<button class="leave-btn" @click="handleLeave" title="Выйти из комнаты">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useActiveRoomStore } from '../../stores/activeRoom'
|
||||
import { usePlayerStore } from '../../stores/player'
|
||||
import { useTracksStore } from '../../stores/tracks'
|
||||
|
||||
const router = useRouter()
|
||||
const activeRoomStore = useActiveRoomStore()
|
||||
const playerStore = usePlayerStore()
|
||||
const tracksStore = useTracksStore()
|
||||
|
||||
const currentTrack = computed(() => {
|
||||
if (!playerStore.currentTrack?.id) return null
|
||||
return tracksStore.tracks.find(t => t.id === playerStore.currentTrack.id)
|
||||
})
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!playerStore.duration) return 0
|
||||
return (playerStore.position / playerStore.duration) * 100
|
||||
})
|
||||
|
||||
function formatTime(ms) {
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
if (playerStore.isPlaying) {
|
||||
activeRoomStore.sendPlayerAction('pause', playerStore.position)
|
||||
} else {
|
||||
activeRoomStore.sendPlayerAction('play', playerStore.position)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
activeRoomStore.sendPlayerAction('prev')
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
activeRoomStore.sendPlayerAction('next')
|
||||
}
|
||||
|
||||
function handleSeek(e) {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const percent = (e.clientX - rect.left) / rect.width
|
||||
const position = Math.floor(percent * playerStore.duration)
|
||||
activeRoomStore.sendPlayerAction('seek', position)
|
||||
}
|
||||
|
||||
function goToRoom() {
|
||||
router.push(`/room/${activeRoomStore.roomId}`)
|
||||
}
|
||||
|
||||
async function handleLeave() {
|
||||
await activeRoomStore.leaveRoom()
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mini-player {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #1a1a2e;
|
||||
border-top: 1px solid #333;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.mini-player-info {
|
||||
flex: 0 0 200px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-size: 12px;
|
||||
color: #7c3aed;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.track-info {
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mini-player-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
background: #7c3aed;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-btn:hover {
|
||||
background: #6d28d9;
|
||||
}
|
||||
|
||||
.mini-player-progress {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: #333;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #7c3aed;
|
||||
border-radius: 3px;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
min-width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.leave-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
font-size: 18px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.leave-btn:hover {
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -7,14 +7,16 @@
|
||||
v-for="(track, index) in queue"
|
||||
:key="track.id"
|
||||
class="queue-item"
|
||||
@click="$emit('play-track', track)"
|
||||
>
|
||||
<span class="queue-index">{{ index + 1 }}</span>
|
||||
<div class="queue-track-info">
|
||||
<div class="queue-track-info" @click="$emit('play-track', track)">
|
||||
<span class="queue-track-title">{{ track.title }}</span>
|
||||
<span class="queue-track-artist">{{ track.artist }}</span>
|
||||
</div>
|
||||
<span class="queue-duration">{{ formatDuration(track.duration) }}</span>
|
||||
<button class="btn-remove" @click.stop="$emit('remove-track', track)" title="Удалить из очереди">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -27,7 +29,7 @@ defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['play-track'])
|
||||
defineEmits(['play-track', 'remove-track'])
|
||||
|
||||
function formatDuration(ms) {
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
@@ -95,4 +97,20 @@ function formatDuration(ms) {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -69,7 +69,13 @@ export function usePlayer(onTrackEnded = null) {
|
||||
initAudio()
|
||||
}
|
||||
|
||||
if (state.track_url && state.track_url !== playerStore.currentTrackUrl) {
|
||||
// Load track if URL changed OR if audio has no source (e.g. after returning to room)
|
||||
const needsLoad = state.track_url && (
|
||||
state.track_url !== playerStore.currentTrackUrl ||
|
||||
!audio.value.src
|
||||
)
|
||||
|
||||
if (needsLoad) {
|
||||
loadTrack(state.track_url)
|
||||
playerStore.currentTrackUrl = state.track_url
|
||||
}
|
||||
|
||||
223
frontend/src/stores/activeRoom.js
Normal file
223
frontend/src/stores/activeRoom.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAuthStore } from './auth'
|
||||
import { usePlayerStore } from './player'
|
||||
import { useRoomStore } from './room'
|
||||
import api from '../composables/useApi'
|
||||
|
||||
export const useActiveRoomStore = defineStore('activeRoom', () => {
|
||||
const ws = ref(null)
|
||||
const connected = ref(false)
|
||||
const roomId = ref(null)
|
||||
const roomName = ref(null)
|
||||
const chatMessages = ref([])
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const playerStore = usePlayerStore()
|
||||
const roomStore = useRoomStore()
|
||||
|
||||
// Audio element
|
||||
let audio = null
|
||||
let onTrackEndedCallback = null
|
||||
|
||||
const isInRoom = computed(() => roomId.value !== null)
|
||||
|
||||
function initAudio() {
|
||||
if (audio) return
|
||||
|
||||
audio = new Audio()
|
||||
audio.volume = playerStore.volume / 100
|
||||
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
playerStore.setPosition(Math.floor(audio.currentTime * 1000))
|
||||
})
|
||||
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
playerStore.setDuration(Math.floor(audio.duration * 1000))
|
||||
})
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
if (onTrackEndedCallback) {
|
||||
onTrackEndedCallback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function connect(id, name) {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
if (roomId.value === id) return
|
||||
disconnect()
|
||||
}
|
||||
|
||||
roomId.value = id
|
||||
roomName.value = name
|
||||
|
||||
const wsUrl = import.meta.env.VITE_WS_URL || window.location.origin.replace('http', 'ws')
|
||||
ws.value = new WebSocket(`${wsUrl}/ws/rooms/${id}?token=${authStore.token}`)
|
||||
|
||||
ws.value.onopen = () => {
|
||||
connected.value = true
|
||||
send({ type: 'sync_request' })
|
||||
}
|
||||
|
||||
ws.value.onclose = () => {
|
||||
connected.value = false
|
||||
}
|
||||
|
||||
ws.value.onerror = () => {}
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
handleMessage(data)
|
||||
}
|
||||
|
||||
// Set callback for track ended
|
||||
onTrackEndedCallback = () => {
|
||||
sendPlayerAction('next')
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (ws.value) {
|
||||
ws.value.close()
|
||||
ws.value = null
|
||||
}
|
||||
connected.value = false
|
||||
roomId.value = null
|
||||
roomName.value = null
|
||||
chatMessages.value = []
|
||||
playerStore.reset()
|
||||
if (audio) {
|
||||
audio.pause()
|
||||
audio.src = ''
|
||||
}
|
||||
}
|
||||
|
||||
function send(data) {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
ws.value.send(JSON.stringify(data))
|
||||
}
|
||||
}
|
||||
|
||||
function sendPlayerAction(action, position = null, trackId = null) {
|
||||
send({
|
||||
type: 'player_action',
|
||||
action,
|
||||
position,
|
||||
track_id: trackId,
|
||||
})
|
||||
}
|
||||
|
||||
function sendChatMessage(text) {
|
||||
send({
|
||||
type: 'chat_message',
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
||||
function handleMessage(msg) {
|
||||
switch (msg.type) {
|
||||
case 'player_state':
|
||||
case 'sync_state':
|
||||
syncToState(msg)
|
||||
playerStore.setPlayerState(msg)
|
||||
break
|
||||
case 'user_joined':
|
||||
roomStore.addParticipant(msg.user)
|
||||
break
|
||||
case 'user_left':
|
||||
roomStore.removeParticipant(msg.user_id)
|
||||
break
|
||||
case 'queue_updated':
|
||||
if (roomId.value) {
|
||||
roomStore.fetchQueue(roomId.value)
|
||||
}
|
||||
break
|
||||
case 'chat_message':
|
||||
chatMessages.value.push({
|
||||
id: msg.id,
|
||||
user_id: msg.user_id,
|
||||
username: msg.username,
|
||||
text: msg.text,
|
||||
created_at: msg.created_at
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function syncToState(state) {
|
||||
if (!audio) {
|
||||
initAudio()
|
||||
}
|
||||
|
||||
const needsLoad = state.track_url && (
|
||||
state.track_url !== playerStore.currentTrackUrl ||
|
||||
!audio.src
|
||||
)
|
||||
|
||||
if (needsLoad) {
|
||||
const apiUrl = import.meta.env.VITE_API_URL || ''
|
||||
const fullUrl = state.track_url.startsWith('/') ? `${apiUrl}${state.track_url}` : state.track_url
|
||||
audio.src = fullUrl
|
||||
audio.load()
|
||||
playerStore.currentTrackUrl = state.track_url
|
||||
}
|
||||
|
||||
if (state.position !== undefined) {
|
||||
const diff = Math.abs(state.position - playerStore.position)
|
||||
if (diff > 2000) {
|
||||
audio.currentTime = state.position / 1000
|
||||
}
|
||||
}
|
||||
|
||||
if (state.is_playing) {
|
||||
audio.play().catch(() => {})
|
||||
} else {
|
||||
audio.pause()
|
||||
}
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (audio) {
|
||||
audio.play().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (audio) {
|
||||
audio.pause()
|
||||
}
|
||||
}
|
||||
|
||||
function setVolume(vol) {
|
||||
if (audio) {
|
||||
audio.volume = vol / 100
|
||||
}
|
||||
playerStore.setVolume(vol)
|
||||
}
|
||||
|
||||
async function leaveRoom() {
|
||||
if (roomId.value) {
|
||||
await api.post(`/api/rooms/${roomId.value}/leave`)
|
||||
}
|
||||
disconnect()
|
||||
}
|
||||
|
||||
return {
|
||||
ws,
|
||||
connected,
|
||||
roomId,
|
||||
roomName,
|
||||
chatMessages,
|
||||
isInRoom,
|
||||
connect,
|
||||
disconnect,
|
||||
send,
|
||||
sendPlayerAction,
|
||||
sendChatMessage,
|
||||
leaveRoom,
|
||||
play,
|
||||
pause,
|
||||
setVolume,
|
||||
}
|
||||
})
|
||||
@@ -47,6 +47,14 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
isPlaying.value = false
|
||||
currentTrack.value = null
|
||||
currentTrackUrl.value = null
|
||||
position.value = 0
|
||||
duration.value = 0
|
||||
}
|
||||
|
||||
// Load saved volume
|
||||
const savedVolume = localStorage.getItem('volume')
|
||||
if (savedVolume) {
|
||||
@@ -67,5 +75,6 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
setVolume,
|
||||
play,
|
||||
pause,
|
||||
reset,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,28 +2,22 @@
|
||||
<div class="room-page" v-if="room">
|
||||
<div class="room-header">
|
||||
<h1>{{ room.name }}</h1>
|
||||
<button class="btn-secondary" @click="leaveAndGoHome">Выйти из комнаты</button>
|
||||
</div>
|
||||
|
||||
<div class="room-layout">
|
||||
<div class="main-section">
|
||||
<AudioPlayer
|
||||
:ws="websocket"
|
||||
@player-action="handlePlayerAction"
|
||||
/>
|
||||
|
||||
<div class="queue-section card">
|
||||
<div class="queue-header">
|
||||
<h3>Очередь</h3>
|
||||
<button class="btn-secondary" @click="showAddTrack = true">Добавить</button>
|
||||
</div>
|
||||
<Queue :queue="roomStore.queue" @play-track="playTrack" />
|
||||
<Queue :queue="roomStore.queue" @play-track="playTrack" @remove-track="removeFromQueue" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-section">
|
||||
<ParticipantsList :participants="roomStore.participants" />
|
||||
<ChatWindow :room-id="roomId" :ws="websocket" />
|
||||
<ChatWindow :room-id="roomId" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,14 +33,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoomStore } from '../stores/room'
|
||||
import { useTracksStore } from '../stores/tracks'
|
||||
import { usePlayerStore } from '../stores/player'
|
||||
import { useWebSocket } from '../composables/useWebSocket'
|
||||
import { usePlayer } from '../composables/usePlayer'
|
||||
import AudioPlayer from '../components/player/AudioPlayer.vue'
|
||||
import { useActiveRoomStore } from '../stores/activeRoom'
|
||||
import Queue from '../components/room/Queue.vue'
|
||||
import ParticipantsList from '../components/room/ParticipantsList.vue'
|
||||
import ChatWindow from '../components/chat/ChatWindow.vue'
|
||||
@@ -54,45 +45,14 @@ import TrackList from '../components/tracks/TrackList.vue'
|
||||
import Modal from '../components/common/Modal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const roomStore = useRoomStore()
|
||||
const tracksStore = useTracksStore()
|
||||
const playerStore = usePlayerStore()
|
||||
const activeRoomStore = useActiveRoomStore()
|
||||
|
||||
const roomId = route.params.id
|
||||
const room = ref(null)
|
||||
const showAddTrack = ref(false)
|
||||
|
||||
const { syncToState, setOnTrackEnded } = usePlayer()
|
||||
|
||||
function handleTrackEnded() {
|
||||
sendPlayerAction('next')
|
||||
}
|
||||
|
||||
function handleWsMessage(msg) {
|
||||
switch (msg.type) {
|
||||
case 'player_state':
|
||||
case 'sync_state':
|
||||
// Call syncToState BEFORE updating store so it can detect URL changes
|
||||
syncToState(msg)
|
||||
playerStore.setPlayerState(msg)
|
||||
break
|
||||
case 'user_joined':
|
||||
roomStore.addParticipant(msg.user)
|
||||
break
|
||||
case 'user_left':
|
||||
roomStore.removeParticipant(msg.user_id)
|
||||
break
|
||||
case 'queue_updated':
|
||||
roomStore.fetchQueue(roomId)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const { connect, disconnect, sendPlayerAction, connected } = useWebSocket(roomId, handleWsMessage)
|
||||
|
||||
const websocket = { sendPlayerAction, connected }
|
||||
|
||||
onMounted(async () => {
|
||||
await roomStore.fetchRoom(roomId)
|
||||
room.value = roomStore.currentRoom
|
||||
@@ -101,22 +61,12 @@ onMounted(async () => {
|
||||
await roomStore.fetchQueue(roomId)
|
||||
await tracksStore.fetchTracks()
|
||||
|
||||
// Set callback for when track ends
|
||||
setOnTrackEnded(handleTrackEnded)
|
||||
|
||||
connect()
|
||||
// Connect to room via global store
|
||||
activeRoomStore.connect(roomId, room.value.name)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnect()
|
||||
})
|
||||
|
||||
function handlePlayerAction(action, position) {
|
||||
sendPlayerAction(action, position)
|
||||
}
|
||||
|
||||
function playTrack(track) {
|
||||
sendPlayerAction('set_track', null, track.id)
|
||||
activeRoomStore.sendPlayerAction('set_track', null, track.id)
|
||||
}
|
||||
|
||||
async function addTrackToQueue(track) {
|
||||
@@ -124,9 +74,8 @@ async function addTrackToQueue(track) {
|
||||
showAddTrack.value = false
|
||||
}
|
||||
|
||||
async function leaveAndGoHome() {
|
||||
await roomStore.leaveRoom(roomId)
|
||||
router.push('/')
|
||||
async function removeFromQueue(track) {
|
||||
await roomStore.removeFromQueue(roomId, track.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user