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

+94
View File
@@ -0,0 +1,94 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { FolderOpen, FolderUp, UploadCloud } from 'lucide-vue-next'
import UploadedFileRow from '@/components/UploadedFileRow.vue'
import { DASHBOARD_FILE_LIMIT, useUploads } from '@/composables/useUploads'
import { useToasts } from '@/composables/useToasts'
import { filesLabel } from '@/utils/plural'
import type { BotStatus } from '@/types/status'
const props = defineProps<{ status: BotStatus | null }>()
defineEmits<{ openLibrary: [] }>()
const { files, uploading, uploadLabel, upload, remove, enqueue } = useUploads()
const { notify } = useToasts()
const fileInput = ref<HTMLInputElement | null>(null)
const recent = computed(() => files.value.slice(0, DASHBOARD_FILE_LIMIT))
async function onFilesPicked(event: Event) {
const input = event.target as HTMLInputElement
if (input.files?.length) await upload(input.files)
input.value = ''
}
function play(filename: string) {
if (!props.status?.active_voice_channel) {
notify('Bot musi być połączony z kanałem głosowym!', 'error')
return
}
enqueue(filename)
}
</script>
<template>
<div class="glass-card uploads-panel">
<div class="queue-header">
<h2 class="card-title"><FolderUp /> Serwerowa baza plików</h2>
<span class="queue-count">{{ filesLabel(files.length) }}</span>
</div>
<div class="upload-area">
<input ref="fileInput" type="file" hidden multiple @change="onFilesPicked" />
<button class="btn btn-secondary btn-block" :disabled="uploading" @click="fileInput?.click()">
<UploadCloud /> Prześlij plik wideo/audio na serwer
</button>
<div v-if="uploading" class="upload-progress">
<div class="progress-bar-bg">
<div class="progress-bar-fill indeterminate"></div>
</div>
<span class="time-text">{{ uploadLabel }}</span>
</div>
</div>
<div class="queue-list-container list-short">
<ul class="queue-list">
<UploadedFileRow
v-for="file in recent"
:key="file.filename"
:file="file"
@enqueue="play"
@remove="remove"
/>
<div v-if="!files.length" class="empty-queue-message">
<FolderOpen />
<p>Brak przesłanych plików na serwerze.</p>
</div>
</ul>
</div>
<button class="btn btn-secondary btn-block" @click="$emit('openLibrary')">
<FolderOpen /> Pokaż wszystkie pliki na serwerze ({{ files.length }})
</button>
</div>
</template>
<style scoped>
.upload-area {
margin-bottom: 15px;
}
.upload-progress {
margin-top: 10px;
text-align: center;
}
.upload-progress .time-text {
display: block;
margin-top: 4px;
}
.btn-block:last-of-type {
margin-top: 10px;
}
</style>