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

614 lines
47 KiB

{% extends "layout.html" %}
{% block main %}
<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-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-ghost-neo text-sm">
Reset Defaults
</button>
<button form="settingsForm" type="submit" class="btn-primary-neo text-sm">
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="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>
<!-- Tab Content -->
<div class="lg:col-span-3 space-y-6">
<!-- Platform Tab -->
<div id="tab-platform" class="tab-pane">
<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-[#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-[#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="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-[#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-[#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-[#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-[#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-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="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-[#111111]/60 font-mono text-xs">Discovery Method</span></label>
<select name="threads.discovery_method" id="threadsDiscoveryMethodSelect" 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="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2 threads-discovery-section hidden" data-discovery-methods="scrape" data-preserve-hidden="true">/// Threads Login</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 threads-discovery-section hidden" data-discovery-methods="scrape" data-preserve-hidden="true">
<div class="form-control w-full">
<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.get('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-[#111111]/60 font-mono text-xs">Threads Password</span></label>
<input name="threads.creds.password" value="{{ data.get('threads.creds.password', '') }}" type="password" class="input-neo w-full" placeholder="Password">
</div>
</div>
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2 threads-discovery-section hidden" data-discovery-methods="api">/// Threads API Credentials</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control w-full threads-discovery-section hidden" data-discovery-methods="api">
<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.get('threads.creds.access_token', '') }}" type="text" class="input-neo w-full" placeholder="Long-lived Graph API Token">
</div>
<div class="form-control w-full threads-discovery-section hidden" data-discovery-methods="api">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Threads User ID</span></label>
<input name="threads.creds.user_id" value="{{ data.get('threads.creds.user_id', '') }}" type="text" class="input-neo w-full" placeholder="12345678901234567">
</div>
</div>
<p class="text-[#111111]/50 font-mono text-xs leading-relaxed threads-discovery-section hidden" data-discovery-methods="api">
Screenshots still reuse your saved Threads username/password. Switch to Scrape if you need to edit those login credentials.
</p>
</div>
</div>
</div>
</div>
<!-- Content Tab -->
<div id="tab-content" class="tab-pane hidden">
<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-[#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-[#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-[#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-[#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-[#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-[#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-[#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-[#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="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-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-[#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>
</div>
</div>
<!-- Visuals Tab -->
<div id="tab-visuals" class="tab-pane hidden">
<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-[#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-[#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="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-[#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-[#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-[#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 %}
</select>
</div>
</div>
<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>
</div>
<!-- Audio Tab -->
<div id="tab-audio" class="tab-pane hidden">
<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-[#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">
{% for provider in checks["settings.tts.voice_choice"]["options"] %}
<option value="{{ provider }}">{% if provider == "streamlabspolly" %}Streamlabs Polly (Free){% elif provider == "googletranslate" %}Google Translate{% elif provider == "awspolly" %}AWS Polly{% elif provider == "pyttsx" %}System Voice (pyttsx){% else %}{{ provider }}{% endif %}</option>
{% endfor %}
</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-[#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-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-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="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2 tts-provider-section hidden" data-tts-providers="elevenlabs,OpenAI,tiktok">/// API Credentials</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control tts-provider-section hidden" data-tts-providers="elevenlabs">
<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.get('settings.tts.elevenlabs_api_key', '') }}" type="password" class="input-neo w-full text-sm">
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="OpenAI">
<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.get('settings.tts.openai_api_key', '') }}" type="password" class="input-neo w-full text-sm">
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="OpenAI">
<label class="label"><span class="label-text text-[#111111]/50 font-mono text-xs">OpenAI API URL</span></label>
<input name="settings.tts.openai_api_url" value="{{ data.get('settings.tts.openai_api_url', '') }}" type="text" class="input-neo w-full text-sm">
</div>
<div class="form-control tts-provider-section hidden md:col-span-2" data-tts-providers="tiktok">
<label class="label"><span class="label-text text-[#111111]/50 font-mono text-xs">TikTok Session ID</span></label>
<input name="settings.tts.tiktok_sessionid" value="{{ data.get('settings.tts.tiktok_sessionid', '') }}" type="password" class="input-neo w-full text-sm">
</div>
</div>
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2 tts-provider-section hidden" data-tts-providers="Supertonic,elevenlabs,OpenAI,streamlabspolly,awspolly,tiktok,pyttsx">/// Provider Settings</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control tts-provider-section hidden" data-tts-providers="Supertonic">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Supertonic Voice</span></label>
<select name="settings.tts.supertonic_voice" class="input-neo w-full bg-white">
{% for voice in checks["settings.tts.supertonic_voice"]["options"] %}
<option value="{{ voice }}">{{ voice }}</option>
{% endfor %}
</select>
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="Supertonic">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Language Code</span></label>
<input name="settings.tts.supertonic_lang" value="{{ data.get('settings.tts.supertonic_lang', '') }}" type="text" class="input-neo w-full text-sm" placeholder="na">
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="Supertonic">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Quality Steps: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
<input name="settings.tts.supertonic_steps" type="range" min="5" max="12" step="1" class="range range-xs accent-[#DE6C56]" value="{{ data.get('settings.tts.supertonic_steps', 8) }}">
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="Supertonic">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Playback Speed: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
<input name="settings.tts.supertonic_speed" type="range" min="0.7" max="2" step="0.05" class="range range-xs accent-[#DE6C56]" value="{{ data.get('settings.tts.supertonic_speed', 1.05) }}">
</div>
<div class="form-control tts-provider-section hidden md:col-span-2" data-tts-providers="Supertonic">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Custom Voice JSON Path</span></label>
<input name="settings.tts.supertonic_custom_voice_path" value="{{ data.get('settings.tts.supertonic_custom_voice_path', '') }}" type="text" class="input-neo w-full text-sm" placeholder="/app/voices/my-voice.json">
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="elevenlabs">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">ElevenLabs Voice</span></label>
<select name="settings.tts.elevenlabs_voice_name" class="input-neo w-full bg-white">
{% for voice in checks["settings.tts.elevenlabs_voice_name"]["options"] %}
<option value="{{ voice }}">{{ voice }}</option>
{% endfor %}
</select>
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="OpenAI">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">OpenAI Voice</span></label>
<select name="settings.tts.openai_voice_name" class="input-neo w-full bg-white">
{% for voice in checks["settings.tts.openai_voice_name"]["options"] %}
<option value="{{ voice }}">{{ voice }}</option>
{% endfor %}
</select>
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="OpenAI">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">OpenAI Model</span></label>
<select name="settings.tts.openai_model" class="input-neo w-full bg-white">
{% for model in checks["settings.tts.openai_model"]["options"] %}
<option value="{{ model }}">{{ model }}</option>
{% endfor %}
</select>
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="streamlabspolly">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Streamlabs Voice</span></label>
<input name="settings.tts.streamlabs_polly_voice" value="{{ data.get('settings.tts.streamlabs_polly_voice', '') }}" type="text" class="input-neo w-full text-sm">
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="awspolly">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">AWS Polly Voice</span></label>
<input name="settings.tts.aws_polly_voice" value="{{ data.get('settings.tts.aws_polly_voice', '') }}" type="text" class="input-neo w-full text-sm">
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="tiktok">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">TikTok Voice</span></label>
<input name="settings.tts.tiktok_voice" value="{{ data.get('settings.tts.tiktok_voice', '') }}" type="text" class="input-neo w-full text-sm">
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="pyttsx">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">System Voice Index</span></label>
<input name="settings.tts.python_voice" value="{{ data.get('settings.tts.python_voice', '') }}" type="text" class="input-neo w-full text-sm">
</div>
<div class="form-control tts-provider-section hidden" data-tts-providers="pyttsx">
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Installed Voice Count</span></label>
<input name="settings.tts.py_voice_num" value="{{ data.get('settings.tts.py_voice_num', '') }}" type="text" class="input-neo w-full text-sm">
</div>
</div>
</div>
</div>
</div>
<!-- Integration Tab -->
<div id="tab-integration" class="tab-pane hidden">
<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-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-[#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-[#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-[#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>
</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 -------------------------------------------
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];
}
} else if (validateChecks[name] && validateChecks[name].default !== undefined) {
if (input.type === 'checkbox') {
input.checked = (validateChecks[name].default === "True" || validateChecks[name].default === true);
} else {
input.value = validateChecks[name].default;
}
}
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');
const threadsDiscoveryMethodSelect = document.getElementById('threadsDiscoveryMethodSelect');
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);
});
}
function matchesDiscoveryMethod(section, method) {
return (section.dataset.discoveryMethods || '')
.split(',')
.map(value => value.trim().toLowerCase())
.filter(Boolean)
.includes((method || '').trim().toLowerCase());
}
function applyThreadsDiscoveryVisibility() {
const isThreads = (platformSelect.value || 'reddit') === 'threads';
const current = threadsDiscoveryMethodSelect?.value || 'api';
document.querySelectorAll('.threads-discovery-section').forEach(section => {
const matches = isThreads && matchesDiscoveryMethod(section, current);
const preserveHidden = section.dataset.preserveHidden === 'true';
section.classList.toggle('hidden', !matches);
section.querySelectorAll('input, select, textarea').forEach(el => {
el.disabled = !isThreads || (!matches && !preserveHidden);
});
});
}
platformSelect.addEventListener('change', applyPlatformVisibility);
platformSelect.addEventListener('change', applyThreadsDiscoveryVisibility);
threadsDiscoveryMethodSelect?.addEventListener('change', applyThreadsDiscoveryVisibility);
applyPlatformVisibility();
applyThreadsDiscoveryVisibility();
// ---- TTS Provider Visibility ---------------------------------------
const voiceChoiceSelect = document.getElementById('voiceChoiceSelect');
function matchesTtsProvider(section, provider) {
return (section.dataset.ttsProviders || '')
.split(',')
.map(value => value.trim().toLowerCase())
.filter(Boolean)
.includes((provider || '').trim().toLowerCase());
}
function applyTtsVisibility() {
const current = voiceChoiceSelect.value || 'Supertonic';
document.querySelectorAll('.tts-provider-section').forEach(section => {
const matches = matchesTtsProvider(section, current);
section.classList.toggle('hidden', !matches);
section.querySelectorAll('input, select, textarea').forEach(el => {
el.disabled = !matches;
});
});
}
voiceChoiceSelect.addEventListener('change', applyTtsVisibility);
applyTtsVisibility();
// ---- Validation ----------------------------------------------------
function validateInput(input) {
const check = validateChecks[input.name];
if (!check) return true;
let value = input.value;
let isValid = true;
if (value.length === 0) {
isValid = !!check.optional;
} else {
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;
}
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;
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;
}
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);
}
});
applyPlatformVisibility();
applyThreadsDiscoveryVisibility();
applyTtsVisibility();
});
lucide.createIcons();
});
</script>
{% endblock %}