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.
RedditVideoMakerBot/GUI/settings.html

472 lines
32 KiB

{% extends "layout.html" %}
{% block main %}
<div class="bg-slate-900 min-h-screen 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>
</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">
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">
Save Changes
</button>
</div>
</div>
<form id="settingsForm" action="/settings" method="post" novalidate>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- 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>
</div>
<!-- Tab Content -->
<div class="lg:col-span-3 space-y-6">
<!-- 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="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">
<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>
</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="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">
</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">
</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">
</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">
</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>
</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="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">
<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">
</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">
</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">
</div>
</div>
</div>
</div>
</div>
<!-- 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="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">
</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'] }}">
</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'] }}">
</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>
</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">
</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'] }}">
</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'] }}">
</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>
</div>
</div>
</div>
<div class="divider border-white/5">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>
<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'] }}">
</div>
</div>
</div>
</div>
</div>
<!-- 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="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">
<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'] }}">
</div>
</div>
<div class="divider border-white/5">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">
{% 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>
</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">
{% for audio in checks["settings.background.background_audio"]["options"] %}
<option value="{{audio}}">{{ audio or 'None' }}</option>
{% endfor %}
</select>
</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>
</div>
</div>
</div>
<!-- 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="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">
<option value="streamlabspolly">Streamlabs Polly (Free)</option>
<option value="tiktok">TikTok</option>
<option value="googletranslate">Google Translate</option>
<option value="awspolly">AWS Polly</option>
<option value="elevenlabs">ElevenLabs</option>
<option value="OpenAI">OpenAI</option>
<option value="pyttsx">System Voice (pyttsx)</option>
</select>
</div>
<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'] }}">
</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>
</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>
</div>
</div>
</div>
<div class="divider border-white/5">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">
</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">
</div>
</div>
</div>
</div>
</div>
<!-- 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="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>
<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">
</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">
<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">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const data = {{ data | tojson | safe }};
const validateChecks = {{ checks | tojson | safe }};
const form = document.getElementById('settingsForm');
// ---- Tab Switching -------------------------------------------------
const tabs = document.querySelectorAll('#settingsTabs a');
const panes = document.querySelectorAll('.tab-pane');
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
e.preventDefault();
const target = tab.dataset.tab;
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
panes.forEach(p => p.classList.toggle('hidden', p.id !== `tab-${target}`));
});
});
// ---- 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) {
if (input.type === 'checkbox') {
input.checked = (data[name] === "True" || data[name] === true);
} else {
input.value = data[name];
}
}
// Trigger input events for range displays
if (input.type === 'range') {
updateRangeDisplay(input);
input.addEventListener('input', () => updateRangeDisplay(input));
}
});
function updateRangeDisplay(input) {
const display = input.closest('.form-control')?.querySelector('.val-display');
if (display) display.textContent = input.value;
}
// ---- Platform Visibility -------------------------------------------
const platformSelect = document.getElementById('platformSelect');
function applyPlatformVisibility() {
const current = platformSelect.value || "reddit";
document.querySelectorAll('.platform-section').forEach(section => {
const matches = section.dataset.platform === current;
section.classList.toggle('hidden', !matches);
section.querySelectorAll('input, select, textarea').forEach(el => el.disabled = !matches);
});
}
platformSelect.addEventListener('change', applyPlatformVisibility);
applyPlatformVisibility();
// ---- Validation ----------------------------------------------------
function validateInput(input) {
const check = validateChecks[input.name];
if (!check) return true;
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;
if (check.nmax !== undefined && num > check.nmax) isValid = false;
} else {
if (check.nmin !== undefined && value.length < check.nmin) isValid = false;
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;
}
}
input.classList.toggle('input-error', !isValid);
input.classList.toggle('select-error', !isValid && input.tagName === 'SELECT');
return isValid;
}
form.querySelectorAll('input, select').forEach(input => {
input.addEventListener('change', () => validateInput(input));
if (input.tagName === 'INPUT') input.addEventListener('keyup', () => validateInput(input));
});
// ---- Submit Logic --------------------------------------------------
form.addEventListener('submit', (e) => {
let formIsValid = true;
const enabledInputs = form.querySelectorAll('input:not(:disabled), select:not(:disabled)');
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-', '')}"]`);
if (tabBtn) tabBtn.click();
}
}
});
if (!formIsValid) {
e.preventDefault();
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');
hidden.type = 'hidden';
hidden.name = cb.name;
hidden.value = 'False';
form.appendChild(hidden);
}
});
});
// ---- Defaults ------------------------------------------------------
document.getElementById('defaultSettingsBtn').addEventListener('click', () => {
if (!confirm('Are you sure you want to reset visible settings to defaults?')) return;
form.querySelectorAll('input:not(:disabled), select:not(:disabled)').forEach(input => {
const check = validateChecks[input.name];
if (check && check.default !== undefined) {
if (input.type === 'checkbox') {
input.checked = (check.default === "True" || check.default === true);
} else {
input.value = check.default;
}
if (input.type === 'range') updateRangeDisplay(input);
validateInput(input);
}
});
});
lucide.createIcons();
});
</script>
{% endblock %}