You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
11 KiB
245 lines
11 KiB
{% extends "layout.html" %}
|
|
{% block main %}
|
|
|
|
<div class="bg-slate-900 min-h-screen py-8">
|
|
<div class="container mx-auto px-4">
|
|
<!-- Header & Actions -->
|
|
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-8">
|
|
<h1 class="text-2xl font-bold text-white">Background Manager</h1>
|
|
<div class="flex w-full md:w-auto gap-2">
|
|
<div class="relative flex-grow md:w-64">
|
|
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"></i>
|
|
<input type="text"
|
|
class="searchFilter input input-bordered w-full pl-10 bg-slate-800 border-slate-700 text-slate-200 focus:border-indigo-500"
|
|
placeholder="Search..."
|
|
onkeyup="searchFilter()">
|
|
</div>
|
|
<button onclick="add_modal.showModal()" class="btn btn-indigo bg-indigo-600 hover:bg-indigo-500 border-none text-white">
|
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
<span class="hidden sm:inline">Add Video</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Background Grid -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" id="backgrounds">
|
|
<!-- Backgrounds will be injected here -->
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div id="empty-state" class="hidden flex flex-col items-center justify-center py-20 text-slate-500">
|
|
<i data-lucide="film" class="w-16 h-16 mb-4 opacity-20"></i>
|
|
<p class="text-lg">No backgrounds found</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Background Modal -->
|
|
<dialog id="delete_modal" class="modal modal-bottom sm:modal-middle">
|
|
<div class="modal-box bg-slate-800 border border-white/10">
|
|
<h3 class="font-bold text-lg text-white">Delete Background</h3>
|
|
<p class="py-4 text-slate-400">Are you sure you want to delete this background video? This action cannot be undone.</p>
|
|
<div class="modal-action">
|
|
<form action="background/delete" method="post" class="flex gap-2">
|
|
<input type="hidden" id="background-key" name="background-key" value="">
|
|
<button type="button" onclick="delete_modal.close()" class="btn btn-ghost text-slate-400">Cancel</button>
|
|
<button type="submit" class="btn btn-error">Delete</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
|
|
<!-- Add Background Modal -->
|
|
<dialog id="add_modal" class="modal modal-bottom sm:modal-middle">
|
|
<div class="modal-box bg-slate-800 border border-white/10 max-w-lg">
|
|
<h3 class="font-bold text-lg text-white mb-6">Add Background Video</h3>
|
|
<form id="addBgForm" action="background/add" method="post" novalidate class="space-y-4">
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-slate-300">YouTube URI</span></label>
|
|
<div class="join w-full">
|
|
<div class="btn join-item no-animation bg-slate-900 border-slate-700 pointer-events-none">
|
|
<i data-lucide="youtube" class="w-4 h-4 text-red-500"></i>
|
|
</div>
|
|
<input name="youtube_uri" type="text" placeholder="https://www.youtube.com/watch?v=..."
|
|
class="input input-bordered join-item w-full bg-slate-900 border-slate-700 text-slate-200 focus:border-indigo-500">
|
|
</div>
|
|
<label class="label h-6"><span id="feedbackYT" class="label-text-alt text-error hidden"></span></label>
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-slate-300">Filename</span></label>
|
|
<div class="join w-full">
|
|
<div class="btn join-item no-animation bg-slate-900 border-slate-700 pointer-events-none">
|
|
<i data-lucide="file-video" class="w-4 h-4 text-indigo-500"></i>
|
|
</div>
|
|
<input name="filename" type="text" placeholder="e.g. minecraft-parkour"
|
|
class="input input-bordered join-item w-full bg-slate-900 border-slate-700 text-slate-200 focus:border-indigo-500">
|
|
</div>
|
|
<label class="label h-6"><span id="feedbackFilename" class="label-text-alt text-error hidden"></span></label>
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-slate-300">Credits</span></label>
|
|
<div class="join w-full">
|
|
<div class="btn join-item no-animation bg-slate-900 border-slate-700 pointer-events-none">
|
|
<i data-lucide="user" class="w-4 h-4 text-slate-400"></i>
|
|
</div>
|
|
<input name="citation" type="text" placeholder="YouTube Channel Name"
|
|
class="input input-bordered join-item w-full bg-slate-900 border-slate-700 text-slate-200 focus:border-indigo-500">
|
|
</div>
|
|
<label class="label"><span class="label-text-alt text-slate-500 italic">Name of the video owner.</span></label>
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-slate-300 text-xs">Advanced: Position</span></label>
|
|
<input name="position" type="text" placeholder="center (optional)"
|
|
class="input input-bordered w-full bg-slate-900 border-slate-700 text-slate-200 focus:border-indigo-500 text-sm h-10">
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" onclick="add_modal.close()" class="btn btn-ghost text-slate-400">Cancel</button>
|
|
<button type="submit" class="btn btn-indigo bg-indigo-600 hover:bg-indigo-500 border-none text-white">Add Background</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</dialog>
|
|
|
|
<script>
|
|
let keys = [];
|
|
let youtube_urls = [];
|
|
|
|
function h(str) {
|
|
return String(str ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
async function loadBackgrounds() {
|
|
try {
|
|
const response = await fetch("backgrounds.json");
|
|
const data = await response.json();
|
|
delete data["__comment"];
|
|
|
|
const container = document.getElementById('backgrounds');
|
|
let html = '';
|
|
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
keys.push(key);
|
|
youtube_urls.push(value[0]);
|
|
|
|
const videoId = value[0].includes('?v=') ? value[0].split('?v=')[1] : value[0].split('/').pop();
|
|
|
|
html += `
|
|
<div class="bg-card group bg-slate-800 rounded-xl overflow-hidden border border-white/5 hover:border-indigo-500/50 transition-all duration-300 shadow-lg">
|
|
<div class="aspect-video w-full bg-black relative">
|
|
<iframe class="w-full h-full"
|
|
src="https://www.youtube-nocookie.com/embed/${videoId}"
|
|
title="YouTube video player"
|
|
frameborder="0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowfullscreen></iframe>
|
|
</div>
|
|
<div class="p-4">
|
|
<h3 class="text-slate-200 font-medium truncate mb-1" title="${h(key)}">${h(key)}</h3>
|
|
<p class="text-slate-500 text-xs truncate mb-4">${h(value[2])}</p>
|
|
|
|
<div class="flex justify-end">
|
|
<button onclick="confirmDelete('${key}')" class="btn btn-square btn-sm btn-ghost hover:bg-red-500/20 hover:text-red-400">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
lucide.createIcons();
|
|
} catch (error) {
|
|
console.error("Error loading backgrounds:", error);
|
|
}
|
|
}
|
|
|
|
function confirmDelete(key) {
|
|
document.getElementById('background-key').value = key;
|
|
delete_modal.showModal();
|
|
}
|
|
|
|
function searchFilter() {
|
|
const query = document.querySelector(".searchFilter").value.toLowerCase();
|
|
const cards = document.querySelectorAll(".bg-card");
|
|
let visibleCount = 0;
|
|
|
|
cards.forEach(card => {
|
|
const text = card.textContent.toLowerCase();
|
|
const matches = text.includes(query);
|
|
card.classList.toggle('hidden', !matches);
|
|
if (matches) visibleCount++;
|
|
});
|
|
|
|
document.getElementById('empty-state').classList.toggle('hidden', visibleCount > 0);
|
|
}
|
|
|
|
const form = document.getElementById('addBgForm');
|
|
form.addEventListener('submit', (e) => {
|
|
let isValid = true;
|
|
form.querySelectorAll('input').forEach(input => {
|
|
if (!validate(input)) isValid = false;
|
|
});
|
|
if (!isValid) e.preventDefault();
|
|
});
|
|
|
|
form.querySelectorAll('input').forEach(input => {
|
|
input.addEventListener('keyup', () => validate(input));
|
|
});
|
|
|
|
function validate(input) {
|
|
const name = input.name;
|
|
const value = input.value;
|
|
let valid = true;
|
|
let message = "";
|
|
|
|
if (name === "youtube_uri") {
|
|
const regex = /(?:\/|%3D|v=|vi=)([0-9A-z-_]{11})(?:[%#?&]|$)/;
|
|
if (!regex.test(value)) {
|
|
message = "Invalid YouTube URI";
|
|
valid = false;
|
|
} else if (youtube_urls.includes(value)) {
|
|
message = "Background already added";
|
|
valid = false;
|
|
}
|
|
const feedback = document.getElementById('feedbackYT');
|
|
feedback.textContent = message;
|
|
feedback.classList.toggle('hidden', valid);
|
|
}
|
|
|
|
if (name === "filename") {
|
|
if (keys.includes(value)) {
|
|
message = "Filename already taken";
|
|
valid = false;
|
|
} else if (!/^([a-zA-Z0-9\s_-]{1,100})$/.test(value)) {
|
|
valid = false;
|
|
}
|
|
const feedback = document.getElementById('feedbackFilename');
|
|
feedback.textContent = message;
|
|
feedback.classList.toggle('hidden', valid);
|
|
}
|
|
|
|
if (name === "position") {
|
|
if (value && value !== "center" && isNaN(parseFloat(value))) {
|
|
valid = false;
|
|
}
|
|
}
|
|
|
|
input.classList.toggle('input-error', !valid);
|
|
return valid;
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', loadBackgrounds);
|
|
</script>
|
|
|
|
{% endblock %}
|