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:
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,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user