Add admin UI for managing radio fallback music
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m44s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m44s
Upload, list, and delete fallback music files from the admin page. Backend handlers validate file type/size and prevent path traversal. Nginx max body size increased to 50M to support large audio files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import CreateFavorite from "./CreateFavorite.vue";
|
||||
import CreateActivity from "./CreateActivity.vue";
|
||||
import CreateRowing from "./CreateRowing.vue";
|
||||
import ManageUsers from "./ManageUsers.vue";
|
||||
import ManageRadio from "./ManageRadio.vue";
|
||||
|
||||
const auth = useAuthStore();
|
||||
</script>
|
||||
@@ -23,6 +24,7 @@ const auth = useAuthStore();
|
||||
<CreateActivity class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
|
||||
<CreateRowing class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
|
||||
<ManageUsers class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
|
||||
<ManageRadio class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
85
vue/src/views/admin/ManageRadio.vue
Normal file
85
vue/src/views/admin/ManageRadio.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup>
|
||||
import Button from "@/components/input/Button.vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import axios from "axios";
|
||||
|
||||
const songs = ref([]);
|
||||
const files = ref([]);
|
||||
const results = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
async function fetchSongs() {
|
||||
try {
|
||||
const res = await axios.get("/api/radio/songs");
|
||||
songs.value = res.data.songs;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function onFileChange(e) {
|
||||
files.value = Array.from(e.target.files);
|
||||
results.value = [];
|
||||
}
|
||||
|
||||
async function upload() {
|
||||
if (!files.value.length) return;
|
||||
loading.value = true;
|
||||
results.value = files.value.map((f) => ({ name: f.name, status: "Uploading..." }));
|
||||
|
||||
await Promise.all(
|
||||
files.value.map(async (file, i) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
try {
|
||||
await axios.post("/api/radio/upload", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
results.value[i].status = "Uploaded";
|
||||
results.value[i].ok = true;
|
||||
} catch (err) {
|
||||
results.value[i].status = err.response?.data?.error || "Upload failed";
|
||||
results.value[i].ok = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
files.value = [];
|
||||
loading.value = false;
|
||||
await fetchSongs();
|
||||
}
|
||||
|
||||
async function deleteSong(name) {
|
||||
try {
|
||||
await axios.delete(`/api/radio/songs/${encodeURIComponent(name)}`);
|
||||
await fetchSongs();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
||||
}
|
||||
|
||||
onMounted(fetchSongs);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1>Manage Radio</h1>
|
||||
<input type="file" accept=".mp3,.ogg,.flac,.wav,.m4a,.opus" multiple @change="onFileChange" />
|
||||
<Button @click="upload" :disabled="loading">Upload</Button>
|
||||
<div v-for="r in results" :key="r.name">
|
||||
<span class="text-primary">{{ r.name }}: </span>
|
||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{ r.status }}</span>
|
||||
</div>
|
||||
<div v-for="song in songs" :key="song.name" class="flex flex-row items-center gap-2">
|
||||
<span>{{ song.name }}</span>
|
||||
<span class="text-secondary text-sm">{{ formatSize(song.size) }}</span>
|
||||
<Button @click="deleteSong(song.name)">Delete</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user