feat(frontend): komponenty panelu
Widok rozbity na komponenty odpowiadające kafelkom pulpitu: - AppHeader, VoiceChannelPanel, PlayerPanel, AddTrackPanel - UploadsPanel wraz z UploadedFileRow współdzielonym z oknem modalnym - QueuePanel i QueueItem - przeciąganie, zmiana kolejności, limit odtworzeń - HistoryPanel, ModalShell, SettingsModal, UploadsLibraryModal, ToastHost Arkusz stylów przeniesiony ze static/style.css bez zmian w wyglądzie. Style, które app.js wstrzykiwał inline przy budowaniu HTML-a, przeniesione do klas współdzielonych i bloków scoped w komponentach. Poprawki bezpieczeństwa wynikające z przepisania: - Tytuły utworów i nazwy plików nie trafiają już do innerHTML. Poprzednia wersja składała wiersze kolejki, historii i listy plików ze stringów, więc tytuł zawierający znaczniki HTML był wstawiany do DOM bez escapowania. - Nazwy plików nie są już wklejane do atrybutów onclick z ręcznym escapowaniem apostrofu, które i tak nie obejmowało cudzysłowu. Zdarzenia podpina Vue, bez generowania kodu w stringach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
119bff00a3
commit
038fa2ead4
16 files changed
+1245
No files matched your search
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { LogIn, LogOut, Radio } from 'lucide-vue-next'
|
||||
import { api } from '@/api/client'
|
||||
import { useToasts } from '@/composables/useToasts'
|
||||
import type { BotStatus } from '@/types/status'
|
||||
|
||||
const props = defineProps<{ status: BotStatus | null }>()
|
||||
|
||||
const { notify, run } = useToasts()
|
||||
const selected = ref('')
|
||||
|
||||
const channels = computed(() => props.status?.voice_channels ?? [])
|
||||
const active = computed(() => props.status?.active_voice_channel ?? null)
|
||||
|
||||
// Wybór podąża za realnym połączeniem, a bez niego domyślnie wskazuje pierwszy kanał.
|
||||
watch(
|
||||
[active, channels],
|
||||
([activeChannel, available]) => {
|
||||
if (activeChannel) {
|
||||
selected.value = activeChannel.channel_id
|
||||
} else if (!available.some((channel) => channel.channel_id === selected.value)) {
|
||||
selected.value = available[0]?.channel_id ?? ''
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function toggle() {
|
||||
if (active.value) {
|
||||
await run(() => api.disconnect(), 'Rozłączono z kanału głosowego', 'Błąd rozłączania')
|
||||
return
|
||||
}
|
||||
if (!selected.value) {
|
||||
notify('Wybierz kanał głosowy z listy!', 'error')
|
||||
return
|
||||
}
|
||||
await run(() => api.connect(selected.value), 'Połączono z kanałem głosowym!', 'Nie udało się połączyć')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="glass-card connection-panel">
|
||||
<h2 class="card-title"><Radio /> Kanał Głosowy</h2>
|
||||
<div class="control-row">
|
||||
<select v-model="selected" class="premium-select" :disabled="!channels.length">
|
||||
<option v-if="!channels.length" value="">Brak dostępnych kanałów</option>
|
||||
<option v-for="channel in channels" :key="channel.channel_id" :value="channel.channel_id">
|
||||
{{ channel.guild_name }} - {{ channel.channel_name }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn" :class="active ? 'btn-danger' : 'btn-primary btn-glow'" @click="toggle">
|
||||
<component :is="active ? LogOut : LogIn" />
|
||||
{{ active ? 'Odłącz' : 'Połącz' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user