2025-12-12 13:30:09 +03:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="room-page" v-if="room">
|
|
|
|
|
|
<div class="room-header">
|
|
|
|
|
|
<h1>{{ room.name }}</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="room-layout">
|
|
|
|
|
|
<div class="main-section">
|
|
|
|
|
|
<div class="queue-section card">
|
|
|
|
|
|
<div class="queue-header">
|
|
|
|
|
|
<h3>Очередь</h3>
|
|
|
|
|
|
<button class="btn-secondary" @click="showAddTrack = true">Добавить</button>
|
|
|
|
|
|
</div>
|
2025-12-12 15:02:02 +03:00
|
|
|
|
<Queue :queue="roomStore.queue" @play-track="playTrack" @remove-track="removeFromQueue" />
|
2025-12-12 13:30:09 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="side-section">
|
|
|
|
|
|
<ParticipantsList :participants="roomStore.participants" />
|
2025-12-12 15:02:02 +03:00
|
|
|
|
<ChatWindow :room-id="roomId" />
|
2025-12-12 13:30:09 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-19 19:22:35 +03:00
|
|
|
|
<Modal v-if="showAddTrack" title="Добавить в очередь" @close="closeAddTrackModal">
|
|
|
|
|
|
<div class="filters-section">
|
|
|
|
|
|
<div class="search-filters">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="searchTitle"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="Поиск по названию..."
|
|
|
|
|
|
class="search-input"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="searchArtist"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="Поиск по артисту..."
|
|
|
|
|
|
class="search-input"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="checkbox-filters">
|
|
|
|
|
|
<label class="checkbox-label">
|
|
|
|
|
|
<input type="checkbox" v-model="filterMyTracks" />
|
|
|
|
|
|
<span>Мои треки</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="checkbox-label">
|
|
|
|
|
|
<input type="checkbox" v-model="filterNotInQueue" />
|
|
|
|
|
|
<span>Не добавленные в комнату</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="add-track-controls">
|
|
|
|
|
|
<div class="selection-info">
|
|
|
|
|
|
<span>Найдено: {{ filteredTracks.length }}</span>
|
|
|
|
|
|
<span v-if="selectedTracks.length > 0" class="selected-count">
|
|
|
|
|
|
Выбрано: {{ selectedTracks.length }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="selectedTracks.length > 0"
|
|
|
|
|
|
class="btn-text"
|
|
|
|
|
|
@click="clearSelection"
|
|
|
|
|
|
>
|
|
|
|
|
|
Очистить
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="btn-primary"
|
|
|
|
|
|
:disabled="selectedTracks.length === 0"
|
|
|
|
|
|
@click="addSelectedTracks"
|
|
|
|
|
|
>
|
|
|
|
|
|
Добавить выбранные ({{ selectedTracks.length }})
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p v-if="addTrackError" class="error-message">{{ addTrackError }}</p>
|
|
|
|
|
|
<p v-if="addTrackSuccess" class="success-message">{{ addTrackSuccess }}</p>
|
2025-12-12 13:30:09 +03:00
|
|
|
|
<TrackList
|
2025-12-19 19:22:35 +03:00
|
|
|
|
:tracks="filteredTracks"
|
|
|
|
|
|
:queue-track-ids="queueTrackIds"
|
|
|
|
|
|
:selected-track-ids="selectedTracks"
|
|
|
|
|
|
multi-select
|
|
|
|
|
|
@toggle-select="toggleTrackSelection"
|
2025-12-12 13:30:09 +03:00
|
|
|
|
/>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="loading">Загрузка...</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-12-19 19:22:35 +03:00
|
|
|
|
import { ref, computed, onMounted } from 'vue'
|
2025-12-12 15:02:02 +03:00
|
|
|
|
import { useRoute } from 'vue-router'
|
2025-12-12 13:30:09 +03:00
|
|
|
|
import { useRoomStore } from '../stores/room'
|
|
|
|
|
|
import { useTracksStore } from '../stores/tracks'
|
2025-12-12 15:02:02 +03:00
|
|
|
|
import { useActiveRoomStore } from '../stores/activeRoom'
|
2025-12-19 19:22:35 +03:00
|
|
|
|
import { useAuthStore } from '../stores/auth'
|
2025-12-12 13:30:09 +03:00
|
|
|
|
import Queue from '../components/room/Queue.vue'
|
|
|
|
|
|
import ParticipantsList from '../components/room/ParticipantsList.vue'
|
|
|
|
|
|
import ChatWindow from '../components/chat/ChatWindow.vue'
|
|
|
|
|
|
import TrackList from '../components/tracks/TrackList.vue'
|
|
|
|
|
|
import Modal from '../components/common/Modal.vue'
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const roomStore = useRoomStore()
|
|
|
|
|
|
const tracksStore = useTracksStore()
|
2025-12-12 15:02:02 +03:00
|
|
|
|
const activeRoomStore = useActiveRoomStore()
|
2025-12-19 19:22:35 +03:00
|
|
|
|
const authStore = useAuthStore()
|
2025-12-12 13:30:09 +03:00
|
|
|
|
|
|
|
|
|
|
const roomId = route.params.id
|
|
|
|
|
|
const room = ref(null)
|
|
|
|
|
|
const showAddTrack = ref(false)
|
2025-12-19 19:22:35 +03:00
|
|
|
|
const addTrackError = ref('')
|
|
|
|
|
|
const addTrackSuccess = ref('')
|
|
|
|
|
|
const selectedTracks = ref([])
|
|
|
|
|
|
|
|
|
|
|
|
// Filters
|
|
|
|
|
|
const searchTitle = ref('')
|
|
|
|
|
|
const searchArtist = ref('')
|
|
|
|
|
|
const filterMyTracks = ref(false)
|
|
|
|
|
|
const filterNotInQueue = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const queueTrackIds = computed(() => {
|
|
|
|
|
|
return roomStore.queue.map(track => track.id)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const filteredTracks = computed(() => {
|
|
|
|
|
|
let tracks = tracksStore.tracks
|
|
|
|
|
|
|
|
|
|
|
|
// Filter by title
|
|
|
|
|
|
if (searchTitle.value.trim()) {
|
|
|
|
|
|
const searchLower = searchTitle.value.toLowerCase()
|
|
|
|
|
|
tracks = tracks.filter(track =>
|
|
|
|
|
|
track.title.toLowerCase().includes(searchLower)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Filter by artist
|
|
|
|
|
|
if (searchArtist.value.trim()) {
|
|
|
|
|
|
const searchLower = searchArtist.value.toLowerCase()
|
|
|
|
|
|
tracks = tracks.filter(track =>
|
|
|
|
|
|
track.artist.toLowerCase().includes(searchLower)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Filter my tracks
|
|
|
|
|
|
if (filterMyTracks.value) {
|
|
|
|
|
|
const currentUserId = authStore.user?.id
|
|
|
|
|
|
tracks = tracks.filter(track => track.uploaded_by === currentUserId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Filter not in queue
|
|
|
|
|
|
if (filterNotInQueue.value) {
|
|
|
|
|
|
tracks = tracks.filter(track => !queueTrackIds.value.includes(track.id))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return tracks
|
|
|
|
|
|
})
|
2025-12-12 13:30:09 +03:00
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await roomStore.fetchRoom(roomId)
|
|
|
|
|
|
room.value = roomStore.currentRoom
|
|
|
|
|
|
|
|
|
|
|
|
await roomStore.joinRoom(roomId)
|
|
|
|
|
|
await roomStore.fetchQueue(roomId)
|
|
|
|
|
|
await tracksStore.fetchTracks()
|
|
|
|
|
|
|
2025-12-12 15:02:02 +03:00
|
|
|
|
// Connect to room via global store
|
|
|
|
|
|
activeRoomStore.connect(roomId, room.value.name)
|
2025-12-12 13:30:09 +03:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function playTrack(track) {
|
2025-12-12 15:02:02 +03:00
|
|
|
|
activeRoomStore.sendPlayerAction('set_track', null, track.id)
|
2025-12-12 13:30:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 19:22:35 +03:00
|
|
|
|
function toggleTrackSelection(trackId) {
|
|
|
|
|
|
const index = selectedTracks.value.indexOf(trackId)
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
|
selectedTracks.value.push(trackId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectedTracks.value.splice(index, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearSelection() {
|
|
|
|
|
|
selectedTracks.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeAddTrackModal() {
|
2025-12-12 13:30:09 +03:00
|
|
|
|
showAddTrack.value = false
|
2025-12-19 19:22:35 +03:00
|
|
|
|
clearSelection()
|
|
|
|
|
|
addTrackError.value = ''
|
|
|
|
|
|
addTrackSuccess.value = ''
|
|
|
|
|
|
// Reset filters
|
|
|
|
|
|
searchTitle.value = ''
|
|
|
|
|
|
searchArtist.value = ''
|
|
|
|
|
|
filterMyTracks.value = false
|
|
|
|
|
|
filterNotInQueue.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function addSelectedTracks() {
|
|
|
|
|
|
if (selectedTracks.value.length === 0) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
addTrackError.value = ''
|
|
|
|
|
|
addTrackSuccess.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
const result = await roomStore.addMultipleToQueue(roomId, selectedTracks.value)
|
|
|
|
|
|
|
|
|
|
|
|
if (result.skipped > 0) {
|
|
|
|
|
|
addTrackSuccess.value = `Добавлено: ${result.added}, пропущено (уже в очереди): ${result.skipped}`
|
|
|
|
|
|
} else {
|
|
|
|
|
|
addTrackSuccess.value = `Добавлено треков: ${result.added}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearSelection()
|
|
|
|
|
|
|
|
|
|
|
|
// Close modal after a short delay
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
closeAddTrackModal()
|
|
|
|
|
|
}, 1500)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (e.response?.data?.detail === 'All tracks already in queue') {
|
|
|
|
|
|
addTrackError.value = 'Все выбранные треки уже в очереди'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
addTrackError.value = e.response?.data?.detail || 'Ошибка добавления треков'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-12 13:30:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 15:02:02 +03:00
|
|
|
|
async function removeFromQueue(track) {
|
|
|
|
|
|
await roomStore.removeFromQueue(roomId, track.id)
|
2025-12-12 13:30:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.room-page {
|
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.room-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.room-header h1 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.room-layout {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 350px;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.main-section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.side-section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.queue-section {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.queue-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.queue-header h3 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 19:22:35 +03:00
|
|
|
|
.filters-section {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: #1a1a1a;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-filters {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-input {
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
background: #252525;
|
|
|
|
|
|
border: 1px solid #333;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
transition: border-color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-input:focus {
|
|
|
|
|
|
border-color: var(--color-primary, #1db954);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-input::placeholder {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkbox-filters {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkbox-label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: #ccc;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkbox-label input[type="checkbox"] {
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
accent-color: var(--color-primary, #1db954);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkbox-label:hover {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-track-controls {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding-bottom: 12px;
|
|
|
|
|
|
border-bottom: 1px solid #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selection-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selected-count {
|
|
|
|
|
|
color: var(--color-primary, #1db954);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-text {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-primary, #1db954);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-text:hover {
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.success-message {
|
|
|
|
|
|
color: #4caf50;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 13:30:09 +03:00
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.room-layout {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|