style(gui): apply desktop neo redesign

Refresh the GUI templates around the desktop-first neo-brutalist direction so the library, create, background, and settings screens share one visual system.

Tested: rtk docker compose run --rm test

Tested: Playwright desktop screenshots for /, select mode, /backgrounds, and /settings
pull/2550/head
Hong Phuc 3 weeks ago
parent d3fdd4145b
commit d12faef764

@ -1,22 +1,22 @@
{% extends "layout.html" %}
{% block main %}
<div class="bg-slate-900 min-h-screen py-8">
<div class="bg-[#F2EFEB] min-h-[calc(100vh-6.5rem)] 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>
<h1 class="text-3xl font-display font-black uppercase tracking-tighter text-[#111111]">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"
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#111111]/40"></i>
<input type="text"
class="searchFilter input-neo w-full pl-10"
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">
<button onclick="add_modal.showModal()" class="btn-primary-neo text-sm">
<i data-lucide="plus" class="w-4 h-4"></i>
<span class="hidden sm:inline">Add Video</span>
<span class="hidden sm:inline ml-1">Add Video</span>
</button>
</div>
</div>
@ -27,23 +27,23 @@
</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 id="empty-state" class="hidden flex flex-col items-center justify-center py-20 text-[#111111]/30">
<i data-lucide="film" class="w-16 h-16 mb-4"></i>
<p class="text-lg font-mono uppercase tracking-wider">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-box bg-white border border-[#111111]">
<h3 class="font-display font-black uppercase tracking-tighter text-lg text-[#111111]">Delete Background</h3>
<p class="py-4 text-[#111111]/60 font-mono text-sm">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>
<button type="button" onclick="delete_modal.close()" class="btn-ghost-neo">Cancel</button>
<button type="submit" class="btn-danger-neo text-sm">Delete</button>
</form>
</div>
</div>
@ -51,54 +51,54 @@
<!-- 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>
<div class="modal-box bg-white border border-[#111111] max-w-lg">
<h3 class="font-display font-black uppercase tracking-tighter text-lg text-[#111111] 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>
<label class="label"><span class="label-text text-[#111111]/70 font-mono text-xs uppercase tracking-wider">YouTube URI</span></label>
<div class="flex border border-[#111111]">
<div class="flex items-center justify-center px-3 bg-[#111111]/5 border-r border-[#111111] shrink-0">
<i data-lucide="youtube" class="w-4 h-4 text-[#111111]"></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">
<input name="youtube_uri" type="text" placeholder="https://www.youtube.com/watch?v=..."
class="input-neo border-0 flex-1">
</div>
<label class="label h-6"><span id="feedbackYT" class="label-text-alt text-error hidden"></span></label>
<label class="label h-6"><span id="feedbackYT" class="label-text-alt text-[#DE6C56] font-mono text-xs 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>
<label class="label"><span class="label-text text-[#111111]/70 font-mono text-xs uppercase tracking-wider">Filename</span></label>
<div class="flex border border-[#111111]">
<div class="flex items-center justify-center px-3 bg-[#111111]/5 border-r border-[#111111] shrink-0">
<i data-lucide="file-video" class="w-4 h-4 text-[#111111]"></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">
<input name="filename" type="text" placeholder="e.g. minecraft-parkour"
class="input-neo border-0 flex-1">
</div>
<label class="label h-6"><span id="feedbackFilename" class="label-text-alt text-error hidden"></span></label>
<label class="label h-6"><span id="feedbackFilename" class="label-text-alt text-[#DE6C56] font-mono text-xs 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>
<label class="label"><span class="label-text text-[#111111]/70 font-mono text-xs uppercase tracking-wider">Credits</span></label>
<div class="flex border border-[#111111]">
<div class="flex items-center justify-center px-3 bg-[#111111]/5 border-r border-[#111111] shrink-0">
<i data-lucide="user" class="w-4 h-4 text-[#111111]"></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">
<input name="citation" type="text" placeholder="YouTube Channel Name"
class="input-neo border-0 flex-1">
</div>
<label class="label"><span class="label-text-alt text-slate-500 italic">Name of the video owner.</span></label>
<label class="label"><span class="label-text-alt text-[#111111]/40 font-mono text-xs">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">
<label class="label"><span class="label-text text-[#111111]/70 font-mono text-xs uppercase tracking-wider">Advanced: Position</span></label>
<input name="position" type="text" placeholder="center (optional)"
class="input-neo w-full 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>
<button type="button" onclick="add_modal.close()" class="btn-ghost-neo">Cancel</button>
<button type="submit" class="btn-primary-neo text-sm">Add Background</button>
</div>
</form>
</div>
@ -122,10 +122,10 @@
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]);
@ -133,21 +133,21 @@
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"
<div class="bg-card group bg-white border border-[#111111] hover:border-[#DE6C56] transition-colors duration-200">
<div class="aspect-video w-full bg-black border-b border-[#111111] 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>
<h3 class="text-[#111111] font-mono text-sm font-medium truncate mb-1" title="${h(key)}">${h(key)}</h3>
<p class="text-[#111111]/40 font-mono 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">
<button onclick="confirmDelete('${key}')" class="btn-ghost-neo p-2 hover:text-[#DE6C56]">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
@ -169,7 +169,7 @@
}
function searchFilter() {
const query = document.querySelector(".searchFilter").value.toLowerCase();
const query = document.querySelector(".searchFilter") ? document.querySelector(".searchFilter").value.toLowerCase() : '';
const cards = document.querySelectorAll(".bg-card");
let visibleCount = 0;
@ -203,7 +203,7 @@
let message = "";
if (name === "youtube_uri") {
const regex = /(?:\/|%3D|v=|vi=)([0-9A-z-_]{11})(?:[%#?&]|$)/;
const regex = /(?:\/|%3D|v=|vi=)([0-9A-Za-z_-]{11})(?:[%#?&]|$)/;
if (!regex.test(value)) {
message = "Invalid YouTube URI";
valid = false;

@ -1,21 +1,21 @@
{% extends "layout.html" %}
{% block main %}
<div class="bg-slate-900 min-h-screen py-12">
<div class="bg-[#F2EFEB] min-h-[calc(100vh-6.5rem)] py-12">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 lg:grid-cols-5 gap-6">
<!-- LEFT PANEL: Controls + Progress -->
<div class="lg:col-span-2">
<div class="card bg-slate-800 border border-white/5 shadow-2xl">
<div class="card-body p-8">
<div class="card-neo">
<div class="p-8">
<div class="flex items-center gap-4 mb-8">
<div class="bg-indigo-600/20 p-3 rounded-xl">
<i data-lucide="plus-square" class="w-8 h-8 text-indigo-500"></i>
<div class="bg-[#DE6C56]/10 p-3 border border-[#DE6C56]">
<i data-lucide="plus-square" class="w-8 h-8 text-[#DE6C56]"></i>
</div>
<div>
<h2 class="card-title text-2xl text-white">Create New Short</h2>
<p class="text-slate-400 text-sm">Start the automated video creation pipeline.</p>
<h2 class="font-display font-black uppercase tracking-tighter text-2xl text-[#111111]">Create New Short</h2>
<p class="text-[#111111]/50 font-mono text-xs mt-1">Start the automated video creation pipeline.</p>
</div>
</div>
@ -23,26 +23,26 @@
<!-- Search Keywords Input -->
<div class="form-control w-full">
<label class="label px-0">
<span class="label-text text-slate-300 font-medium">Search Keywords</span>
<span class="label-text-alt text-slate-500">Optional — overrides config</span>
<span class="label-text text-[#111111] font-mono text-xs uppercase tracking-wider font-medium">Search Keywords</span>
<span class="label-text-alt text-[#111111]/40 font-mono text-[10px]">Optional</span>
</label>
<div class="flex gap-2">
<input id="keywords-input" type="text"
class="input input-bordered bg-slate-900 border-slate-700 flex-1 text-white placeholder:text-slate-600"
class="input-neo flex-1"
placeholder="news, politics, trending, viral"
value="{{ default_search_queries }}">
<button id="clear-keywords" class="btn btn-ghost btn-square text-slate-500 hover:text-slate-300"
<button id="clear-keywords" class="btn-ghost-neo"
onclick="document.getElementById('keywords-input').value=''" type="button">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<label class="label px-0">
<span class="label-text-alt text-slate-500">Comma-separated topics to search on Threads. Leave empty for config default.</span>
<span class="label-text-alt text-[#111111]/40 font-mono text-xs">Comma-separated topics. Leave empty for config default.</span>
</label>
</div>
<!-- Action Button -->
<button id="create-btn" class="btn btn-indigo btn-lg w-full bg-indigo-600 hover:bg-indigo-500 border-none text-white h-16 text-lg"
<button id="create-btn" class="btn-primary-neo w-full h-16 text-lg"
onclick="startPipeline()" disabled>
<span id="btn-text">Initializing...</span>
<span id="btn-spinner" class="loading loading-spinner loading-md hidden"></span>
@ -52,30 +52,30 @@
<div id="progress-area" class="hidden space-y-4">
<div class="flex justify-between items-end">
<div class="space-y-1">
<span class="text-xs uppercase tracking-widest text-slate-500 font-bold">Current Stage</span>
<span class="text-xs uppercase tracking-widest text-[#111111]/40 font-mono font-bold">Current Stage</span>
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-indigo-500 animate-pulse"></div>
<h3 id="stage-text" class="text-indigo-400 font-semibold text-lg capitalize">Preparing...</h3>
<div class="w-2 h-2 bg-[#DE6C56]"></div>
<h3 id="stage-text" class="text-[#DE6C56] font-mono font-bold text-lg uppercase tracking-wider">Preparing...</h3>
</div>
</div>
<span id="pct-text" class="text-2xl font-black text-slate-700 font-mono">0%</span>
<span id="pct-text" class="text-2xl font-black text-[#111111]/20 font-mono">0%</span>
</div>
<progress id="progress-bar" class="progress progress-indigo w-full h-3 bg-slate-900" value="0" max="100"></progress>
<progress id="progress-bar" class="progress w-full h-3 bg-[#111111]/10 [&::-webkit-progress-value]:bg-[#DE6C56] [&::-moz-progress-bar]:bg-[#DE6C56]" value="0" max="100"></progress>
<div class="grid grid-cols-2 gap-4 pt-4">
<div class="bg-slate-900/50 p-3 rounded-lg border border-white/5 flex items-center gap-3">
<i data-lucide="clock" class="w-4 h-4 text-slate-500"></i>
<div class="bg-white border border-[#111111] p-3 flex items-center gap-3">
<i data-lucide="clock" class="w-4 h-4 text-[#111111]/40"></i>
<div class="flex flex-col">
<span class="text-[10px] uppercase text-slate-500 font-bold">Elapsed</span>
<span id="elapsed-time" class="text-sm font-mono text-slate-300">00:00</span>
<span class="text-[10px] uppercase text-[#111111]/40 font-mono font-bold">Elapsed</span>
<span id="elapsed-time" class="text-sm font-mono text-[#111111]">00:00</span>
</div>
</div>
<div class="bg-slate-900/50 p-3 rounded-lg border border-white/5 flex items-center gap-3">
<i data-lucide="layers" class="w-4 h-4 text-slate-500"></i>
<div class="bg-white border border-[#111111] p-3 flex items-center gap-3">
<i data-lucide="layers" class="w-4 h-4 text-[#111111]/40"></i>
<div class="flex flex-col">
<span class="text-[10px] uppercase text-slate-500 font-bold">Status</span>
<span class="text-sm text-indigo-400">Processing</span>
<span class="text-[10px] uppercase text-[#111111]/40 font-mono font-bold">Status</span>
<span class="text-sm font-mono text-[#DE6C56]">Processing</span>
</div>
</div>
</div>
@ -83,25 +83,25 @@
<!-- Success Message -->
<div id="done-area" class="hidden">
<div class="alert bg-emerald-500/10 border-emerald-500/20 text-emerald-400 flex flex-col items-start gap-4 p-6">
<div class="border border-[#4ADE80] bg-[#4ADE80]/10 p-6 flex flex-col items-start gap-4">
<div class="flex items-center gap-3">
<div class="bg-emerald-500 text-slate-900 p-1 rounded-full">
<div class="bg-[#4ADE80] text-[#111111] p-1">
<i data-lucide="check" class="w-4 h-4"></i>
</div>
<span class="font-bold text-lg">Generation Complete!</span>
<span class="font-display font-black uppercase tracking-tighter text-lg text-[#111111]">Generation Complete!</span>
</div>
<p id="done-msg" class="text-slate-300 text-sm">Your video has been rendered and saved to the library.</p>
<a href="/" class="btn btn-emerald btn-sm bg-emerald-600 hover:bg-emerald-500 border-none text-white px-6">View Video</a>
<p id="done-msg" class="text-[#111111]/60 font-mono text-sm">Your video has been rendered and saved to the library.</p>
<a href="/" class="btn-secondary-neo text-sm">View Video</a>
</div>
</div>
<!-- Error Message -->
<div id="error-area" class="hidden">
<div class="alert bg-red-500/10 border-red-500/20 text-red-400 p-6">
<i data-lucide="alert-triangle" class="w-6 h-6"></i>
<div>
<h3 class="font-bold">Pipeline Failed</h3>
<div id="error-text" class="text-xs mt-2 font-mono bg-black/20 p-3 rounded overflow-x-auto whitespace-pre-wrap"></div>
<div class="border border-[#DE6C56] bg-[#DE6C56]/5 p-6">
<i data-lucide="alert-triangle" class="w-6 h-6 text-[#DE6C56]"></i>
<div class="mt-2">
<h3 class="font-display font-black uppercase tracking-tighter text-[#111111]">Pipeline Failed</h3>
<div id="error-text" class="text-xs mt-2 font-mono bg-[#111111]/5 p-3 overflow-x-auto whitespace-pre-wrap text-[#111111]/70"></div>
</div>
</div>
</div>
@ -109,10 +109,10 @@
<!-- Log Output -->
<div id="log-area" class="hidden space-y-3">
<div class="flex items-center justify-between">
<h4 class="text-xs uppercase tracking-widest text-slate-500 font-bold">Execution Logs</h4>
<span class="badge badge-outline border-slate-700 text-slate-500 text-[10px]">Real-time</span>
<h4 class="text-xs uppercase tracking-widest text-[#111111]/40 font-mono font-bold">Execution Logs</h4>
<span class="font-mono text-[10px] uppercase border border-[#111111] px-2 py-0.5 text-[#111111]/50">Real-time</span>
</div>
<div id="log-list" class="bg-slate-900 rounded-xl p-4 font-mono text-[11px] leading-relaxed text-slate-400 h-48 overflow-y-auto border border-white/5 shadow-inner">
<div id="log-list" class="bg-white border border-[#111111] p-4 font-mono text-[11px] leading-relaxed text-[#111111]/60 h-48 overflow-y-auto">
</div>
</div>
</div>
@ -122,14 +122,14 @@
<!-- RIGHT PANEL: Pipeline Activity Visualization -->
<div class="lg:col-span-3">
<div class="card bg-slate-800 border border-white/5 shadow-2xl">
<div class="card-body p-6">
<div class="card-neo">
<div class="p-6">
<div class="flex items-center justify-between mb-5">
<div class="flex items-center gap-2">
<i data-lucide="activity" class="w-5 h-5 text-indigo-400"></i>
<h3 class="card-title text-white text-lg">Pipeline Activity</h3>
<i data-lucide="activity" class="w-5 h-5 text-[#DE6C56]"></i>
<h3 class="font-display font-black uppercase tracking-tighter text-lg text-[#111111]">Pipeline Activity</h3>
</div>
<span class="badge badge-outline border-slate-700 text-slate-500 text-[10px]">Live</span>
<span class="font-mono text-[10px] uppercase border border-[#4ADE80] text-[#4ADE80] px-2 py-0.5">Live</span>
</div>
<!-- Stage Diagram -->
@ -137,14 +137,14 @@
</div>
<!-- Scraper Event Feed -->
<div id="scraper-feed" class="space-y-2 max-h-[500px] overflow-y-auto pr-1 custom-scrollbar">
<div id="scraper-feed" class="space-y-2 max-h-[500px] overflow-y-auto pr-1">
</div>
<!-- Empty state -->
<div id="feed-empty" class="text-center py-16 text-slate-600">
<i data-lucide="eye-off" class="w-12 h-12 mx-auto mb-3 opacity-50"></i>
<p class="text-sm">Scraper activity will appear here</p>
<p class="text-xs mt-1">Start a pipeline to see real-time scraping visualization</p>
<div id="feed-empty" class="text-center py-16 text-[#111111]/30">
<i data-lucide="eye-off" class="w-12 h-12 mx-auto mb-3"></i>
<p class="text-sm font-mono uppercase tracking-wider">Scraper activity will appear here</p>
<p class="text-xs mt-1 font-mono">Start a pipeline to see real-time scraping visualization</p>
</div>
</div>
</div>
@ -255,31 +255,27 @@
const isActive = globalIdx === activeIdx;
const isPending = globalIdx > activeIdx;
let cls = 'flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap shrink-0 transition-all duration-300 ';
let cls = 'flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-mono font-medium whitespace-nowrap shrink-0 transition-all duration-300 border ';
if (isActive) {
cls += 'bg-indigo-600/30 text-indigo-300 ring-1 ring-indigo-500/50';
cls += 'bg-[#DE6C56]/10 border-[#DE6C56] text-[#DE6C56]';
} else if (isDone) {
cls += 'bg-emerald-600/20 text-emerald-400';
cls += 'bg-[#4ADE80]/10 border-[#4ADE80] text-[#111111]';
} else {
cls += 'bg-slate-700/50 text-slate-500';
cls += 'bg-white border-[#111111]/20 text-[#111111]/40';
}
html += '<div class="' + cls + '">';
if (isDone) {
html += '<i data-lucide="check" class="w-3 h-3 text-emerald-400"></i>';
html += '<i data-lucide="check" class="w-3 h-3 text-[#4ADE80]"></i>';
} else if (isActive) {
html += '<i data-lucide="loader-2" class="w-3 h-3 text-indigo-300 animate-spin"></i>';
html += '<i data-lucide="loader-2" class="w-3 h-3 text-[#DE6C56] animate-spin"></i>';
} else {
html += '<i data-lucide="' + st.icon + '" class="w-3 h-3"></i>';
}
html += '<span>' + st.label + '</span></div>';
html += '<span class="uppercase tracking-wider">' + st.label + '</span></div>';
if (idx < visible.length - 1) {
html += '<div class="text-slate-600 shrink-0">';
html += isDone
? '<i data-lucide="chevron-right" class="w-3 h-3 text-emerald-400/50"></i>'
: '<i data-lucide="chevron-right" class="w-3 h-3"></i>';
html += '</div>';
html += '<div class="text-[#111111]/30 shrink-0 mx-0.5">///</div>';
}
});
html += '</div>';
@ -295,18 +291,18 @@
const templates = {
'post_discovered': () =>
'<div class="bg-slate-700/30 rounded-lg p-3 border border-white/5 hover:border-indigo-500/30 transition-colors">' +
'<div class="bg-white border border-[#111111] p-3 hover:border-[#DE6C56] transition-colors">' +
'<div class="flex items-start gap-3">' +
'<div class="bg-indigo-600/20 p-1.5 rounded-full shrink-0">' +
'<i data-lucide="message-circle" class="w-3.5 h-3.5 text-indigo-400"></i>' +
'<div class="bg-[#DE6C56]/10 p-1.5 border border-[#DE6C56] shrink-0">' +
'<i data-lucide="message-circle" class="w-3.5 h-3.5 text-[#DE6C56]"></i>' +
'</div>' +
'<div class="flex-1 min-w-0">' +
'<div class="flex items-center gap-2 mb-1">' +
'<span class="text-xs font-semibold text-slate-200 truncate">' + esc(d.username || 'unknown') + '</span>' +
'<span class="text-[10px] text-slate-500">' + fmtTime(ts) + '</span>' +
'<span class="text-xs font-mono font-bold text-[#111111] truncate uppercase">' + esc(d.username || 'unknown') + '</span>' +
'<span class="text-[10px] text-[#111111]/40 font-mono">' + fmtTime(ts) + '</span>' +
'</div>' +
'<p class="text-[11px] text-slate-400 leading-relaxed line-clamp-2">' + esc(d.body || '') + '</p>' +
'<div class="flex items-center gap-3 mt-1.5 text-[10px] text-slate-500">' +
'<p class="text-[11px] text-[#111111]/60 font-mono leading-relaxed line-clamp-2">' + esc(d.body || '') + '</p>' +
'<div class="flex items-center gap-3 mt-1.5 text-[10px] text-[#111111]/40 font-mono">' +
'<span>&#9829; ' + fmtMetric(d.likes) + '</span>' +
'<span>&#128172; ' + fmtMetric(d.replies) + '</span>' +
(d.reposts ? '<span>&#128259; ' + fmtMetric(d.reposts) + '</span>' : '') +
@ -316,71 +312,71 @@
'</div>',
'feed_scroll': () =>
'<div class="flex items-center gap-2 py-1.5 px-1">' +
'<div class="bg-cyan-600/20 p-1 rounded-full">' +
'<i data-lucide="mouse-pointer-2" class="w-3 h-3 text-cyan-400"></i>' +
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="mouse-pointer-2" class="w-3 h-3 text-[#DE6C56]"></i>' +
'</div>' +
'<span class="text-[11px] text-slate-400">' +
'Scrolled <strong class="text-slate-300">' + (d.scroll || '?') + '/' + (d.max_scrolls || '?') + '</strong>' +
' &mdash; <strong class="text-slate-300">' + (d.new_posts || 0) + '</strong> new,' +
' <strong class="text-slate-300">' + (d.total_posts || 0) + '</strong> total posts' +
'<span class="text-[11px] text-[#111111]/50">' +
'Scrolled <strong class="text-[#111111]">' + (d.scroll || '?') + '/' + (d.max_scrolls || '?') + '</strong>' +
' &mdash; <strong class="text-[#111111]">' + (d.new_posts || 0) + '</strong> new,' +
' <strong class="text-[#111111]">' + (d.total_posts || 0) + '</strong> total posts' +
'</span>' +
'</div>',
'search_query': () =>
'<div class="flex items-center gap-2 py-1.5 px-1">' +
'<div class="bg-purple-600/20 p-1 rounded-full">' +
'<i data-lucide="search" class="w-3 h-3 text-purple-400"></i>' +
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="search" class="w-3 h-3 text-[#111111]"></i>' +
'</div>' +
'<span class="text-[11px] text-slate-400">' +
'Searched "<strong class="text-slate-300">' + esc(d.query || '') + '</strong>"' +
' &mdash; <strong class="text-slate-300">' + (d.posts_found || 0) + '</strong> posts found' +
'<span class="text-[11px] text-[#111111]/50">' +
'Searched "<strong class="text-[#111111]">' + esc(d.query || '') + '</strong>"' +
' &mdash; <strong class="text-[#111111]">' + (d.posts_found || 0) + '</strong> posts found' +
'</span>' +
'</div>',
'filter_results': () =>
'<div class="flex items-center gap-2 py-1.5 px-1">' +
'<div class="bg-amber-600/20 p-1 rounded-full">' +
'<i data-lucide="filter" class="w-3 h-3 text-amber-400"></i>' +
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="filter" class="w-3 h-3 text-[#111111]"></i>' +
'</div>' +
'<span class="text-[11px] text-slate-400">' +
'Filtered <strong class="text-slate-300">' + (d.before || 0) + '</strong> posts &rarr;' +
' <strong class="text-slate-300">' + (d.after || 0) + '</strong> candidates' +
'<span class="text-[11px] text-[#111111]/50">' +
'Filtered <strong class="text-[#111111]">' + (d.before || 0) + '</strong> posts &rarr;' +
' <strong class="text-[#111111]">' + (d.after || 0) + '</strong> candidates' +
(d.min_engagement ? ' (min ' + fmtMetric(d.min_engagement) + ' engagement)' : '') +
'</span>' +
'</div>',
'visiting_post': () =>
'<div class="flex items-center gap-2 py-1.5 px-1">' +
'<div class="bg-rose-600/20 p-1 rounded-full">' +
'<i data-lucide="external-link" class="w-3 h-3 text-rose-400"></i>' +
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="external-link" class="w-3 h-3 text-[#DE6C56]"></i>' +
'</div>' +
'<span class="text-[11px] text-slate-400">' +
'<span class="text-[11px] text-[#111111]/50">' +
'Examining candidate #' + (d.attempt || '?') + ': "' + esc((d.body || '').substring(0, 40)) + '..."' +
' <span class="text-slate-500 ml-1">&#9829;' + fmtMetric(d.likes) + '</span>' +
' <span class="text-[#111111]/40 ml-1">&#9829;' + fmtMetric(d.likes) + '</span>' +
'</span>' +
'</div>',
'replies_found': () =>
'<div class="flex items-center gap-2 py-1.5 px-1">' +
'<div class="bg-emerald-600/20 p-1 rounded-full">' +
'<i data-lucide="message-square" class="w-3 h-3 text-emerald-400"></i>' +
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="message-square" class="w-3 h-3 text-[#4ADE80]"></i>' +
'</div>' +
'<span class="text-[11px] text-slate-400">' +
'<span class="text-[11px] text-[#111111]/50">' +
d.count + ' replies found' + (d.min_required ? ' (need ' + d.min_required + ')' : '') +
'</span>' +
'</div>',
'post_selected': () =>
'<div class="bg-emerald-600/10 rounded-lg p-3 border border-emerald-500/20">' +
'<div class="bg-[#4ADE80]/10 border border-[#4ADE80] p-3">' +
'<div class="flex items-start gap-3">' +
'<div class="bg-emerald-600/30 p-1.5 rounded-full shrink-0">' +
'<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-400"></i>' +
'<div class="bg-[#4ADE80] p-1.5 shrink-0">' +
'<i data-lucide="check-circle" class="w-3.5 h-3.5 text-[#111111]"></i>' +
'</div>' +
'<div>' +
'<span class="text-xs font-semibold text-emerald-300">Post Selected!</span>' +
'<p class="text-[11px] text-slate-400 mt-0.5">' + esc(d.title || '') + '</p>' +
'<div class="flex gap-3 mt-1 text-[10px] text-slate-500">' +
'<span class="text-xs font-mono font-bold text-[#111111] uppercase">Post Selected!</span>' +
'<p class="text-[11px] text-[#111111]/60 font-mono mt-0.5">' + esc(d.title || '') + '</p>' +
'<div class="flex gap-3 mt-1 text-[10px] text-[#111111]/40 font-mono">' +
'<span>&#9829; ' + fmtMetric(d.likes) + '</span>' +
'<span>&#128172; ' + (d.replies_count || 0) + ' replies</span>' +
'</div>' +
@ -389,26 +385,26 @@
'</div>',
'login': () =>
'<div class="flex items-center gap-2 py-1.5 px-1">' +
'<div class="bg-blue-600/20 p-1 rounded-full">' +
'<i data-lucide="log-in" class="w-3 h-3 text-blue-400"></i>' +
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="log-in" class="w-3 h-3 text-[#111111]"></i>' +
'</div>' +
'<span class="text-[11px] text-slate-400">' + esc(d.message || '') + '</span>' +
'<span class="text-[11px] text-[#111111]/50">' + esc(d.message || '') + '</span>' +
'</div>',
'browser_launch': () =>
'<div class="flex items-center gap-2 py-1.5 px-1">' +
'<div class="bg-slate-600/20 p-1 rounded-full">' +
'<i data-lucide="globe" class="w-3 h-3 text-slate-400"></i>' +
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="globe" class="w-3 h-3 text-[#DE6C56]"></i>' +
'</div>' +
'<span class="text-[11px] text-slate-400">' + esc(d.message || '') + '</span>' +
'<span class="text-[11px] text-[#111111]/50">' + esc(d.message || '') + '</span>' +
'</div>',
};
const fn = templates[type];
return fn ? fn() : (
'<div class="flex items-center gap-2 py-1.5 px-1">' +
'<span class="text-[11px] text-slate-400">' + esc(d.message || type) + '</span>' +
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<span class="text-[11px] text-[#111111]/50">' + esc(d.message || type) + '</span>' +
'</div>'
);
}
@ -496,7 +492,7 @@
if (state.log && state.log.length > 0) {
logList.innerHTML = state.log.map(function(l) {
return '<div class="py-0.5 border-b border-white/5 last:border-0">' + esc(l) + '</div>';
return '<div class="py-0.5 border-b border-[#111111]/10 last:border-0">' + esc(l) + '</div>';
}).join('');
logList.scrollTop = logList.scrollHeight;
}
@ -549,10 +545,9 @@
btn.disabled = true;
document.getElementById('btn-spinner').classList.remove('hidden');
document.getElementById('btn-text').textContent = 'Running...';
startTime = new Date(); // Approximate
startTime = new Date();
elapsedTimer = setInterval(updateElapsedTime, 1000);
pollTimer = setInterval(pollStatus, 2000);
// Re-render visualization from state
renderStageDiagram(state.stage);
if (state.scraper_events) {
renderScraperFeed(state.scraper_events);

@ -1,25 +1,25 @@
{% extends "layout.html" %}
{% block main %}
<div class="bg-slate-900 min-h-screen py-8">
<div class="bg-[#F2EFEB] min-h-[calc(100vh-6.5rem)] py-8">
<div class="container mx-auto px-4">
<!-- Header & Search -->
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-8">
<h1 class="text-2xl font-bold text-white">Video Library</h1>
<div class="flex flex-col md:flex-row justify-between items-stretch md:items-center gap-4 mb-8">
<h1 class="text-3xl font-display font-black uppercase tracking-tighter text-[#111111]">Video Library</h1>
<!-- Bulk-action bar (visible in select mode only) -->
<div id="bulk-bar" class="hidden items-center gap-2">
<div id="bulk-bar" class="hidden w-full md:w-auto items-center justify-between md:justify-end gap-2 border border-[#111111] bg-white p-2">
<button type="button" onclick="selectAll()"
class="btn btn-ghost text-slate-400 border border-slate-700">
class="btn-ghost-neo h-10">
<i data-lucide="check-square" class="w-4 h-4 mr-1"></i>
<span id="select-all-label">Select All</span>
</button>
<button type="button" onclick="cancelSelectMode()"
class="btn btn-ghost text-slate-400">
class="btn-ghost-neo h-10">
Cancel
</button>
<button id="bulk-delete-btn" type="button" onclick="confirmBulkDelete()"
class="btn btn-error" disabled>
class="btn-danger-neo text-sm h-10">
<i data-lucide="trash-2" class="w-4 h-4 mr-1"></i>
Delete (<span id="selection-count">0</span>)
</button>
@ -28,15 +28,14 @@
<!-- Normal toolbar (hidden in select mode) -->
<div id="normal-toolbar" class="flex items-center gap-2 w-full md:w-auto">
<button type="button" onclick="toggleSelectMode()"
class="btn btn-ghost text-slate-400 border border-slate-700 shrink-0"
style="height: 3rem; min-height: 3rem;">
class="btn-ghost-neo shrink-0" style="height: 3rem; min-height: 3rem;">
<i data-lucide="check-square" class="w-4 h-4 mr-1"></i>
Select
</button>
<div class="relative w-full md:w-72">
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"></i>
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#111111]/40"></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"
class="searchFilter input-neo w-full pl-10"
style="height: 3rem;"
placeholder="Search videos..."
onkeyup="searchFilter()">
@ -50,19 +49,19 @@
</div>
<!-- Empty State -->
<div id="empty-state" class="hidden flex flex-col items-center justify-center py-20 text-slate-500">
<i data-lucide="video-off" class="w-16 h-16 mb-4 opacity-20"></i>
<p class="text-lg">No videos found</p>
<div id="empty-state" class="hidden flex flex-col items-center justify-center py-20 text-[#111111]/30">
<i data-lucide="video-off" class="w-16 h-16 mb-4"></i>
<p class="text-lg font-mono uppercase tracking-wider">No videos found</p>
</div>
</div>
</div>
<!-- Video Player Modal -->
<dialog id="player_modal" class="modal modal-bottom sm:modal-middle">
<div class="modal-box bg-slate-900 border border-white/10 max-w-2xl p-0 overflow-hidden">
<div class="flex justify-between items-center px-4 py-3 border-b border-white/5">
<h3 id="player_title" class="font-medium text-slate-200 truncate pr-4"></h3>
<button type="button" onclick="closePlayer()" class="btn btn-square btn-sm btn-ghost text-slate-400">
<div class="modal-box bg-white border border-[#111111] max-w-2xl p-0 overflow-hidden">
<div class="flex justify-between items-center px-4 py-3 border-b border-[#111111]">
<h3 id="player_title" class="font-mono text-sm text-[#111111] truncate pr-4 uppercase"></h3>
<button type="button" onclick="closePlayer()" class="btn-ghost-neo p-1">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
@ -73,17 +72,17 @@
<!-- Delete Confirmation Modal -->
<dialog id="delete_modal" class="modal">
<div class="modal-box bg-slate-900 border border-white/10">
<div class="modal-box bg-white border border-[#111111]">
<div class="flex items-center gap-3 mb-3">
<i data-lucide="triangle-alert" class="w-5 h-5 text-red-400 shrink-0"></i>
<h3 class="font-bold text-lg text-white">Delete Video?</h3>
<i data-lucide="triangle-alert" class="w-5 h-5 text-[#DE6C56] shrink-0"></i>
<h3 class="font-display font-black uppercase tracking-tighter text-lg text-[#111111]">Delete Video?</h3>
</div>
<p id="delete-modal-msg" class="text-slate-400 mb-6 text-sm"></p>
<p id="delete-modal-msg" class="text-[#111111]/60 mb-6 text-sm font-mono"></p>
<div class="modal-action mt-0">
<form method="dialog">
<button class="btn btn-ghost text-slate-400">Cancel</button>
<button class="btn-ghost-neo">Cancel</button>
</form>
<button type="button" class="btn btn-error" onclick="executeDelete()">
<button type="button" class="btn-danger-neo text-sm" onclick="executeDelete()">
<i data-lucide="trash-2" class="w-4 h-4 mr-1"></i>
Delete
</button>
@ -143,56 +142,55 @@
data.sort((b, a) => a['time'] - b['time']);
const container = document.getElementById('videos');
// Use data-* attributes for arbitrary strings — never embed them in onclick/href
container.innerHTML = data.map(v => {
const title = checkTitle(v.reddit_title, v.filename);
return `
<div class="video-card group bg-slate-800 rounded-xl overflow-hidden border border-white/5 hover:border-indigo-500/50 transition-all duration-300 hover:shadow-2xl hover:shadow-indigo-500/10 relative"
<div class="video-card group bg-white border border-[#111111] relative hover:border-[#DE6C56] transition-colors duration-200 flex flex-col min-h-[23.25rem]"
data-video-id="${h(v.id)}">
<!-- Checkbox overlay (shown in select mode) -->
<div class="select-overlay hidden absolute top-3 right-3 z-10 pointer-events-none">
<input type="checkbox" class="card-checkbox checkbox checkbox-primary w-6 h-6 pointer-events-auto" />
<input type="checkbox" class="card-checkbox w-5 h-5 pointer-events-auto accent-[#DE6C56]" />
</div>
<button type="button"
class="play-btn aspect-video w-full bg-slate-900 flex items-center justify-center relative overflow-hidden cursor-pointer"
class="play-btn aspect-video w-full bg-[#111111]/5 flex items-center justify-center relative overflow-hidden cursor-pointer border-b border-[#111111]"
data-video-id="${h(v.id)}"
data-video-title="${h(title)}">
<i data-lucide="play-circle" class="w-12 h-12 text-slate-700 group-hover:text-indigo-500 transition-colors"></i>
<i data-lucide="play-circle" class="w-12 h-12 text-[#111111]/30 group-hover:text-[#DE6C56] transition-colors"></i>
<div class="absolute top-3 left-3">
<span class="badge badge-sm bg-slate-900/80 border-none text-indigo-400 font-medium backdrop-blur-md">
<span class="inline-block bg-white border border-[#111111] text-[#111111] font-mono text-[10px] uppercase tracking-wider px-2 py-0.5">
${h(categoryLabel(v.subreddit))}
</span>
</div>
</button>
<div class="p-4">
<h3 class="text-slate-200 font-medium line-clamp-2 mb-4 h-12" title="${h(title)}">
<div class="p-4 flex flex-col flex-1">
<h3 class="text-[#111111] font-mono text-sm font-medium line-clamp-2 break-words mb-4 min-h-10" title="${h(title)}">
${h(title)}
</h3>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center justify-between gap-2 mt-auto">
<div class="flex gap-1">
<a href="${h(sourceUrl(v.subreddit, v.id))}" target="_blank"
class="btn btn-square btn-sm btn-ghost hover:bg-indigo-500/20 hover:text-indigo-400"
class="btn-ghost-neo p-2"
title="View Source">
<i data-lucide="external-link" class="w-4 h-4"></i>
</a>
<a href="/video/${encodeURIComponent(v.id)}?download=1" download
class="btn btn-square btn-sm btn-ghost hover:bg-indigo-500/20 hover:text-indigo-400"
class="btn-ghost-neo p-2"
title="Download">
<i data-lucide="download" class="w-4 h-4"></i>
</a>
</div>
<div class="flex gap-1">
<button class="btn btn-square btn-sm btn-ghost hover:bg-slate-700 copy-btn"
<button class="btn-ghost-neo p-2 copy-btn"
data-copy="${h(sourceUrl(v.subreddit, v.id))}"
title="Copy Link">
<i data-lucide="link" class="w-4 h-4"></i>
</button>
<button class="btn btn-square btn-sm btn-ghost hover:bg-red-500/20 hover:text-red-400 delete-btn"
<button class="btn-ghost-neo p-2 hover:text-[#DE6C56] delete-btn"
data-video-id="${h(v.id)}"
title="Delete">
<i data-lucide="trash-2" class="w-4 h-4"></i>
@ -200,8 +198,8 @@
</div>
</div>
<div class="mt-4 pt-4 border-t border-white/5 flex justify-between items-center">
<span class="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">
<div class="mt-4 pt-4 border-t border-[#111111]/20 flex justify-between items-center">
<span class="text-[10px] uppercase tracking-wider text-[#111111]/40 font-mono">
${timeSince(v.time)}
</span>
</div>
@ -228,7 +226,7 @@
btn.addEventListener('click', () => {
navigator.clipboard.writeText(btn.dataset.copy).then(() => {
const orig = btn.innerHTML;
btn.innerHTML = '<i data-lucide="check" class="w-4 h-4 text-green-500"></i>';
btn.innerHTML = '<i data-lucide="check" class="w-4 h-4 text-[#4ADE80]"></i>';
lucide.createIcons();
setTimeout(() => { btn.innerHTML = orig; lucide.createIcons(); }, 2000);
});
@ -344,7 +342,7 @@
}
function searchFilter() {
const query = document.querySelector(".searchFilter").value.toLowerCase();
const query = document.querySelector(".searchFilter") ? document.querySelector(".searchFilter").value.toLowerCase() : '';
const cards = document.querySelectorAll(".video-card");
let visibleCount = 0;
@ -387,4 +385,4 @@
document.addEventListener('DOMContentLoaded', loadVideos);
</script>
{% endblock %}
{% endblock %}

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<html lang="en">
<head>
<meta charset="utf-8">
@ -7,70 +7,325 @@
<meta http-equiv="cache-control" content="no-cache" />
<title>VideoMakerBot</title>
<!-- Modern Tech Stack -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css" rel="stylesheet" type="text/css" />
<!-- Tailwind config — must load BEFORE the CDN script -->
<script>
tailwind = window.tailwind || {};
tailwind.config = {
theme: {
extend: {
colors: {
cream: '#F2EFEB',
jet: '#111111',
terracotta: '#DE6C56',
'terracotta-dark': '#C85A46',
terminal: '#4ADE80',
},
fontFamily: {
display: ['"Clash Display"', 'system-ui', 'sans-serif'],
mono: ['"Space Mono"', 'monospace'],
},
}
}
};
</script>
<script src="https://cdn.tailwindcss.com"></script>
<!-- DaisyUI (component library) -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css" rel="stylesheet" type="text/css" />
<!-- Fonts: Clash Display (display/headings via Fontshare) + Space Mono (body/UI via Google Fonts) -->
<link rel="preconnect" href="https://api.fontshare.com">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@400,500,600,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<!-- Lucide icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
/* ── Neo-Brutalist Global Overrides ───────────────────────────── */
/* DaisyUI border-radius kill switch */
:root {
--rounded-box: 0rem;
--rounded-btn: 0rem;
--rounded-badge: 0rem;
--tab-radius: 0rem;
--tooltip-radius: 0rem;
--alert-radius: 0rem;
--input-radius: 0rem;
}
/* Global reset */
body {
font-family: 'Inter', sans-serif;
background-color: #0f172a; /* Slate 900 */
font-family: 'Space Mono', monospace;
background-color: #F2EFEB;
color: #111111;
}
/* Kill all box-shadows */
*,
*::before,
*::after {
box-shadow: none !important;
}
/* ── Typography ────────────────────────────────────────────────── */
h1, h2, h3 {
font-family: 'Clash Display', system-ui, sans-serif;
font-weight: 900;
text-transform: uppercase;
letter-spacing: -0.025em;
}
/* All UI text: monospace */
button, a, input, select, textarea, label, .btn, .badge, nav, footer {
font-family: 'Space Mono', monospace !important;
}
.glass-nav {
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
/* ── DaisyUI Component Overrides ───────────────────────────────── */
/* Buttons — kill radius, uppercase */
.btn {
border-radius: 0 !important;
text-transform: uppercase;
letter-spacing: 0.025em;
font-weight: 500;
}
/* Custom scrollbar for a modern look */
::-webkit-scrollbar {
width: 8px;
/* Cards */
.card {
border-radius: 0 !important;
}
::-webkit-scrollbar-track {
background: #1e293b;
/* Inputs / Selects / Textareas */
.input, .select, .textarea {
border-radius: 0 !important;
border: 1px solid #111111;
}
::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 10px;
/* Badges */
.badge {
border-radius: 0 !important;
}
::-webkit-scrollbar-thumb:hover {
background: #475569;
/* Modals */
.modal-box {
border-radius: 0 !important;
}
/* Dotted path inputs shouldn't show default browser validation UI */
input:invalid {
box-shadow: none;
/* Menu items */
.menu li > * {
border-radius: 0 !important;
}
/* Toggle / Checkbox — kill radius */
.toggle {
border-radius: 0 !important;
}
/* Alerts */
.alert {
border-radius: 0 !important;
}
/* Dividers — use /// text pattern where plain dividers appear */
.divider:not(.divider-text)::after,
.divider:not(.divider-text)::before {
/* We'll use .divider-text overrides in templates */
}
/* Progress bar */
.progress {
border-radius: 0 !important;
}
/* Join groups */
.join > * {
border-radius: 0 !important;
}
/* ── Custom Scrollbar ──────────────────────────────────────────── */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #F2EFEB; }
::-webkit-scrollbar-thumb { background: #111111; }
::-webkit-scrollbar-thumb:hover { background: #333333; }
/* Dotted path inputs — suppress default browser validation UI */
input:invalid { box-shadow: none; }
/* ── Navigation ────────────────────────────────────────────────── */
.nav-link {
font-family: 'Space Mono', monospace;
font-size: 0.8125rem;
color: #111111;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.375rem 0.75rem;
transition: background-color 150ms;
}
.nav-link:hover {
background-color: #111111;
color: #F2EFEB;
}
/* ── Buttons ───────────────────────────────────────────────────── */
.btn-primary-neo {
background-color: #DE6C56;
color: #F2EFEB;
border: 1px solid #DE6C56;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: 'Space Mono', monospace;
text-transform: uppercase;
font-weight: 500;
letter-spacing: 0.025em;
padding: 0.5rem 1.25rem;
cursor: pointer;
transition: all 150ms;
border-radius: 0;
}
.btn-primary-neo:hover {
background-color: #111111;
border-color: #111111;
color: #F2EFEB;
}
.btn-secondary-neo {
background-color: transparent;
color: #111111;
border: 2px solid #111111;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: 'Space Mono', monospace;
text-transform: uppercase;
font-weight: 500;
letter-spacing: 0.025em;
padding: 0.5rem 1.25rem;
cursor: pointer;
transition: all 150ms;
border-radius: 0;
}
.btn-secondary-neo:hover {
background-color: #111111;
color: #F2EFEB;
}
.btn-ghost-neo {
background: transparent;
color: #111111;
border: 1px solid #111111;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: 'Space Mono', monospace;
text-transform: uppercase;
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
cursor: pointer;
border-radius: 0;
transition: all 150ms;
}
.btn-ghost-neo:hover {
background-color: #111111;
color: #F2EFEB;
}
.btn-danger-neo {
background-color: transparent;
color: #DE6C56;
border: 1px solid #DE6C56;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: 'Space Mono', monospace;
text-transform: uppercase;
font-weight: 500;
letter-spacing: 0.025em;
padding: 0.5rem 1.25rem;
cursor: pointer;
transition: all 150ms;
border-radius: 0;
}
.btn-danger-neo:hover {
background-color: #DE6C56;
color: #F2EFEB;
}
.btn-danger-neo:disabled {
opacity: 0.45;
cursor: not-allowed;
background-color: transparent;
color: #DE6C56;
}
/* ── Cards ─────────────────────────────────────────────────────── */
.card-neo {
background-color: #FFFFFF;
border: 1px solid #111111;
border-radius: 0;
}
/* ── Inputs ────────────────────────────────────────────────────── */
.input-neo {
background-color: #FFFFFF;
border: 1px solid #111111;
color: #111111;
font-family: 'Space Mono', monospace;
border-radius: 0;
padding: 0.5rem 0.75rem;
font-size: 0.8125rem;
}
.input-neo::placeholder { color: #999999; }
.input-neo:focus {
outline: none;
border-width: 2px;
}
/* ── Footer Ticker ─────────────────────────────────────────────── */
.footer-ticker {
background-color: #111111;
color: #F2EFEB;
font-family: 'Space Mono', monospace;
font-size: 0.6875rem;
letter-spacing: 0.05em;
overflow: hidden;
white-space: nowrap;
padding: 0.625rem 0;
}
.footer-ticker a {
color: #4ADE80;
text-decoration: none;
}
.footer-ticker a:hover {
color: #F2EFEB;
text-decoration: underline;
}
</style>
</head>
<body class="min-h-screen flex flex-col">
<body class="min-h-screen flex flex-col bg-[#F2EFEB] text-[#111111]">
<header class="sticky top-0 z-50 glass-nav">
<!-- ── Navigation ─────────────────────────────────────────────────── -->
<header class="sticky top-0 z-50 bg-[#F2EFEB] border-b border-[#111111]">
<div class="container mx-auto px-4">
<div class="navbar px-0">
<div class="flex-1">
<a href="/" class="flex items-center gap-2 group">
<div class="bg-indigo-600 p-2 rounded-lg group-hover:bg-indigo-500 transition-colors">
<i data-lucide="video" class="w-5 h-5 text-white"></i>
</div>
<span class="text-xl font-bold tracking-tight text-white">VideoMakerBot</span>
</a>
</div>
<div class="flex-none gap-2">
<ul class="menu menu-horizontal px-1 gap-1">
<li><a href="/" class="rounded-lg hover:bg-white/10 text-slate-300">Library</a></li>
<li><a href="/backgrounds" class="rounded-lg hover:bg-white/10 text-slate-300">Backgrounds</a></li>
<li><a href="/settings" class="rounded-lg hover:bg-white/10 text-slate-300">Settings</a></li>
</ul>
<a href="/create" class="btn btn-indigo btn-sm ml-2 rounded-lg capitalize border-none bg-indigo-600 hover:bg-indigo-500 text-white">
<i data-lucide="plus" class="w-4 h-4"></i>
<div class="flex items-center justify-between h-14">
<!-- Logo -->
<a href="/" class="flex items-center gap-2 no-underline">
<span class="font-display text-lg text-[#111111] tracking-tighter uppercase">VideoMakerBot</span>
</a>
<!-- Nav Items -->
<div class="flex items-center gap-0">
<a href="/" class="nav-link">Library</a>
<a href="/backgrounds" class="nav-link">Backgrounds</a>
<a href="/settings" class="nav-link">Settings</a>
<span class="text-[#111111]/20 mx-1">///</span>
<a href="/create" class="btn-primary-neo text-sm py-1.5 px-4">
<i data-lucide="plus" class="w-3.5 h-3.5 inline mr-1"></i>
Create
</a>
</div>
@ -78,40 +333,38 @@
</div>
</header>
<!-- ── Flash Messages ─────────────────────────────────────────────── -->
{% if get_flashed_messages() %}
<div class="container mx-auto px-4 mt-4">
{% for category, message in get_flashed_messages(with_categories=true) %}
<div class="alert {{ 'alert-error' if category == 'error' else 'alert-success' }} shadow-lg mb-2">
<i data-lucide="{{ 'alert-circle' if category == 'error' else 'check-circle' }}" class="w-5 h-5"></i>
<div class="border px-4 py-3 mb-2 font-mono text-sm
{{ 'border-[#DE6C56] text-[#DE6C56] bg-[#DE6C56]/5' if category == 'error' else 'border-[#4ADE80] text-[#111111] bg-[#4ADE80]/10' }}">
<i data-lucide="{{ 'alert-circle' if category == 'error' else 'check-circle' }}" class="w-4 h-4 inline mr-2"></i>
<span>{{ message }}</span>
</div>
{% endfor %}
</div>
{% endif %}
<!-- ── Main Content ───────────────────────────────────────────────── -->
<main class="flex-grow">
{% block main %}{% endblock %}
</main>
<footer class="bg-slate-900 border-t border-white/5 py-12 mt-12">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between items-center gap-6">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<i data-lucide="video" class="w-5 h-5 text-indigo-500"></i>
<span class="text-lg font-semibold text-white">VideoMakerBot</span>
</div>
<p class="text-slate-400 text-sm">Automated short-form video creator.</p>
</div>
<div class="flex gap-6 text-sm text-slate-400">
<a href="https://github.com/elebumm/RedditVideoMakerBot" target="_blank" class="hover:text-white transition-colors">GitHub</a>
<a href="#" class="hover:text-white transition-colors" id="hard-reload">Hard Reload</a>
</div>
</div>
<div class="mt-8 pt-8 border-t border-white/5 text-center text-xs text-slate-500">
&copy; 2026 VideoMakerBot. Built for speed and creativity.
</div>
<!-- ── Footer Ticker ──────────────────────────────────────────────── -->
<footer class="footer-ticker border-t-2 border-[#111111]">
<div class="container mx-auto px-4 flex items-center gap-3 overflow-x-auto">
<span class="text-[#4ADE80] shrink-0">● ONLINE</span>
<span class="text-[#F2EFEB]/30 shrink-0">///</span>
<span class="shrink-0">VideoMakerBot</span>
<span class="text-[#F2EFEB]/30 shrink-0">///</span>
<span class="shrink-0">v3.4.0</span>
<span class="text-[#F2EFEB]/30 shrink-0">///</span>
<a href="https://github.com/elebumm/RedditVideoMakerBot" target="_blank" class="shrink-0">GITHUB</a>
<span class="text-[#F2EFEB]/30 shrink-0">///</span>
<a href="#" id="hard-reload" class="shrink-0">RELOAD</a>
<span class="text-[#F2EFEB]/30 shrink-0">///</span>
<span class="text-[#F2EFEB]/50 shrink-0">&copy; 2026</span>
</div>
</footer>

@ -1,18 +1,18 @@
{% extends "layout.html" %}
{% block main %}
<div class="bg-slate-900 min-h-screen py-8">
<div class="bg-[#F2EFEB] min-h-[calc(100vh-6.5rem)] py-8">
<div class="container mx-auto px-4 max-w-5xl">
<div class="flex flex-col md:flex-row justify-between items-end gap-4 mb-8">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Settings</h1>
<p class="text-slate-400">Configure your platform credentials and video generation preferences.</p>
<h1 class="text-3xl font-display font-black uppercase tracking-tighter text-[#111111] mb-2">Settings</h1>
<p class="text-[#111111]/50 font-mono text-sm">Configure platform credentials and video generation preferences.</p>
</div>
<div class="flex gap-2">
<button id="defaultSettingsBtn" type="button" class="btn btn-outline btn-sm text-slate-400 border-slate-700 hover:bg-slate-800">
<button id="defaultSettingsBtn" type="button" class="btn-ghost-neo text-sm">
Reset Defaults
</button>
<button form="settingsForm" type="submit" class="btn btn-indigo btn-sm bg-indigo-600 hover:bg-indigo-500 border-none text-white">
<button form="settingsForm" type="submit" class="btn-primary-neo text-sm">
Save Changes
</button>
</div>
@ -23,12 +23,12 @@
<!-- Navigation Tabs -->
<div class="lg:col-span-1">
<ul class="menu bg-slate-800/50 rounded-xl p-2 border border-white/5 sticky top-24" id="settingsTabs">
<li><a class="active flex gap-3 py-3" data-tab="platform"><i data-lucide="layout-template" class="w-4 h-4"></i> Platform</a></li>
<li><a class="flex gap-3 py-3" data-tab="content"><i data-lucide="file-text" class="w-4 h-4"></i> Content</a></li>
<li><a class="flex gap-3 py-3" data-tab="visuals"><i data-lucide="palette" class="w-4 h-4"></i> Visuals</a></li>
<li><a class="flex gap-3 py-3" data-tab="audio"><i data-lucide="volume-2" class="w-4 h-4"></i> Audio</a></li>
<li><a class="flex gap-3 py-3" data-tab="integration"><i data-lucide="share-2" class="w-4 h-4"></i> Integration</a></li>
<ul class="border border-[#111111] bg-white sticky top-24" id="settingsTabs">
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] border-b border-[#111111]/20 hover:bg-[#111111]/5 cursor-pointer active" data-tab="platform"><i data-lucide="layout-template" class="w-4 h-4"></i> Platform</a></li>
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] border-b border-[#111111]/20 hover:bg-[#111111]/5 cursor-pointer" data-tab="content"><i data-lucide="file-text" class="w-4 h-4"></i> Content</a></li>
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] border-b border-[#111111]/20 hover:bg-[#111111]/5 cursor-pointer" data-tab="visuals"><i data-lucide="palette" class="w-4 h-4"></i> Visuals</a></li>
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] border-b border-[#111111]/20 hover:bg-[#111111]/5 cursor-pointer" data-tab="audio"><i data-lucide="volume-2" class="w-4 h-4"></i> Audio</a></li>
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] hover:bg-[#111111]/5 cursor-pointer" data-tab="integration"><i data-lucide="share-2" class="w-4 h-4"></i> Integration</a></li>
</ul>
</div>
@ -37,69 +37,69 @@
<!-- Platform Tab -->
<div id="tab-platform" class="tab-pane">
<div class="card bg-slate-800 border border-white/5 shadow-xl">
<div class="card-body">
<h2 class="card-title text-white mb-6">Source Configuration</h2>
<div class="card-neo">
<div class="p-6">
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111] mb-6">Source Configuration</h2>
<div class="form-control w-full mb-8">
<label class="label"><span class="label-text text-slate-300 font-medium">Content Platform</span></label>
<select name="settings.platform" id="platformSelect" class="select select-bordered bg-slate-900 border-slate-700 focus:border-indigo-500">
<label class="label"><span class="label-text text-[#111111] font-mono text-xs uppercase tracking-wider font-medium">Content Platform</span></label>
<select name="settings.platform" id="platformSelect" class="input-neo w-full bg-white">
<option value="reddit">Reddit</option>
<option value="threads">Threads (Meta)</option>
</select>
<label class="label"><span class="label-text-alt text-slate-500">Which social media platform to pull content from</span></label>
<label class="label"><span class="label-text-alt text-[#111111]/40 font-mono text-xs">Which social media platform to pull content from</span></label>
</div>
<!-- Reddit Section -->
<div class="platform-section space-y-6" data-platform="reddit">
<div class="divider text-slate-500 text-xs uppercase tracking-widest">Reddit Credentials</div>
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2">/// Reddit Credentials</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Client ID</span></label>
<input name="reddit.creds.client_id" value="{{ data['reddit.creds.client_id'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="Your Client ID">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Client ID</span></label>
<input name="reddit.creds.client_id" value="{{ data['reddit.creds.client_id'] }}" type="text" class="input-neo w-full" placeholder="Your Client ID">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Client Secret</span></label>
<input name="reddit.creds.client_secret" value="{{ data['reddit.creds.client_secret'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="Your Client Secret">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Client Secret</span></label>
<input name="reddit.creds.client_secret" value="{{ data['reddit.creds.client_secret'] }}" type="text" class="input-neo w-full" placeholder="Your Client Secret">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Username</span></label>
<input name="reddit.creds.username" value="{{ data['reddit.creds.username'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="Reddit Username">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Username</span></label>
<input name="reddit.creds.username" value="{{ data['reddit.creds.username'] }}" type="text" class="input-neo w-full" placeholder="Reddit Username">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Password</span></label>
<input name="reddit.creds.password" value="{{ data['reddit.creds.password'] }}" type="password" class="input input-bordered bg-slate-900 border-slate-700" placeholder="Reddit Password">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Password</span></label>
<input name="reddit.creds.password" value="{{ data['reddit.creds.password'] }}" type="password" class="input-neo w-full" placeholder="Reddit Password">
</div>
</div>
<div class="flex items-center gap-4 bg-slate-900/50 p-4 rounded-lg border border-white/5">
<input name="reddit.creds.2fa" type="checkbox" class="toggle toggle-indigo" value="True">
<span class="text-sm text-slate-300">Enable 2FA Support</span>
<div class="flex items-center gap-4 bg-white border border-[#111111] p-4">
<input name="reddit.creds.2fa" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
<span class="text-sm text-[#111111] font-mono">Enable 2FA Support</span>
</div>
</div>
<!-- Threads Section -->
<div class="platform-section space-y-6 hidden" data-platform="threads">
<div class="divider text-slate-500 text-xs uppercase tracking-widest">Threads Configuration</div>
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2">/// Threads Configuration</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Discovery Method</span></label>
<select name="threads.discovery_method" class="select select-bordered bg-slate-900 border-slate-700">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Discovery Method</span></label>
<select name="threads.discovery_method" class="input-neo w-full bg-white">
<option value="api">API (Your own posts)</option>
<option value="scrape">Scrape (For You feed)</option>
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Threads Username</span></label>
<input name="threads.creds.username" value="{{ data['threads.creds.username'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="Username">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Threads Username</span></label>
<input name="threads.creds.username" value="{{ data['threads.creds.username'] }}" type="text" class="input-neo w-full" placeholder="Username">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Threads Password</span></label>
<input name="threads.creds.password" value="{{ data['threads.creds.password'] }}" type="password" class="input input-bordered bg-slate-900 border-slate-700" placeholder="Password">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Threads Password</span></label>
<input name="threads.creds.password" value="{{ data['threads.creds.password'] }}" type="password" class="input-neo w-full" placeholder="Password">
</div>
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Access Token</span></label>
<input name="threads.creds.access_token" value="{{ data['threads.creds.access_token'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="Long-lived Graph API Token">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Access Token</span></label>
<input name="threads.creds.access_token" value="{{ data['threads.creds.access_token'] }}" type="text" class="input-neo w-full" placeholder="Long-lived Graph API Token">
</div>
</div>
</div>
@ -108,64 +108,64 @@
<!-- Content Tab -->
<div id="tab-content" class="tab-pane hidden">
<div class="card bg-slate-800 border border-white/5 shadow-xl">
<div class="card-body space-y-6">
<h2 class="card-title text-white mb-2">Content Filtering</h2>
<div class="card-neo">
<div class="p-6 space-y-6">
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111] mb-2">Content Filtering</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Reddit Options -->
<div class="platform-section space-y-4" data-platform="reddit">
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Target Subreddit</span></label>
<input name="reddit.thread.subreddit" value="{{ data['reddit.thread.subreddit'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Target Subreddit</span></label>
<input name="reddit.thread.subreddit" value="{{ data['reddit.thread.subreddit'] }}" type="text" class="input-neo w-full">
</div>
<div class="form-control">
<label class="label"><span class="label-text text-slate-400">Max Comment Length: <span class="val-display font-mono text-indigo-400"></span></span></label>
<input name="reddit.thread.max_comment_length" type="range" min="100" max="1000" step="50" class="range range-xs range-indigo" value="{{ data['reddit.thread.max_comment_length'] }}">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Max Comment Length: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
<input name="reddit.thread.max_comment_length" type="range" min="100" max="1000" step="50" class="range range-xs accent-[#DE6C56]" value="{{ data['reddit.thread.max_comment_length'] }}">
</div>
<div class="form-control">
<label class="label"><span class="label-text text-slate-400">Min Comments: <span class="val-display font-mono text-indigo-400"></span></span></label>
<input name="reddit.thread.min_comments" type="range" min="1" max="100" step="1" class="range range-xs range-indigo" value="{{ data['reddit.thread.min_comments'] }}">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Min Comments: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
<input name="reddit.thread.min_comments" type="range" min="1" max="100" step="1" class="range range-xs accent-[#DE6C56]" value="{{ data['reddit.thread.min_comments'] }}">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Blocked Words</span></label>
<input name="reddit.thread.blocked_words" value="{{ data['reddit.thread.blocked_words'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="nsfw, spoiler, politics">
<label class="label"><span class="label-text-alt text-slate-500">Comma-separated. Posts and comments matching these words are skipped.</span></label>
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Blocked Words</span></label>
<input name="reddit.thread.blocked_words" value="{{ data['reddit.thread.blocked_words'] }}" type="text" class="input-neo w-full" placeholder="nsfw, spoiler, politics">
<label class="label"><span class="label-text-alt text-[#111111]/40 font-mono text-xs">Comma-separated. Matching posts and comments are skipped.</span></label>
</div>
</div>
<!-- Threads Options -->
<div class="platform-section hidden space-y-4" data-platform="threads">
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Search Queries</span></label>
<input name="threads.thread.search_queries" value="{{ data['threads.thread.search_queries'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="news,viral,stories">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Search Queries</span></label>
<input name="threads.thread.search_queries" value="{{ data['threads.thread.search_queries'] }}" type="text" class="input-neo w-full" placeholder="news,viral,stories">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Max Reply Length: <span class="val-display font-mono text-indigo-400"></span></span></label>
<input name="threads.thread.max_reply_length" type="range" min="100" max="1000" step="50" class="range range-xs range-indigo" value="{{ data['threads.thread.max_reply_length'] }}">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Max Reply Length: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
<input name="threads.thread.max_reply_length" type="range" min="100" max="1000" step="50" class="range range-xs accent-[#DE6C56]" value="{{ data['threads.thread.max_reply_length'] }}">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Min Replies: <span class="val-display font-mono text-indigo-400"></span></span></label>
<input name="threads.thread.min_replies" type="range" min="1" max="50" step="1" class="range range-xs range-indigo" value="{{ data['threads.thread.min_replies'] }}">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Min Replies: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
<input name="threads.thread.min_replies" type="range" min="1" max="50" step="1" class="range range-xs accent-[#DE6C56]" value="{{ data['threads.thread.min_replies'] }}">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Blocked Words</span></label>
<input name="threads.thread.blocked_words" value="{{ data['threads.thread.blocked_words'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="nsfw, spoiler, politics">
<label class="label"><span class="label-text-alt text-slate-500">Comma-separated. Posts and replies matching these words are skipped.</span></label>
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Blocked Words</span></label>
<input name="threads.thread.blocked_words" value="{{ data['threads.thread.blocked_words'] }}" type="text" class="input-neo w-full" placeholder="nsfw, spoiler, politics">
<label class="label"><span class="label-text-alt text-[#111111]/40 font-mono text-xs">Comma-separated. Matching posts and replies are skipped.</span></label>
</div>
</div>
</div>
<div class="divider border-white/5">General</div>
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2">/// General</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center gap-4 bg-slate-900/50 p-4 rounded-lg border border-white/5">
<input name="settings.allow_nsfw" type="checkbox" class="toggle toggle-error" value="True">
<span class="text-sm text-slate-300">Allow NSFW Content</span>
<div class="flex items-center gap-4 bg-white border border-[#111111] p-4">
<input name="settings.allow_nsfw" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
<span class="text-sm text-[#111111] font-mono">Allow NSFW Content</span>
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400 font-medium">Videos to generate: <span class="val-display font-mono text-indigo-400"></span></span></label>
<input name="settings.times_to_run" type="range" min="1" max="50" step="1" class="range range-xs range-indigo" value="{{ data['settings.times_to_run'] }}">
<label class="label"><span class="label-text text-[#111111] font-mono text-xs font-medium">Videos to generate: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
<input name="settings.times_to_run" type="range" min="1" max="50" step="1" class="range range-xs accent-[#DE6C56]" value="{{ data['settings.times_to_run'] }}">
</div>
</div>
</div>
@ -174,40 +174,40 @@
<!-- Visuals Tab -->
<div id="tab-visuals" class="tab-pane hidden">
<div class="card bg-slate-800 border border-white/5 shadow-xl">
<div class="card-body space-y-6">
<h2 class="card-title text-white">Visual Styling</h2>
<div class="card-neo">
<div class="p-6 space-y-6">
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111]">Visual Styling</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control">
<label class="label"><span class="label-text text-slate-400">Screenshot Theme</span></label>
<select name="settings.theme" class="select select-bordered bg-slate-900 border-slate-700">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Screenshot Theme</span></label>
<select name="settings.theme" class="input-neo w-full bg-white">
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="transparent">Transparent</option>
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text text-slate-400">Comment Opacity: <span class="val-display font-mono text-indigo-400"></span></span></label>
<input name="settings.opacity" type="range" min="0" max="1" step="0.05" class="range range-xs range-indigo" value="{{ data['settings.opacity'] }}">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Comment Opacity: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
<input name="settings.opacity" type="range" min="0" max="1" step="0.05" class="range range-xs accent-[#DE6C56]" value="{{ data['settings.opacity'] }}">
</div>
</div>
<div class="divider border-white/5">Backgrounds</div>
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2">/// Backgrounds</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400 font-medium">Video Background</span></label>
<select name="settings.background.background_video" class="select select-bordered bg-slate-900 border-slate-700">
<label class="label"><span class="label-text text-[#111111] font-mono text-xs font-medium">Video Background</span></label>
<select name="settings.background.background_video" class="input-neo w-full bg-white">
{% for background in checks["settings.background.background_video"]["options"] %}
<option value="{{background}}">{{ background or 'Random' }}</option>
{% endfor %}
</select>
<label class="label"><a href="/backgrounds" target="_blank" class="label-text-alt text-indigo-400 hover:text-indigo-300">Manage video files →</a></label>
<label class="label"><a href="/backgrounds" target="_blank" class="label-text-alt text-[#DE6C56] hover:text-[#111111] font-mono text-xs">Manage video files &rarr;</a></label>
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400 font-medium">Audio Track</span></label>
<select name="settings.background.background_audio" class="select select-bordered bg-slate-900 border-slate-700">
<label class="label"><span class="label-text text-[#111111] font-mono text-xs font-medium">Audio Track</span></label>
<select name="settings.background.background_audio" class="input-neo w-full bg-white">
{% for audio in checks["settings.background.background_audio"]["options"] %}
<option value="{{audio}}">{{ audio or 'None' }}</option>
{% endfor %}
@ -215,9 +215,9 @@
</div>
</div>
<div class="flex items-center gap-4 bg-slate-900/50 p-4 rounded-lg border border-white/5">
<input name="settings.background.background_thumbnail" type="checkbox" class="toggle toggle-indigo" value="True">
<span class="text-sm text-slate-300">Generate Thumbnail overlay</span>
<div class="flex items-center gap-4 bg-white border border-[#111111] p-4">
<input name="settings.background.background_thumbnail" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
<span class="text-sm text-[#111111] font-mono">Generate Thumbnail overlay</span>
</div>
</div>
</div>
@ -225,13 +225,13 @@
<!-- Audio Tab -->
<div id="tab-audio" class="tab-pane hidden">
<div class="card bg-slate-800 border border-white/5 shadow-xl">
<div class="card-body space-y-6">
<h2 class="card-title text-white">Voice & Speech</h2>
<div class="card-neo">
<div class="p-6 space-y-6">
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111]">Voice & Speech</h2>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400 font-medium">TTS Provider</span></label>
<select name="settings.tts.voice_choice" id="voiceChoiceSelect" class="select select-bordered bg-slate-900 border-slate-700">
<label class="label"><span class="label-text text-[#111111] font-mono text-xs font-medium">TTS Provider</span></label>
<select name="settings.tts.voice_choice" id="voiceChoiceSelect" class="input-neo w-full bg-white">
<option value="streamlabspolly">Streamlabs Polly (Free)</option>
<option value="tiktok">TikTok</option>
<option value="googletranslate">Google Translate</option>
@ -244,30 +244,30 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control">
<label class="label"><span class="label-text text-slate-400">Silence between comments: <span class="val-display font-mono text-indigo-400"></span>s</span></label>
<input name="settings.tts.silence_duration" type="range" min="0" max="5" step="0.1" class="range range-xs range-indigo" value="{{ data['settings.tts.silence_duration'] }}">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Silence between comments: <span class="val-display font-mono text-[#DE6C56]"></span>s</span></label>
<input name="settings.tts.silence_duration" type="range" min="0" max="5" step="0.1" class="range range-xs accent-[#DE6C56]" value="{{ data['settings.tts.silence_duration'] }}">
</div>
<div class="flex flex-col gap-3 justify-center">
<div class="flex items-center gap-4">
<input name="settings.tts.random_voice" type="checkbox" class="toggle toggle-indigo" value="True">
<span class="text-sm text-slate-300">Randomize voices</span>
<input name="settings.tts.random_voice" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
<span class="text-sm text-[#111111] font-mono">Randomize voices</span>
</div>
<div class="flex items-center gap-4">
<input name="settings.tts.no_emojis" type="checkbox" class="toggle toggle-indigo" value="True">
<span class="text-sm text-slate-300">Strip emojis</span>
<input name="settings.tts.no_emojis" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
<span class="text-sm text-[#111111] font-mono">Strip emojis</span>
</div>
</div>
</div>
<div class="divider border-white/5">API Credentials</div>
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2">/// API Credentials</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label"><span class="label-text text-slate-500">ElevenLabs Key</span></label>
<input name="settings.tts.elevenlabs_api_key" value="{{ data['settings.tts.elevenlabs_api_key'] }}" type="password" class="input input-bordered input-sm bg-slate-900 border-slate-700">
<label class="label"><span class="label-text text-[#111111]/50 font-mono text-xs">ElevenLabs Key</span></label>
<input name="settings.tts.elevenlabs_api_key" value="{{ data['settings.tts.elevenlabs_api_key'] }}" type="password" class="input-neo w-full text-sm">
</div>
<div class="form-control">
<label class="label"><span class="label-text text-slate-500">OpenAI Key</span></label>
<input name="settings.tts.openai_api_key" value="{{ data['settings.tts.openai_api_key'] }}" type="password" class="input input-bordered input-sm bg-slate-900 border-slate-700">
<label class="label"><span class="label-text text-[#111111]/50 font-mono text-xs">OpenAI Key</span></label>
<input name="settings.tts.openai_api_key" value="{{ data['settings.tts.openai_api_key'] }}" type="password" class="input-neo w-full text-sm">
</div>
</div>
</div>
@ -276,32 +276,32 @@
<!-- Integration Tab -->
<div id="tab-integration" class="tab-pane hidden">
<div class="card bg-slate-800 border border-white/5 shadow-xl">
<div class="card-body space-y-6">
<h2 class="card-title text-white">YouTube Integration</h2>
<div class="card-neo">
<div class="p-6 space-y-6">
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111]">YouTube Integration</h2>
<div class="flex items-center gap-4 bg-slate-900/50 p-4 rounded-lg border border-white/5">
<input name="youtube.enabled" type="checkbox" class="toggle toggle-success" value="True">
<span class="text-sm text-slate-300 font-medium">Auto-upload after rendering</span>
<div class="flex items-center gap-4 bg-white border border-[#111111] p-4">
<input name="youtube.enabled" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#4ADE80] [&:checked]:bg-[#4ADE80] [&:checked]:border-[#4ADE80]" value="True">
<span class="text-sm text-[#111111] font-mono font-medium">Auto-upload after rendering</span>
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-slate-400">Client Secret Path</span></label>
<input name="youtube.client_secret_path" value="{{ data['youtube.client_secret_path'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="path/to/secret.json">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Client Secret Path</span></label>
<input name="youtube.client_secret_path" value="{{ data['youtube.client_secret_path'] }}" type="text" class="input-neo w-full" placeholder="path/to/secret.json">
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-control">
<label class="label"><span class="label-text text-slate-400">Privacy</span></label>
<select name="youtube.privacy" class="select select-bordered bg-slate-900 border-slate-700">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Privacy</span></label>
<select name="youtube.privacy" class="input-neo w-full bg-white">
<option value="public">Public</option>
<option value="private">Private</option>
<option value="unlisted">Unlisted</option>
</select>
</div>
<div class="form-control col-span-2">
<label class="label"><span class="label-text text-slate-400">Tags (comma-separated)</span></label>
<input name="youtube.tags" value="{{ data['youtube.tags'] }}" type="text" class="input input-bordered bg-slate-900 border-slate-700" placeholder="shorts, reddit, viral">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Tags (comma-separated)</span></label>
<input name="youtube.tags" value="{{ data['youtube.tags'] }}" type="text" class="input-neo w-full" placeholder="shorts, reddit, viral">
</div>
</div>
</div>
@ -337,7 +337,6 @@
});
// ---- Form Initialization -------------------------------------------
// Set values for all inputs based on the flattened data object
form.querySelectorAll('input, select, textarea').forEach(input => {
const name = input.name;
if (data[name] !== undefined) {
@ -348,7 +347,6 @@
}
}
// Trigger input events for range displays
if (input.type === 'range') {
updateRangeDisplay(input);
input.addEventListener('input', () => updateRangeDisplay(input));
@ -381,11 +379,9 @@
let value = input.value;
let isValid = true;
// Optional/Empty check
if (value.length === 0) {
isValid = !!check.optional;
} else {
// Type specific checks
if (check.type === 'int' || check.type === 'float') {
const num = parseFloat(value);
if (check.nmin !== undefined && num < check.nmin) isValid = false;
@ -395,7 +391,6 @@
if (check.nmax !== undefined && value.length > check.nmax) isValid = false;
}
// Regex check
if (isValid && check.regex) {
const re = new RegExp(check.regex);
if (!re.test(value)) isValid = false;
@ -420,7 +415,6 @@
enabledInputs.forEach(input => {
if (!validateInput(input)) {
formIsValid = false;
// Switch to the tab containing the error
const pane = input.closest('.tab-pane');
if (pane) {
const tabBtn = document.querySelector(`[data-tab="${pane.id.replace('tab-', '')}"]`);
@ -434,7 +428,6 @@
return;
}
// Handle un-checked checkboxes (ensure they submit "False")
form.querySelectorAll('input[type="checkbox"]:not(:disabled)').forEach(cb => {
if (!cb.checked) {
const hidden = document.createElement('input');

Loading…
Cancel
Save