Initial clean commit
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Audio Settings - Quizzical Beats{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-navy-800 font-montserrat mb-2">Custom Audio Settings</h1>
|
||||
<p class="text-gray-600">Upload or generate your own intro, outro, and replay announcements for quizzes.</p>
|
||||
</div>
|
||||
|
||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
||||
<div class="mb-6 p-4 rounded {% if category == 'danger' %}bg-red-50 text-red-700 border border-red-300{% elif category == 'success' %}bg-green-50 text-green-700 border border-green-300{% else %}bg-blue-50 text-blue-700 border border-blue-300{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Intro Audio Section -->
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
|
||||
<div class="bg-navy-50 p-4 border-b">
|
||||
<h2 class="text-xl font-semibold text-navy-800">Intro Audio</h2>
|
||||
<p class="text-gray-600 text-sm mt-1">This audio plays at the beginning of your music quiz</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-2">Current Setting</h3>
|
||||
{% if current_user.intro_mp3 %}
|
||||
<div class="p-4 bg-navy-50 rounded-lg">
|
||||
<p class="font-medium text-navy-800 mb-2">Custom intro audio is active</p>
|
||||
<audio controls class="w-full">
|
||||
<source src="{{ url_for('static', filename='audio/intro.mp3') if not current_user.intro_mp3 else url_for('core.serve_user_audio', filepath=current_user.intro_mp3) }}" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
<form method="POST" class="mt-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="reset">
|
||||
<input type="hidden" name="mp3_type" value="intro">
|
||||
<button type="submit" class="text-red-600 text-sm hover:underline">Reset to default</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 bg-gray-50 rounded-lg">
|
||||
<p class="font-medium text-gray-700 mb-2">Using default intro audio</p>
|
||||
<audio controls class="w-full">
|
||||
<source src="{{ url_for('static', filename='audio/intro.mp3') }}" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-6">
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-4">Customize</h3>
|
||||
|
||||
<div class="mb-8">
|
||||
<h4 class="font-medium text-gray-700 mb-2">Option 1: Upload MP3</h4>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="upload">
|
||||
<input type="hidden" name="mp3_type" value="intro">
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-grow">
|
||||
<input type="file" name="audio_file" accept=".mp3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500">
|
||||
</div>
|
||||
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 rounded transition-colors">
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-700 mb-2">Option 2: Generate with Text-to-Speech</h4>
|
||||
{% if has_tts_services %}
|
||||
<form method="GET" class="mb-4 flex items-end space-x-2">
|
||||
<input type="hidden" name="mp3_type" value="intro">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">TTS Service</label>
|
||||
<select name="tts_service" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for svc in tts_services %}
|
||||
<option value="{{ svc.id }}" {% if selected_service and svc.id == selected_service.id %}selected{% endif %}>{{ svc.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-2 rounded">Change</button>
|
||||
</form>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="generate">
|
||||
<input type="hidden" name="mp3_type" value="intro">
|
||||
<input type="hidden" name="tts_service" value="{{ selected_service.id }}">
|
||||
<p class="mt-1 text-sm text-gray-500">{{ selected_service.description if selected_service else '' }}</p>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Voice</label>
|
||||
<select name="tts_voice" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for voice in selected_service.voices %}
|
||||
<option value="{{ voice.id }}">{{ voice.name }} ({{ voice.gender }}, {{ voice.language }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if selected_service.id == 'openai' and selected_service.models %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Model Quality</label>
|
||||
<select name="openai_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for model in selected_service.models %}
|
||||
<option value="{{ model.id }}">{{ model.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if selected_service.id == 'elevenlabs' and selected_service.models %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
|
||||
<select name="elevenlabs_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for model in selected_service.models %}
|
||||
<option value="{{ model.id }}">{{ model.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if selected_service.id == 'elevenlabs' and selected_service.settings %}
|
||||
<div class="mb-4 bg-gray-50 p-4 rounded-md">
|
||||
<h5 class="font-medium text-gray-700 mb-2">Voice Settings</h5>
|
||||
<div class="mb-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Stability <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
|
||||
<input type="range" name="stability" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>More variable</span>
|
||||
<span>More stable</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Similarity Boost <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
|
||||
<input type="range" name="similarity_boost" min="0" max="1" step="0.05" value="0.75" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>More unique</span>
|
||||
<span>More similar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Text to Convert</label>
|
||||
<textarea name="tts_text" rows="3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500"
|
||||
placeholder="Enter text for intro announcement...">{{ default_texts.intro }}</textarea>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded transition-colors">
|
||||
Generate Audio
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="p-4 bg-yellow-50 text-yellow-700 border border-yellow-300 rounded-md">
|
||||
<p>Text-to-speech generation requires API credentials (AWS Polly, OpenAI, or ElevenLabs). Contact your administrator to enable this feature.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outro Audio Section -->
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
|
||||
<div class="bg-navy-50 p-4 border-b">
|
||||
<h2 class="text-xl font-semibold text-navy-800">Outro Audio</h2>
|
||||
<p class="text-gray-600 text-sm mt-1">This audio plays at the end of your music quiz</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-2">Current Setting</h3>
|
||||
{% if current_user.outro_mp3 %}
|
||||
<div class="p-4 bg-navy-50 rounded-lg">
|
||||
<p class="font-medium text-navy-800 mb-2">Custom outro audio is active</p>
|
||||
<audio controls class="w-full">
|
||||
<source src="{{ url_for('static', filename='audio/outro.mp3') if not current_user.outro_mp3 else url_for('core.serve_user_audio', filepath=current_user.outro_mp3) }}" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
<form method="POST" class="mt-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="reset">
|
||||
<input type="hidden" name="mp3_type" value="outro">
|
||||
<button type="submit" class="text-red-600 text-sm hover:underline">Reset to default</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 bg-gray-50 rounded-lg">
|
||||
<p class="font-medium text-gray-700 mb-2">Using default outro audio</p>
|
||||
<audio controls class="w-full">
|
||||
<source src="{{ url_for('static', filename='audio/outro.mp3') }}" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-6">
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-4">Customize</h3>
|
||||
|
||||
<div class="mb-8">
|
||||
<h4 class="font-medium text-gray-700 mb-2">Option 1: Upload MP3</h4>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="upload">
|
||||
<input type="hidden" name="mp3_type" value="outro">
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-grow">
|
||||
<input type="file" name="audio_file" accept=".mp3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500">
|
||||
</div>
|
||||
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 rounded transition-colors">
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-700 mb-2">Option 2: Generate with Text-to-Speech</h4>
|
||||
{% if has_tts_services %}
|
||||
<form method="GET" class="mb-4 flex items-end space-x-2">
|
||||
<input type="hidden" name="mp3_type" value="outro">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">TTS Service</label>
|
||||
<select name="tts_service" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for svc in tts_services %}
|
||||
<option value="{{ svc.id }}" {% if selected_service and svc.id == selected_service.id %}selected{% endif %}>{{ svc.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-2 rounded">Change</button>
|
||||
</form>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="generate">
|
||||
<input type="hidden" name="mp3_type" value="outro">
|
||||
<input type="hidden" name="tts_service" value="{{ selected_service.id }}">
|
||||
<p class="mt-1 text-sm text-gray-500">{{ selected_service.description if selected_service else '' }}</p>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Voice</label>
|
||||
<select name="tts_voice" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for voice in selected_service.voices %}
|
||||
<option value="{{ voice.id }}">{{ voice.name }} ({{ voice.gender }}, {{ voice.language }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if selected_service.id == 'openai' and selected_service.models %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Model Quality</label>
|
||||
<select name="openai_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for model in selected_service.models %}
|
||||
<option value="{{ model.id }}">{{ model.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if selected_service.id == 'elevenlabs' and selected_service.models %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
|
||||
<select name="elevenlabs_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for model in selected_service.models %}
|
||||
<option value="{{ model.id }}">{{ model.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if selected_service.id == 'elevenlabs' and selected_service.settings %}
|
||||
<div class="mb-4 bg-gray-50 p-4 rounded-md">
|
||||
<h5 class="font-medium text-gray-700 mb-2">Voice Settings</h5>
|
||||
<div class="mb-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Stability <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
|
||||
<input type="range" name="stability" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>More variable</span>
|
||||
<span>More stable</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Similarity Boost <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
|
||||
<input type="range" name="similarity_boost" min="0" max="1" step="0.05" value="0.75" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>More unique</span>
|
||||
<span>More similar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Text to Convert</label>
|
||||
<textarea name="tts_text" rows="3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500"
|
||||
placeholder="Enter text for outro announcement...">{{ default_texts.outro }}</textarea>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded transition-colors">
|
||||
Generate Audio
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="p-4 bg-yellow-50 text-yellow-700 border border-yellow-300 rounded-md">
|
||||
<p>Text-to-speech generation requires API credentials (AWS Polly, OpenAI, or ElevenLabs). Contact your administrator to enable this feature.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Replay Audio Section -->
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
|
||||
<div class="bg-navy-50 p-4 border-b">
|
||||
<h2 class="text-xl font-semibold text-navy-800">Replay Audio</h2>
|
||||
<p class="text-gray-600 text-sm mt-1">This audio plays before replaying all songs at the end of the quiz</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-2">Current Setting</h3>
|
||||
{% if current_user.replay_mp3 %}
|
||||
<div class="p-4 bg-navy-50 rounded-lg">
|
||||
<p class="font-medium text-navy-800 mb-2">Custom replay audio is active</p>
|
||||
<audio controls class="w-full">
|
||||
<source src="{{ url_for('static', filename='audio/replay.mp3') if not current_user.replay_mp3 else url_for('core.serve_user_audio', filepath=current_user.replay_mp3) }}" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<form method="POST" class="mt-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="reset">
|
||||
<input type="hidden" name="mp3_type" value="replay">
|
||||
<button type="submit" class="text-red-600 text-sm hover:underline">Reset to default</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 bg-gray-50 rounded-lg">
|
||||
<p class="font-medium text-gray-700 mb-2">Using default replay audio</p>
|
||||
<audio controls class="w-full">
|
||||
<source src="{{ url_for('static', filename='audio/replay.mp3') }}" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="border-t pt-6">
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-4">Customize</h3>
|
||||
<div class="mb-8">
|
||||
<h4 class="font-medium text-gray-700 mb-2">Option 1: Upload MP3</h4>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="upload">
|
||||
<input type="hidden" name="mp3_type" value="replay">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-grow">
|
||||
<input type="file" name="audio_file" accept=".mp3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500">
|
||||
</div>
|
||||
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 rounded transition-colors">
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-700 mb-2">Option 2: Generate with Text-to-Speech</h4>
|
||||
{% if has_tts_services %}
|
||||
<form method="GET" class="mb-4 flex items-end space-x-2">
|
||||
<input type="hidden" name="mp3_type" value="replay">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">TTS Service</label>
|
||||
<select name="tts_service" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for svc in tts_services %}
|
||||
<option value="{{ svc.id }}" {% if selected_service and svc.id == selected_service.id %}selected{% endif %}>{{ svc.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-2 rounded">Change</button>
|
||||
</form>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="generate">
|
||||
<input type="hidden" name="mp3_type" value="replay">
|
||||
<input type="hidden" name="tts_service" value="{{ selected_service.id }}">
|
||||
<p class="mt-1 text-sm text-gray-500">{{ selected_service.description if selected_service else '' }}</p>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Voice</label>
|
||||
<select name="tts_voice" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for voice in selected_service.voices %}
|
||||
<option value="{{ voice.id }}">{{ voice.name }} ({{ voice.gender }}, {{ voice.language }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if selected_service.id == 'openai' and selected_service.models %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Model Quality</label>
|
||||
<select name="openai_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for model in selected_service.models %}
|
||||
<option value="{{ model.id }}">{{ model.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if selected_service.id == 'elevenlabs' and selected_service.models %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
|
||||
<select name="elevenlabs_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
|
||||
{% for model in selected_service.models %}
|
||||
<option value="{{ model.id }}">{{ model.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if selected_service.id == 'elevenlabs' and selected_service.settings %}
|
||||
<div class="mb-4 bg-gray-50 p-4 rounded-md">
|
||||
<h5 class="font-medium text-gray-700 mb-2">Voice Settings</h5>
|
||||
<div class="mb-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Stability <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
|
||||
<input type="range" name="stability" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>More variable</span>
|
||||
<span>More stable</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Similarity Boost <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
|
||||
<input type="range" name="similarity_boost" min="0" max="1" step="0.05" value="0.75" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>More unique</span>
|
||||
<span>More similar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Text to Convert</label>
|
||||
<textarea name="tts_text" rows="3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500"
|
||||
placeholder="Enter text for replay announcement...">{{ default_texts.replay }}</textarea>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded transition-colors">
|
||||
Generate Audio
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="p-4 bg-yellow-50 text-yellow-700 border border-yellow-300 rounded-md">
|
||||
<p>Text-to-speech generation requires API credentials (AWS Polly, OpenAI, or ElevenLabs). Contact your administrator to enable this feature.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-center">
|
||||
<a href="{{ url_for('users.profile') }}" class="inline-block bg-gray-500 hover:bg-gray-600 text-white py-2 px-4 rounded transition-colors">
|
||||
Back to Profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<!-- No Alpine.js needed -->
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Change Password{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-6 text-navy-800">Change Password</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-3 {% if category == 'danger' %}bg-red-100 text-red-700{% elif category == 'success' %}bg-green-100 text-green-700{% else %}bg-blue-100 text-blue-700{% endif %} rounded">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('users.change_password') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="current_password">
|
||||
Current Password
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="current_password" name="current_password" type="password" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="new_password">
|
||||
New Password
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="new_password" name="new_password" type="password" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="confirm_password">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="confirm_password" name="confirm_password" type="password" required>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<button class="bg-teal-500 hover:bg-teal-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit">
|
||||
Change Password
|
||||
</button>
|
||||
<a class="inline-block align-baseline font-bold text-sm text-teal-500 hover:text-teal-800" href="{{ url_for('users.profile') }}">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,566 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Edit Profile{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-6 text-navy-800">Edit Profile</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-3 {% if category == 'danger' %}bg-red-100 text-red-700{% elif category == 'success' %}bg-green-100 text-green-700{% else %}bg-blue-100 text-blue-700{% endif %} rounded">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('users.edit_profile') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
|
||||
Username
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="username" name="username" type="text" placeholder="Username" value="{{ current_user.username }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="email">
|
||||
Email
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="email" name="email" type="email" placeholder="Email address" value="{{ current_user.email }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="first_name">
|
||||
First Name
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="first_name" name="first_name" type="text" placeholder="First name" value="{{ current_user.first_name or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="last_name">
|
||||
Last Name
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="last_name" name="last_name" type="text" placeholder="Last name" value="{{ current_user.last_name or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="dropbox_export_path">
|
||||
Dropbox Export Folder
|
||||
</label>
|
||||
<div class="flex space-x-2">
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="dropbox_export_path" name="dropbox_export_path" type="text" placeholder="/QuizzicalBeats"
|
||||
value="{{ current_user.dropbox_export_path or '/QuizzicalBeats' }}">
|
||||
<button type="button" id="browse-dropbox-btn"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded flex items-center"
|
||||
{% if not current_user.dropbox_token %}disabled{% endif %}>
|
||||
<i class="fab fa-dropbox mr-1"></i> Browse
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">Set the Dropbox folder where your exported rounds will be saved.</p>
|
||||
{% if not current_user.dropbox_token %}
|
||||
<p class="text-xs text-red-500 mt-1">
|
||||
<i class="fas fa-exclamation-circle mr-1"></i>
|
||||
<a href="{{ url_for('users.dropbox_auth') }}" class="underline">Connect your Dropbox account</a> to browse folders.
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id="dropbox-error-container" class="hidden mt-3 p-3 bg-red-100 text-sm rounded">
|
||||
<div class="flex justify-between">
|
||||
<h4 class="font-semibold mb-1">Dropbox API Error</h4>
|
||||
<button id="close-error-btn" class="text-red-700">×</button>
|
||||
</div>
|
||||
<div id="dropbox-error-details"></div>
|
||||
<div class="mt-2">
|
||||
<button id="dropbox-debug-btn" type="button" class="text-xs bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded">
|
||||
Show Technical Details
|
||||
</button>
|
||||
<div id="dropbox-debug-info" class="hidden mt-2 bg-gray-100 p-2 rounded font-mono text-xs overflow-x-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<button class="bg-teal-500 hover:bg-teal-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit">
|
||||
Save Changes
|
||||
</button>
|
||||
<a class="inline-block align-baseline font-bold text-sm text-teal-500 hover:text-teal-800" href="{{ url_for('users.profile') }}">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Dropbox Folder Browser Modal -->
|
||||
<div id="dropbox-folder-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-white rounded-lg shadow-lg w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 class="text-lg font-bold text-navy-800">Select Dropbox Folder</h3>
|
||||
<button id="close-dropbox-modal" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-b border-gray-200 flex items-center bg-gray-50">
|
||||
<div class="flex-1 flex items-center">
|
||||
<button id="parent-folder-btn" class="bg-gray-200 hover:bg-gray-300 rounded p-1 mr-2">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
</button>
|
||||
<span id="current-path" class="text-gray-600 font-mono text-sm">/</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button id="create-folder-btn" class="bg-teal-500 hover:bg-teal-600 text-white rounded p-1 px-2 text-sm flex items-center">
|
||||
<i class="fas fa-folder-plus mr-1"></i> Create Folder
|
||||
</button>
|
||||
<button id="refresh-folders-btn" class="bg-gray-200 hover:bg-gray-300 rounded p-1">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto flex-1 p-2" style="max-height: 60vh;">
|
||||
<div id="folder-list" class="space-y-1">
|
||||
<div class="text-center p-8">
|
||||
<div class="animate-spin inline-block w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full mb-2"></div>
|
||||
<p>Loading folders...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-2">
|
||||
<button id="select-folder-btn" class="bg-teal-500 hover:bg-teal-600 text-white px-4 py-2 rounded">
|
||||
Select This Folder
|
||||
</button>
|
||||
<button id="cancel-selection-btn" class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Folder Modal -->
|
||||
<div id="create-folder-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-white rounded-lg shadow-lg w-full max-w-md">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-bold text-navy-800">Create New Folder</h3>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="new-folder-name">
|
||||
Folder Name
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="new-folder-name" type="text" placeholder="My New Folder">
|
||||
<p id="folder-name-error" class="hidden mt-1 text-xs text-red-500"></p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2">
|
||||
Parent Folder
|
||||
</label>
|
||||
<p class="py-2 px-3 bg-gray-100 rounded text-gray-700 font-mono text-sm" id="parent-folder-path"></p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-2 mt-6">
|
||||
<button id="create-folder-submit" class="bg-teal-500 hover:bg-teal-600 text-white px-4 py-2 rounded">
|
||||
Create
|
||||
</button>
|
||||
<button id="create-folder-cancel" class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const browseDropboxBtn = document.getElementById('browse-dropbox-btn');
|
||||
const dropboxFolderModal = document.getElementById('dropbox-folder-modal');
|
||||
const closeDropboxModal = document.getElementById('close-dropbox-modal');
|
||||
const cancelSelectionBtn = document.getElementById('cancel-selection-btn');
|
||||
const selectFolderBtn = document.getElementById('select-folder-btn');
|
||||
const folderList = document.getElementById('folder-list');
|
||||
const currentPathDisplay = document.getElementById('current-path');
|
||||
const parentFolderBtn = document.getElementById('parent-folder-btn');
|
||||
const refreshFoldersBtn = document.getElementById('refresh-folders-btn');
|
||||
const dropboxExportPath = document.getElementById('dropbox_export_path');
|
||||
const createFolderBtn = document.getElementById('create-folder-btn');
|
||||
const createFolderModal = document.getElementById('create-folder-modal');
|
||||
const createFolderSubmit = document.getElementById('create-folder-submit');
|
||||
const createFolderCancel = document.getElementById('create-folder-cancel');
|
||||
const newFolderNameInput = document.getElementById('new-folder-name');
|
||||
const folderNameError = document.getElementById('folder-name-error');
|
||||
const parentFolderPathDisplay = document.getElementById('parent-folder-path');
|
||||
|
||||
// Error display elements
|
||||
const errorContainer = document.getElementById('dropbox-error-container');
|
||||
const errorDetails = document.getElementById('dropbox-error-details');
|
||||
const closeErrorBtn = document.getElementById('close-error-btn');
|
||||
const debugBtn = document.getElementById('dropbox-debug-btn');
|
||||
const debugInfo = document.getElementById('dropbox-debug-info');
|
||||
|
||||
let currentPath = '';
|
||||
let lastApiResponse = null;
|
||||
|
||||
// Open the Dropbox folder browser modal
|
||||
browseDropboxBtn.addEventListener('click', function() {
|
||||
dropboxFolderModal.classList.remove('hidden');
|
||||
// Always start at root when opening the browser
|
||||
currentPath = '/';
|
||||
loadFolders(currentPath);
|
||||
});
|
||||
|
||||
// Close the modal when clicking close button or cancel
|
||||
closeDropboxModal.addEventListener('click', closeModal);
|
||||
cancelSelectionBtn.addEventListener('click', closeModal);
|
||||
|
||||
// Close modal when clicking outside
|
||||
dropboxFolderModal.addEventListener('click', function(event) {
|
||||
if (event.target === dropboxFolderModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close error display
|
||||
closeErrorBtn.addEventListener('click', function() {
|
||||
errorContainer.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Toggle debug info
|
||||
debugBtn.addEventListener('click', function() {
|
||||
if (debugInfo.classList.contains('hidden')) {
|
||||
debugInfo.classList.remove('hidden');
|
||||
debugBtn.textContent = 'Hide Technical Details';
|
||||
} else {
|
||||
debugInfo.classList.add('hidden');
|
||||
debugBtn.textContent = 'Show Technical Details';
|
||||
}
|
||||
});
|
||||
|
||||
// Select the current folder
|
||||
selectFolderBtn.addEventListener('click', function() {
|
||||
dropboxExportPath.value = currentPath;
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Navigate to parent folder
|
||||
parentFolderBtn.addEventListener('click', function() {
|
||||
if (currentPath === '/' || currentPath === '') {
|
||||
return; // Already at root
|
||||
}
|
||||
|
||||
// Get parent path
|
||||
const parts = currentPath.split('/').filter(p => p);
|
||||
parts.pop(); // Remove last folder
|
||||
|
||||
const parentPath = '/' + parts.join('/');
|
||||
loadFolders(parentPath);
|
||||
});
|
||||
|
||||
// Refresh current folder
|
||||
refreshFoldersBtn.addEventListener('click', function() {
|
||||
loadFolders(currentPath);
|
||||
});
|
||||
|
||||
// Open the Create Folder modal
|
||||
createFolderBtn.addEventListener('click', function() {
|
||||
createFolderModal.classList.remove('hidden');
|
||||
parentFolderPathDisplay.textContent = currentPath;
|
||||
newFolderNameInput.value = '';
|
||||
folderNameError.classList.add('hidden');
|
||||
|
||||
// Focus the input field for better UX
|
||||
setTimeout(() => {
|
||||
newFolderNameInput.focus();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Close the Create Folder modal when clicking outside
|
||||
createFolderModal.addEventListener('click', function(event) {
|
||||
if (event.target === createFolderModal) {
|
||||
createFolderModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Close the Create Folder modal
|
||||
createFolderCancel.addEventListener('click', function() {
|
||||
createFolderModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Handle Enter key in the folder name input
|
||||
newFolderNameInput.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
createFolderSubmit.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Submit new folder creation
|
||||
createFolderSubmit.addEventListener('click', function() {
|
||||
const folderName = newFolderNameInput.value.trim();
|
||||
if (!folderName) {
|
||||
folderNameError.textContent = 'Folder name cannot be empty.';
|
||||
folderNameError.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for invalid characters
|
||||
if (/[\/\\:*?"<>|]/.test(folderName)) {
|
||||
folderNameError.textContent = 'Folder name contains invalid characters.';
|
||||
folderNameError.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
createFolderSubmit.disabled = true;
|
||||
createFolderSubmit.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Creating...';
|
||||
folderNameError.classList.add('hidden');
|
||||
|
||||
// Call API to create folder
|
||||
fetch('/api/dropbox/create-folder', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parent_path: currentPath,
|
||||
folder_name: folderName
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(errorData => {
|
||||
throw {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: errorData
|
||||
};
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
throw {
|
||||
status: 500,
|
||||
statusText: 'Server Error',
|
||||
data: { error: data.error }
|
||||
};
|
||||
}
|
||||
|
||||
// Close modal and refresh folder list
|
||||
createFolderModal.classList.add('hidden');
|
||||
|
||||
// If the folder was created successfully, navigate to it
|
||||
if (data.path) {
|
||||
loadFolders(data.path);
|
||||
} else {
|
||||
// Just refresh the current folder
|
||||
loadFolders(currentPath);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.className = 'p-2 mb-3 bg-green-100 text-green-800 rounded text-sm';
|
||||
successDiv.textContent = `Folder "${folderName}" created successfully.`;
|
||||
successDiv.style.opacity = '1';
|
||||
successDiv.style.transition = 'opacity 0.5s ease-in-out';
|
||||
folderList.insertBefore(successDiv, folderList.firstChild);
|
||||
|
||||
// Fade out success message after 3 seconds
|
||||
setTimeout(() => {
|
||||
successDiv.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (successDiv.parentNode) {
|
||||
successDiv.parentNode.removeChild(successDiv);
|
||||
}
|
||||
}, 500);
|
||||
}, 3000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error creating folder:', error);
|
||||
|
||||
// Show error message in the modal
|
||||
folderNameError.textContent = error.data?.error ||
|
||||
(error.status === 409 ? 'A folder with this name already exists.' :
|
||||
error.statusText || 'Error creating folder');
|
||||
folderNameError.classList.remove('hidden');
|
||||
|
||||
// Also show detailed error in the main error display if it's a server error
|
||||
if (error.status >= 500) {
|
||||
showError(error);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
// Reset button state
|
||||
createFolderSubmit.disabled = false;
|
||||
createFolderSubmit.innerHTML = 'Create';
|
||||
});
|
||||
});
|
||||
|
||||
// Function to load folders from Dropbox
|
||||
function loadFolders(path) {
|
||||
// Hide any previous errors
|
||||
errorContainer.classList.add('hidden');
|
||||
|
||||
// Update UI
|
||||
currentPath = path;
|
||||
currentPathDisplay.textContent = path || '/';
|
||||
|
||||
// Show loading indicator
|
||||
folderList.innerHTML = `
|
||||
<div class="text-center p-8">
|
||||
<div class="animate-spin inline-block w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full mb-2"></div>
|
||||
<p>Loading folders...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Make API request to get folders
|
||||
fetch('/api/dropbox/folders?path=' + encodeURIComponent(path))
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(errorData => {
|
||||
throw {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: errorData
|
||||
};
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Save the response for debugging
|
||||
lastApiResponse = data;
|
||||
|
||||
if (data.error) {
|
||||
throw {
|
||||
status: 500,
|
||||
statusText: 'Server Error',
|
||||
data: data
|
||||
};
|
||||
}
|
||||
|
||||
// Check if there's a warning message (returned when a path doesn't exist)
|
||||
if (data.warning) {
|
||||
// Display warning message
|
||||
const warningDiv = document.createElement('div');
|
||||
warningDiv.className = 'p-2 mb-3 bg-yellow-100 text-yellow-800 rounded text-sm';
|
||||
warningDiv.textContent = data.warning;
|
||||
folderList.innerHTML = '';
|
||||
folderList.appendChild(warningDiv);
|
||||
|
||||
// Update the current path
|
||||
currentPath = data.path;
|
||||
currentPathDisplay.textContent = data.path;
|
||||
} else {
|
||||
// Clear folder list
|
||||
folderList.innerHTML = '';
|
||||
}
|
||||
|
||||
if (data.folders.length === 0) {
|
||||
const emptyDiv = document.createElement('div');
|
||||
emptyDiv.className = 'text-center p-8 text-gray-500';
|
||||
emptyDiv.innerHTML = `
|
||||
<i class="fas fa-folder-open text-4xl mb-2"></i>
|
||||
<p>No folders found in this location</p>
|
||||
`;
|
||||
folderList.appendChild(emptyDiv);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort folders alphabetically
|
||||
data.folders.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Add each folder to the list
|
||||
data.folders.forEach(folder => {
|
||||
const folderItem = document.createElement('div');
|
||||
folderItem.className = 'p-2 hover:bg-gray-100 rounded cursor-pointer flex items-center';
|
||||
folderItem.innerHTML = `
|
||||
<i class="fas fa-folder text-blue-500 mr-2"></i>
|
||||
<span>${folder.name}</span>
|
||||
`;
|
||||
|
||||
folderItem.addEventListener('click', function() {
|
||||
loadFolders(folder.path_display);
|
||||
});
|
||||
|
||||
folderList.appendChild(folderItem);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading folders:', error);
|
||||
|
||||
// Display error in the folder list
|
||||
folderList.innerHTML = `
|
||||
<div class="text-center p-8 text-red-500">
|
||||
<i class="fas fa-exclamation-circle text-4xl mb-2"></i>
|
||||
<p>${error.data?.error || error.statusText || 'Error loading folders'}</p>
|
||||
<p class="text-sm mt-2">Make sure your Dropbox account is connected</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Also show detailed error in the profile page
|
||||
showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
function showError(error) {
|
||||
// Format and display the error information
|
||||
errorContainer.classList.remove('hidden');
|
||||
|
||||
let errorMessage = '';
|
||||
if (error.status) {
|
||||
errorMessage += `HTTP ${error.status}: `;
|
||||
}
|
||||
|
||||
if (error.data && error.data.error) {
|
||||
errorMessage += error.data.error;
|
||||
} else if (error.statusText) {
|
||||
errorMessage += error.statusText;
|
||||
} else {
|
||||
errorMessage += 'Unknown error occurred';
|
||||
}
|
||||
|
||||
errorDetails.textContent = errorMessage;
|
||||
|
||||
// Show technical details
|
||||
let debugText = '';
|
||||
if (error.data) {
|
||||
if (error.data.details) {
|
||||
debugText += "Error Details:\n" + JSON.stringify(error.data.details, null, 2) + "\n\n";
|
||||
}
|
||||
if (error.data.traceback) {
|
||||
debugText += "Traceback:\n" + error.data.traceback + "\n\n";
|
||||
}
|
||||
if (error.data.raw_response) {
|
||||
debugText += "Raw Response:\n" + error.data.raw_response + "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!debugText) {
|
||||
debugText = JSON.stringify(error, null, 2);
|
||||
}
|
||||
|
||||
debugInfo.textContent = debugText;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
dropboxFolderModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Forgot Password - Quizzical Beats{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center">
|
||||
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-navy-800 font-montserrat">
|
||||
Forgot Password
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
Enter your email address and we'll send a reset link
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 bg-white py-8 px-6 shadow-md rounded-lg">
|
||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
||||
<div class="mb-4 p-4 rounded-md {% if category == 'danger' %}bg-red-50 text-red-700{% elif category == 'success' %}bg-green-50 text-green-700{% else %}bg-blue-50 text-blue-700{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<form class="space-y-6" action="{{ url_for('users.forgot_password') }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email address</label>
|
||||
<div class="mt-1">
|
||||
<input id="email" name="email" type="email" autocomplete="email" required
|
||||
class="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-orange-500 focus:border-orange-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500">
|
||||
Send Password Reset Link
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('users.login') }}" class="text-sm text-orange-600 hover:text-orange-500">
|
||||
Remember your password? Log in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4 text-sm text-gray-500">
|
||||
<p>Need help? Contact support</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,92 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-6 text-navy-800">Login to Quizzical Beats</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-3 {% if category == 'danger' %}bg-red-100 text-red-700{% elif category == 'success' %}bg-green-100 text-green-700{% else %}bg-blue-100 text-blue-700{% endif %} rounded">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('users.login') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
|
||||
Username or Email
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="username" name="username" type="text" placeholder="Enter your username or email">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">
|
||||
Password
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password" name="password" type="password" placeholder="Enter your password">
|
||||
</div>
|
||||
|
||||
<div class="mb-6 flex items-center">
|
||||
<input class="mr-2" type="checkbox" id="remember" name="remember">
|
||||
<label class="text-sm text-gray-700" for="remember">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<button class="bg-teal-500 hover:bg-teal-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit">
|
||||
Login
|
||||
</button>
|
||||
<a class="inline-block align-baseline font-bold text-sm text-teal-500 hover:text-teal-800" href="{{ url_for('users.forgot_password') }}">
|
||||
Forgot Password?
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Social Login Options -->
|
||||
{% if oauth_providers and (oauth_providers.google or oauth_providers.authentik) %}
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="flex flex-col space-y-3">
|
||||
{% if oauth_providers.google %}
|
||||
<a href="{{ url_for('users.google_login') }}"
|
||||
class="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||
<svg class="h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||
<path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"></path>
|
||||
<path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"></path>
|
||||
<path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"></path>
|
||||
<path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"></path>
|
||||
</svg>
|
||||
Sign in with Google
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if oauth_providers.authentik %}
|
||||
<a href="{{ url_for('users.authentik_login') }}"
|
||||
class="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||
<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik.svg" class="h-5 w-5 mr-2" alt="Authentik logo" />
|
||||
Sign in with Authentik
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-gray-200 text-center">
|
||||
<p class="text-gray-700">
|
||||
Don't have an account?
|
||||
<a href="{{ url_for('users.register') }}" class="font-bold text-teal-500 hover:text-teal-800">
|
||||
Register
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,832 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}My Profile{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-6 text-navy-800">My Profile</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-3 {% if category == 'danger' %}bg-red-100 text-red-700{% elif category == 'success' %}bg-green-100 text-green-700{% else %}bg-blue-100 text-blue-700{% endif %} rounded">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Profile Info -->
|
||||
<div class="md:col-span-2">
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-6">
|
||||
<h3 class="text-lg font-semibold mb-3 text-navy-700">Account Information</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Username</p>
|
||||
<p class="font-medium">{{ current_user.username }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Email</p>
|
||||
<p class="font-medium">{{ current_user.email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Name</p>
|
||||
<p class="font-medium">
|
||||
{% if current_user.first_name or current_user.last_name %}
|
||||
{{ current_user.first_name }} {{ current_user.last_name }}
|
||||
{% else %}
|
||||
<span class="text-gray-400">Not provided</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Member Since</p>
|
||||
<p class="font-medium">{{ current_user.created_at.strftime('%B %d, %Y') }}</p>
|
||||
</div>
|
||||
<!-- Add Admin Status -->
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Admin Status</p>
|
||||
<p class="font-medium">
|
||||
{% if current_user.is_admin() %}
|
||||
<span class="text-green-600">Administrator</span>
|
||||
<a href="{{ url_for('admin.index') }}" class="ml-2 text-xs text-blue-600 hover:underline">
|
||||
Admin Dashboard
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-gray-400">Regular User</span>
|
||||
{% if not admin_exists %}
|
||||
<a href="{{ url_for('users.setup') }}" class="ml-2 text-xs text-blue-600 hover:underline">
|
||||
Setup Admin
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Dropbox Export Folder</p>
|
||||
<p class="font-medium">
|
||||
{{ current_user.dropbox_export_path or '/QuizzicalBeats' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold mb-3 text-navy-700">Account Actions</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<a href="{{ url_for('users.edit_profile') }}" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded-md text-center transition-colors">
|
||||
<i class="fas fa-user-edit mr-2"></i> Edit Profile
|
||||
</a>
|
||||
<a href="{{ url_for('users.change_password') }}" class="bg-navy-600 hover:bg-navy-700 text-white py-2 px-4 rounded-md text-center transition-colors">
|
||||
<i class="fas fa-key mr-2"></i> Change Password
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Spotify Connection Debug - Only for Admins -->
|
||||
{% if current_user.is_admin() %}
|
||||
<div class="mt-6 p-4 border border-gray-300 rounded-lg bg-gray-50">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-lg font-semibold text-navy-700">Spotify Connection Debug</h3>
|
||||
<button id="showSpotifyDebugBtn" class="bg-indigo-500 hover:bg-indigo-600 text-white py-1 px-3 rounded-md text-sm transition-colors">
|
||||
<i class="fas fa-bug mr-1"></i> Show Details
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="w-3 h-3 rounded-full mr-2
|
||||
{% if spotify_status == 'user' %}
|
||||
bg-green-500
|
||||
{% elif spotify_status == 'system' %}
|
||||
bg-yellow-500
|
||||
{% elif spotify_status == 'bearer' %}
|
||||
bg-blue-500
|
||||
{% else %}
|
||||
bg-red-500
|
||||
{% endif %}"></div>
|
||||
<span>
|
||||
{% if spotify_status == 'user' %}
|
||||
Using your Spotify account
|
||||
{% elif spotify_status == 'system' %}
|
||||
Using system Spotify account
|
||||
{% elif spotify_status == 'bearer' %}
|
||||
Using manual bearer token
|
||||
{% else %}
|
||||
No Spotify connection active
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dropbox Connection Debug - Only for Admins -->
|
||||
<div class="mt-4 p-4 border border-gray-300 rounded-lg bg-gray-50">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-lg font-semibold text-navy-700">Dropbox Connection Debug</h3>
|
||||
<button id="showDropboxDebugBtn" class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded-md text-sm transition-colors">
|
||||
<i class="fas fa-bug mr-1"></i> Show Details
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="w-3 h-3 rounded-full mr-2
|
||||
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
|
||||
bg-green-500
|
||||
{% elif current_user.dropbox_token %}
|
||||
bg-yellow-500
|
||||
{% else %}
|
||||
bg-red-500
|
||||
{% endif %}"></div>
|
||||
<span>
|
||||
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
|
||||
Active Dropbox connection
|
||||
{% elif current_user.dropbox_token %}
|
||||
Dropbox token expired or expiring soon
|
||||
{% else %}
|
||||
No Dropbox connection active
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Connected Services -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4 text-navy-700">Connected Services</h3>
|
||||
|
||||
<!-- Spotify Connection - Only shown for admins -->
|
||||
{% if current_user.is_admin() %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-4 shadow-sm">
|
||||
<div class="flex items-center mb-3">
|
||||
<i class="fab fa-spotify text-[#1DB954] text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h4 class="font-medium">Spotify</h4>
|
||||
<p class="text-sm text-gray-600">
|
||||
{% if current_user.spotify_token %}
|
||||
Connected
|
||||
{% if current_user.oauth_id %}
|
||||
as {{ current_user.oauth_id }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Not connected
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('users.spotify_link') }}" class="inline-block w-full bg-{% if current_user.spotify_token %}gray-200 hover:bg-gray-300{% else %}[#1DB954] hover:bg-[#1ed760]{% endif %} text-{% if current_user.spotify_token %}gray-700{% else %}white{% endif %} text-center py-1 px-3 rounded-md text-sm transition-colors">
|
||||
{% if current_user.spotify_token %}
|
||||
Manage Connection
|
||||
{% else %}
|
||||
Connect Spotify
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Dropbox Connection -->
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-4 shadow-sm">
|
||||
<div class="flex items-center mb-3">
|
||||
<i class="fab fa-dropbox text-[#0061FF] text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h4 class="font-medium">Dropbox</h4>
|
||||
<p class="text-sm text-gray-600">
|
||||
{% if current_user.dropbox_id %}
|
||||
Connected
|
||||
{% if current_user.dropbox_id %}
|
||||
as {{ current_user.dropbox_id[:10] }}...
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Not connected
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.dropbox_id %}
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<p class="text-xs text-gray-600">
|
||||
{% if current_user.dropbox_token_expiry %}
|
||||
Token expires: {{ current_user.dropbox_token_expiry.strftime('%Y-%m-%d %H:%M') }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<form action="{{ url_for('users.dropbox_disconnect') }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="inline-block w-full bg-gray-200 hover:bg-gray-300 text-gray-700 text-center py-1 px-3 rounded-md text-sm transition-colors">
|
||||
Disconnect Dropbox
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ url_for('users.dropbox_auth') }}" class="inline-block w-full bg-[#0061FF] hover:bg-[#0055DF] text-white text-center py-1 px-3 rounded-md text-sm transition-colors">
|
||||
Connect Dropbox
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Manual Bearer Token Entry - Only for Admins -->
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-4 shadow-sm">
|
||||
<h4 class="font-medium mb-2">Direct Spotify Access</h4>
|
||||
<p class="text-xs text-gray-600 mb-3">
|
||||
Enter a Spotify bearer token for direct API access.
|
||||
This bypasses OAuth and allows immediate access to Spotify features.
|
||||
</p>
|
||||
|
||||
<form action="{{ url_for('users.update_bearer_token') }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="mb-2">
|
||||
<textarea name="bearer_token" rows="2"
|
||||
placeholder="BQBcQ1foQG0x14axu2kQVz..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm focus:outline-none focus:ring-teal-500 focus:border-teal-500">{{ session.get('access_token', '') }}</textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">Get a token from <a href="https://developer.spotify.com/console/" target="_blank" class="text-teal-600 hover:underline">Spotify Developer Console</a></p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="px-4 py-2 bg-teal-600 text-white text-sm font-medium rounded-md hover:bg-teal-700 transition-colors">
|
||||
Apply Token
|
||||
</button>
|
||||
{% if session.get('access_token') %}
|
||||
<button type="submit" name="clear_token" value="1" class="px-4 py-2 bg-gray-500 text-white text-sm font-medium rounded-md hover:bg-gray-600 transition-colors">
|
||||
Clear Token
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Audio Settings -->
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-4 shadow-sm">
|
||||
<h4 class="font-medium mb-2">Audio Settings</h4>
|
||||
<p class="text-xs text-gray-600 mb-3">
|
||||
Customize your audio preferences for quizzes.
|
||||
</p>
|
||||
<a href="{{ url_for('users.audio_settings') }}" class="inline-block w-full bg-orange-500 hover:bg-orange-600 text-white text-center py-2 px-4 rounded-md text-sm transition-colors">
|
||||
Manage Audio Settings
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Last Login Info -->
|
||||
<div class="mt-6">
|
||||
<h4 class="text-md font-medium mb-2">Last Login</h4>
|
||||
<p class="text-sm text-gray-600">
|
||||
{% if current_user.last_login %}
|
||||
{{ current_user.last_login.strftime('%d %b %Y, %H:%M') }}
|
||||
{% else %}
|
||||
<span class="text-gray-400">No login history</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spotify Debug Modal - Only for Admins -->
|
||||
{% if current_user.is_admin() %}
|
||||
<div id="spotifyDebugModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-white rounded-lg shadow-lg w-full max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-bold text-navy-800">Spotify Connection Details</h3>
|
||||
<button id="closeSpotifyDebugBtn" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Connection Status -->
|
||||
<div class="mb-4 p-3 rounded-lg
|
||||
{% if spotify_status == 'user' %}
|
||||
bg-green-100 border border-green-300
|
||||
{% elif spotify_status == 'system' %}
|
||||
bg-yellow-100 border border-yellow-300
|
||||
{% elif spotify_status == 'bearer' %}
|
||||
bg-blue-100 border border-blue-300
|
||||
{% else %}
|
||||
bg-red-100 border border-red-300
|
||||
{% endif %}">
|
||||
<div class="flex items-center font-semibold mb-2">
|
||||
<div class="w-4 h-4 rounded-full mr-2
|
||||
{% if spotify_status == 'user' %}
|
||||
bg-green-500
|
||||
{% elif spotify_status == 'system' %}
|
||||
bg-yellow-500
|
||||
{% elif spotify_status == 'bearer' %}
|
||||
bg-blue-500
|
||||
{% else %}
|
||||
bg-red-500
|
||||
{% endif %}"></div>
|
||||
<span>
|
||||
{% if spotify_status == 'user' %}
|
||||
Using your Spotify account
|
||||
{% elif spotify_status == 'system' %}
|
||||
Using system Spotify account
|
||||
{% elif spotify_status == 'bearer' %}
|
||||
Using manual bearer token
|
||||
{% else %}
|
||||
No Spotify credentials available
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{% if token_source %}
|
||||
<span class="ml-2 text-xs py-1 px-2 rounded-full bg-gray-200">
|
||||
Source: {{ token_source }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
{% if spotify_status == 'user' %}
|
||||
Your personal Spotify account is connected and authenticated
|
||||
{% elif spotify_status == 'system' %}
|
||||
Using the fallback system Spotify account (refresh token from settings)
|
||||
{% elif spotify_status == 'bearer' %}
|
||||
Using a manual bearer token from session
|
||||
{% else %}
|
||||
No Spotify credentials available
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if token_source %}
|
||||
<p class="mt-2 text-xs text-gray-600">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
{% if token_source == 'user' %}
|
||||
This token was automatically generated from your user account refresh token
|
||||
{% elif token_source == 'system' %}
|
||||
This token was automatically generated from the system fallback refresh token
|
||||
{% else %}
|
||||
Token source: {{ token_source }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Debug Information -->
|
||||
<div class="space-y-4">
|
||||
<!-- Personal Account Details -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">Your Spotify Account</h4>
|
||||
<table class="w-full text-sm">
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Status:</td>
|
||||
<td class="py-2">
|
||||
{% if current_user.spotify_token %}
|
||||
<span class="text-green-600">Connected</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">Not connected</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if current_user.spotify_token %}
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Spotify ID:</td>
|
||||
<td class="py-2">{{ current_user.oauth_id or 'Unknown' }}</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Token Expiry:</td>
|
||||
<td class="py-2">
|
||||
{% if current_user.spotify_token_expiry %}
|
||||
{% if current_user.spotify_token_expiry > now %}
|
||||
<span class="text-green-600">{{ current_user.spotify_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">Expired ({{ current_user.spotify_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }})</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Has Refresh Token:</td>
|
||||
<td class="py-2">
|
||||
{% if current_user.spotify_refresh_token %}
|
||||
<span class="text-green-600">Yes</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- System Account Details -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">System Spotify Account</h4>
|
||||
<table class="w-full text-sm">
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Has Refresh Token:</td>
|
||||
<td class="py-2">
|
||||
{% if system_refresh_token %}
|
||||
<span class="text-green-600">Yes</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if system_refresh_token %}
|
||||
<tr>
|
||||
<td class="py-2 font-medium">Token Preview:</td>
|
||||
<td class="py-2 font-mono">{{ system_refresh_token[:10] }}...{{ system_refresh_token[-5:] }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Manual Bearer Token -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">Manual Bearer Token</h4>
|
||||
<table class="w-full text-sm">
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Status:</td>
|
||||
<td class="py-2">
|
||||
{% if session_bearer and token_source == 'manual' %}
|
||||
<span class="text-green-600">Active manual token</span>
|
||||
{% elif session_bearer %}
|
||||
<span class="text-gray-600">Present but not active</span>
|
||||
{% else %}
|
||||
<span class="text-gray-600">Not present</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if session_bearer %}
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Token Preview:</td>
|
||||
<td class="py-2 font-mono">{{ session_bearer[:10] }}...{{ session_bearer[-5:] }}</td>
|
||||
</tr>
|
||||
{% if token_source == 'manual' and active_token_expiry %}
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Estimated Expiry:</td>
|
||||
<td class="py-2">
|
||||
{% if active_token_expiry > now %}
|
||||
<span class="text-green-600">{{ active_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||
<span class="text-xs text-gray-500">(Manual tokens typically expire after 1 hour)</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">Likely expired ({{ active_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }})</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if active_username and token_source == 'manual' %}
|
||||
<tr>
|
||||
<td class="py-2 font-medium">Associated User:</td>
|
||||
<td class="py-2">
|
||||
<div class="flex items-center">
|
||||
{% if active_user_image %}
|
||||
<img src="{{ active_user_image }}" alt="User" class="w-6 h-6 rounded-full mr-2">
|
||||
{% endif %}
|
||||
<span>{{ active_username }} ({{ active_user_id }})</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Current Active Token -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">Current Active Spotify User</h4>
|
||||
|
||||
{% if active_username %}
|
||||
<div class="flex items-center mb-3">
|
||||
{% if active_user_image %}
|
||||
<img src="{{ active_user_image }}" alt="Profile" class="w-10 h-10 rounded-full mr-3">
|
||||
{% else %}
|
||||
<div class="w-10 h-10 bg-gray-300 rounded-full flex items-center justify-center mr-3">
|
||||
<i class="fab fa-spotify text-gray-600"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<p class="font-medium">{{ active_username }}</p>
|
||||
<p class="text-sm text-gray-600">ID: {{ active_user_id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif token_source == 'client_credentials' %}
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center mr-3">
|
||||
<i class="fab fa-spotify text-purple-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">App Client Credentials</p>
|
||||
<p class="text-sm text-gray-600">Using application's credentials (limited access)</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-600 italic">No active Spotify user detected</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3">
|
||||
<h5 class="text-sm font-medium mb-1">Token Source</h5>
|
||||
<div class="text-sm">
|
||||
{% if token_source %}
|
||||
<span class="px-2 py-1 rounded-full text-xs
|
||||
{% if token_source == 'user' %}
|
||||
bg-green-100 text-green-800
|
||||
{% elif token_source == 'system' %}
|
||||
bg-yellow-100 text-yellow-800
|
||||
{% elif token_source == 'client_credentials' %}
|
||||
bg-purple-100 text-purple-800
|
||||
{% else %}
|
||||
bg-blue-100 text-blue-800
|
||||
{% endif %}">
|
||||
{{ token_source }}
|
||||
</span>
|
||||
|
||||
{% if token_source == 'client_credentials' and client_token_expiry %}
|
||||
<span class="text-xs text-gray-600 ml-2">
|
||||
Expires: {{ client_token_expiry|timestamp_to_datetime|format_datetime }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<span class="text-gray-600 italic">Unknown source</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Priority Order -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">Precedence</h4>
|
||||
<p class="text-sm mb-2">Connection priority: Manual Bearer → Your Account → Client Credentials</p>
|
||||
<div class="flex flex-col space-y-2 text-sm">
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 rounded-full
|
||||
{% if spotify_status == 'bearer' %}bg-blue-500{% else %}bg-gray-300{% endif %}
|
||||
mr-2"></div>
|
||||
<span>1. Manual Bearer Token {% if spotify_status == 'bearer' %}(active){% endif %}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 rounded-full
|
||||
{% if spotify_status == 'user' %}bg-green-500{% else %}bg-gray-300{% endif %}
|
||||
mr-2"></div>
|
||||
<span>2. User Account {% if spotify_status == 'user' %}(active){% endif %}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 rounded-full
|
||||
{% if spotify_status == 'client_credentials' %}bg-purple-500{% else %}bg-gray-300{% endif %}
|
||||
mr-2"></div>
|
||||
<span>3. App Client Credentials {% if spotify_status == 'client_credentials' %}(active){% endif %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button id="closeModalBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-800 py-2 px-4 rounded-md">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dropbox Debug Modal - Only for Admins -->
|
||||
<div id="dropboxDebugModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-white rounded-lg shadow-lg w-full max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-bold text-navy-800">Dropbox Connection Details</h3>
|
||||
<button id="closeDropboxDebugBtn" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Connection Status -->
|
||||
<div class="mb-4 p-3 rounded-lg
|
||||
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
|
||||
bg-green-100 border border-green-300
|
||||
{% elif current_user.dropbox_token %}
|
||||
bg-yellow-100 border border-yellow-300
|
||||
{% else %}
|
||||
bg-red-100 border border-red-300
|
||||
{% endif %}">
|
||||
<div class="flex items-center font-semibold mb-2">
|
||||
<div class="w-4 h-4 rounded-full mr-2
|
||||
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
|
||||
bg-green-500
|
||||
{% elif current_user.dropbox_token %}
|
||||
bg-yellow-500
|
||||
{% else %}
|
||||
bg-red-500
|
||||
{% endif %}"></div>
|
||||
<span>
|
||||
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
|
||||
Active Dropbox connection
|
||||
{% elif current_user.dropbox_token %}
|
||||
Dropbox token expired or expiring soon
|
||||
{% else %}
|
||||
No Dropbox connection
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
|
||||
Your Dropbox account is connected and has a valid access token
|
||||
{% elif current_user.dropbox_token %}
|
||||
Your Dropbox account is connected but the token needs to be refreshed
|
||||
{% else %}
|
||||
No Dropbox credentials available
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Debug Information -->
|
||||
<div class="space-y-4">
|
||||
<!-- Dropbox Account Details -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">Your Dropbox Account</h4>
|
||||
<table class="w-full text-sm">
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Status:</td>
|
||||
<td class="py-2">
|
||||
{% if current_user.dropbox_id %}
|
||||
<span class="text-green-600">Connected</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">Not connected</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if current_user.dropbox_id %}
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">User Info:</td>
|
||||
<td class="py-2">
|
||||
<div class="flex items-center">
|
||||
{% if session.get('dropbox_user_info') and session.get('dropbox_user_info').get('picture') %}
|
||||
<img src="{{ session.get('dropbox_user_info').get('picture') }}" alt="Profile" class="w-8 h-8 rounded-full mr-2">
|
||||
{% else %}
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-2">
|
||||
<i class="fab fa-dropbox text-blue-600"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if session.get('dropbox_user_info') %}
|
||||
<div class="font-medium">{{ session.get('dropbox_user_info').get('name') }}</div>
|
||||
<div class="text-xs text-gray-500">{{ session.get('dropbox_user_info').get('email') }}</div>
|
||||
{% else %}
|
||||
<div class="text-gray-500 italic">User info not available</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Dropbox ID:</td>
|
||||
<td class="py-2">{{ current_user.dropbox_id }}</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Token Expiry:</td>
|
||||
<td class="py-2">
|
||||
{% if current_user.dropbox_token_expiry %}
|
||||
{% if current_user.dropbox_token_expiry > now %}
|
||||
<span class="text-green-600">{{ current_user.dropbox_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">Expired ({{ current_user.dropbox_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }})</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Has Refresh Token:</td>
|
||||
<td class="py-2">
|
||||
{% if current_user.dropbox_refresh_token %}
|
||||
<span class="text-green-600">Yes</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Dropbox Token Details -->
|
||||
{% if current_user.dropbox_token %}
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">Token Information</h4>
|
||||
<table class="w-full text-sm">
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Access Token Preview:</td>
|
||||
<td class="py-2 font-mono">
|
||||
{{ current_user.dropbox_token[:10] }}...{{ current_user.dropbox_token[-5:] if current_user.dropbox_token|length > 15 else '' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if current_user.dropbox_refresh_token %}
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-2 font-medium">Refresh Token Preview:</td>
|
||||
<td class="py-2 font-mono">
|
||||
{{ current_user.dropbox_refresh_token[:10] }}...{{ current_user.dropbox_refresh_token[-5:] if current_user.dropbox_refresh_token|length > 15 else '' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="py-2 font-medium">Token Status:</td>
|
||||
<td class="py-2">
|
||||
{% if current_user.dropbox_token_expiry %}
|
||||
{% if current_user.dropbox_token_expiry > now %}
|
||||
<span class="text-green-600">Valid for {{ ((current_user.dropbox_token_expiry - now).total_seconds() / 60)|int }} more minutes</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">Expired {{ ((now - current_user.dropbox_token_expiry).total_seconds() / 60)|int }} minutes ago</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-gray-600">Unknown expiry time</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Test Actions -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">Connection Test</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
The following actions can help test your Dropbox connection:
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<a href="{{ url_for('users.dropbox_auth') }}" class="inline-block bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded-md text-sm transition-colors">
|
||||
Reconnect Dropbox
|
||||
</a>
|
||||
|
||||
{% if current_user.dropbox_id %}
|
||||
<form action="{{ url_for('users.dropbox_disconnect') }}" method="POST" class="inline-block">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white py-1 px-3 rounded-md text-sm transition-colors">
|
||||
Disconnect Dropbox
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button id="closeDropboxModalBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-800 py-2 px-4 rounded-md">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% if current_user.is_admin() %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal = document.getElementById('spotifyDebugModal');
|
||||
const showBtn = document.getElementById('showSpotifyDebugBtn');
|
||||
const closeBtn = document.getElementById('closeSpotifyDebugBtn');
|
||||
const closeModalBtn = document.getElementById('closeModalBtn');
|
||||
|
||||
// Show modal
|
||||
showBtn.addEventListener('click', function() {
|
||||
modal.classList.remove('hidden');
|
||||
});
|
||||
|
||||
// Close modal
|
||||
closeBtn.addEventListener('click', function() {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
|
||||
closeModalBtn.addEventListener('click', function() {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
modal.addEventListener('click', function(event) {
|
||||
if (event.target === modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
const dropboxModal = document.getElementById('dropboxDebugModal');
|
||||
const showDropboxBtn = document.getElementById('showDropboxDebugBtn');
|
||||
const closeDropboxBtn = document.getElementById('closeDropboxDebugBtn');
|
||||
const closeDropboxModalBtn = document.getElementById('closeDropboxModalBtn');
|
||||
|
||||
// Show Dropbox modal
|
||||
showDropboxBtn.addEventListener('click', function() {
|
||||
dropboxModal.classList.remove('hidden');
|
||||
});
|
||||
|
||||
// Close Dropbox modal
|
||||
closeDropboxBtn.addEventListener('click', function() {
|
||||
dropboxModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
closeDropboxModalBtn.addEventListener('click', function() {
|
||||
dropboxModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Close Dropbox modal when clicking outside
|
||||
dropboxModal.addEventListener('click', function(event) {
|
||||
if (event.target === dropboxModal) {
|
||||
dropboxModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Register{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-6 text-navy-800">Create an Account</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-3 {% if category == 'danger' %}bg-red-100 text-red-700{% elif category == 'success' %}bg-green-100 text-green-700{% else %}bg-blue-100 text-blue-700{% endif %} rounded">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('users.register') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
|
||||
Username
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="username" name="username" type="text" placeholder="Choose a username">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="email">
|
||||
Email
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="email" name="email" type="email" placeholder="Enter your email">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">
|
||||
Password
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password" name="password" type="password" placeholder="Enter your password">
|
||||
<p class="text-xs text-gray-500 mt-1">Must be at least 8 characters long.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="confirm_password">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="confirm_password" name="confirm_password" type="password" placeholder="Confirm your password">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<button class="w-full bg-teal-500 hover:bg-teal-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit">
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<p class="text-gray-700">
|
||||
Already have an account?
|
||||
<a href="{{ url_for('users.login') }}" class="font-bold text-teal-500 hover:text-teal-800">
|
||||
Login
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,67 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Reset Password - Quizzical Beats{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center">
|
||||
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-navy-800 font-montserrat">
|
||||
Reset Your Password
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
Please enter your new password below
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 bg-white py-8 px-6 shadow-md rounded-lg">
|
||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
||||
<div class="mb-4 p-4 rounded-md {% if category == 'danger' %}bg-red-50 text-red-700{% elif category == 'success' %}bg-green-50 text-green-700{% else %}bg-blue-50 text-blue-700{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<form class="space-y-6" action="{{ url_for('users.reset_password', token=token) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">New Password</label>
|
||||
<div class="mt-1">
|
||||
<input id="password" name="password" type="password" autocomplete="new-password" required
|
||||
class="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-orange-500 focus:border-orange-500"
|
||||
minlength="8">
|
||||
<p class="text-xs text-gray-500 mt-1">Password must be at least 8 characters</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm_password" class="block text-sm font-medium text-gray-700">Confirm Password</label>
|
||||
<div class="mt-1">
|
||||
<input id="confirm_password" name="confirm_password" type="password" autocomplete="new-password" required
|
||||
class="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-orange-500 focus:border-orange-500"
|
||||
minlength="8">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500">
|
||||
Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('users.login') }}" class="text-sm text-orange-600 hover:text-orange-500">
|
||||
Return to login page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4 text-sm text-gray-500">
|
||||
<p>Need a new reset link? <a href="{{ url_for('users.forgot_password') }}" class="text-orange-600 hover:text-orange-500">Request password reset again</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,143 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Connect Spotify - Quizzical Beats{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-6 text-navy-800">Connect Spotify Account</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-3 {% if category == 'danger' %}bg-red-100 text-red-700{% elif category == 'success' %}bg-green-100 text-green-700{% else %}bg-blue-100 text-blue-700{% endif %} rounded">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<!-- Left column: Current status -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h3 class="text-lg font-semibold mb-4 text-navy-700">Spotify Connection Status</h3>
|
||||
|
||||
{% if current_user.spotify_token %}
|
||||
<div class="mb-6 flex items-center">
|
||||
<div class="mr-4 bg-green-100 p-3 rounded-full">
|
||||
<i class="fab fa-spotify text-2xl text-green-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-green-700">Connected to Spotify</p>
|
||||
<p class="text-sm text-gray-600">Your account is linked to Spotify</p>
|
||||
{% if current_user.oauth_id %}
|
||||
<p class="text-sm text-gray-600 mt-1">Spotify ID: {{ current_user.oauth_id }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-md font-medium mb-2">Token Information</h4>
|
||||
<div class="bg-white p-4 rounded-lg border border-gray-200 text-sm">
|
||||
<p class="flex justify-between mb-2">
|
||||
<span class="font-medium">Token Status:</span>
|
||||
<span class="{% if current_user.spotify_token_expiry and current_user.spotify_token_expiry > now %}text-green-600{% else %}text-red-600{% endif %}">
|
||||
{% if current_user.spotify_token_expiry and current_user.spotify_token_expiry > now %}
|
||||
Valid
|
||||
{% else %}
|
||||
Expired
|
||||
{% endif %}
|
||||
</span>
|
||||
</p>
|
||||
{% if current_user.spotify_token_expiry %}
|
||||
<p class="flex justify-between">
|
||||
<span class="font-medium">Expires:</span>
|
||||
<span>{{ current_user.spotify_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('users.spotify_link') }}" class="mt-6">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="action" value="disconnect">
|
||||
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white py-2 px-4 rounded-md font-medium">
|
||||
<i class="fas fa-unlink mr-2"></i> Disconnect Spotify
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
<div class="mb-6 flex items-center">
|
||||
<div class="mr-4 bg-gray-200 p-3 rounded-full">
|
||||
<i class="fab fa-spotify text-2xl text-gray-500"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-700">Not Connected</p>
|
||||
<p class="text-sm text-gray-600">Your account is not linked to Spotify</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('users.spotify_auth') }}" class="inline-block bg-[#1DB954] hover:bg-[#1ed760] text-white font-medium py-2 px-4 rounded-md">
|
||||
<i class="fab fa-spotify mr-2"></i> Connect with Spotify
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right column: Info and benefits -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4 text-navy-700">Why Connect Spotify?</h3>
|
||||
|
||||
<div class="bg-teal-50 border-l-4 border-teal-500 p-4 rounded mb-6">
|
||||
<p class="text-teal-700">
|
||||
Connecting your Spotify account enhances your music quiz creation experience
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex">
|
||||
<div class="mr-3 text-teal-500">
|
||||
<i class="fas fa-music"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium">Access Your Playlists</h4>
|
||||
<p class="text-gray-600 text-sm">Import songs from your personal Spotify playlists</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="mr-3 text-teal-500">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium">Search the Spotify Catalog</h4>
|
||||
<p class="text-gray-600 text-sm">Find and import any track from Spotify's huge library</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="mr-3 text-teal-500">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium">Trending Playlists</h4>
|
||||
<p class="text-gray-600 text-sm">Access Spotify's official and trending playlists</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="font-medium mb-2">Privacy Note</h4>
|
||||
<p class="text-sm text-gray-600">
|
||||
We only access your Spotify data to help you create music quizzes.
|
||||
We don't share your information or post to your account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<a href="{{ url_for('users.profile') }}" class="text-teal-600 hover:text-teal-800">
|
||||
<i class="fas fa-arrow-left mr-2"></i> Back to Profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user