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:
lgooseandClaude Bot committed 2026-08-21 01:08:31 +02:00
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>