From f6ff32a746cf5229a779903b0536ecf07c712aa8 Mon Sep 17 00:00:00 2001 From: phamhungd Date: Sun, 23 Nov 2025 23:36:21 +0700 Subject: [PATCH] add template edit --- .gitignore | 2 + app.py | 295 +++++++++++++++++++- static/modules/templateGallery.js | 35 +++ static/script.js | 440 ++++++++++++++++++++++++++++++ static/style.css | 197 +++++++++++++ templates/index.html | 78 ++++++ 6 files changed, 1043 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 5d74793..9168098 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /.venv /static/uploads .DS_Store +user_prompts.json +/static/preview diff --git a/app.py b/app.py index 52dbc75..6967c0d 100644 --- a/app.py +++ b/app.py @@ -253,21 +253,308 @@ def get_prompts(): category = request.args.get('category') try: + all_prompts = [] + # Read prompts.json file prompts_path = os.path.join(os.path.dirname(__file__), 'prompts.json') - with open(prompts_path, 'r', encoding='utf-8') as f: - prompts = json.load(f) + if os.path.exists(prompts_path): + with open(prompts_path, 'r', encoding='utf-8') as f: + all_prompts.extend(json.load(f)) + + # Read user_prompts.json file and mark as user templates + user_prompts_path = os.path.join(os.path.dirname(__file__), 'user_prompts.json') + if os.path.exists(user_prompts_path): + try: + with open(user_prompts_path, 'r', encoding='utf-8') as f: + user_prompts = json.load(f) + if isinstance(user_prompts, list): + # Mark each user template and add index for editing + for idx, template in enumerate(user_prompts): + template['isUserTemplate'] = True + template['userTemplateIndex'] = idx + all_prompts.extend(user_prompts) + except json.JSONDecodeError: + pass # Ignore if empty or invalid # Filter by category if specified if category: - prompts = [p for p in prompts if p.get('category') == category] + all_prompts = [p for p in all_prompts if p.get('category') == category] - response = jsonify({'prompts': prompts}) + response = jsonify({'prompts': all_prompts}) response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" return response except Exception as e: return jsonify({'error': str(e)}), 500 +@app.route('/save_template', methods=['POST']) +def save_template(): + try: + import requests + from urllib.parse import urlparse + + # Handle multipart form data + title = request.form.get('title') + prompt = request.form.get('prompt') + mode = request.form.get('mode', 'generate') + category = request.form.get('category', 'User') + + if not title or not prompt: + return jsonify({'error': 'Title and prompt are required'}), 400 + + # Handle preview image + preview_path = None + preview_dir = os.path.join(app.static_folder, 'preview') + os.makedirs(preview_dir, exist_ok=True) + + # Check if file was uploaded + if 'preview' in request.files: + file = request.files['preview'] + if file.filename: + ext = os.path.splitext(file.filename)[1] + if not ext: + ext = '.png' + + filename = f"template_{uuid.uuid4()}{ext}" + filepath = os.path.join(preview_dir, filename) + file.save(filepath) + + preview_path = url_for('static', filename=f'preview/{filename}') + + # If no file uploaded, check if URL/path provided + if not preview_path: + preview_url = request.form.get('preview_path') + if preview_url: + try: + # Check if it's a URL or local path + if preview_url.startswith('http://') or preview_url.startswith('https://'): + # Download from URL + response = requests.get(preview_url, timeout=10) + response.raise_for_status() + + # Determine extension from content-type or URL + content_type = response.headers.get('content-type', '') + if 'image/png' in content_type: + ext = '.png' + elif 'image/jpeg' in content_type or 'image/jpg' in content_type: + ext = '.jpg' + elif 'image/webp' in content_type: + ext = '.webp' + else: + # Try to get from URL + parsed = urlparse(preview_url) + ext = os.path.splitext(parsed.path)[1] or '.png' + + filename = f"template_{uuid.uuid4()}{ext}" + filepath = os.path.join(preview_dir, filename) + + with open(filepath, 'wb') as f: + f.write(response.content) + + preview_path = url_for('static', filename=f'preview/{filename}') + + elif preview_url.startswith('/static/'): + # Local path - copy to preview folder + rel_path = preview_url.split('/static/')[1] + source_path = os.path.join(app.static_folder, rel_path) + + if os.path.exists(source_path): + ext = os.path.splitext(source_path)[1] or '.png' + filename = f"template_{uuid.uuid4()}{ext}" + dest_path = os.path.join(preview_dir, filename) + + import shutil + shutil.copy2(source_path, dest_path) + + preview_path = url_for('static', filename=f'preview/{filename}') + else: + # File doesn't exist, use original path + preview_path = preview_url + else: + # Use as-is if it's already a valid path + preview_path = preview_url + + except Exception as e: + print(f"Error processing preview image URL: {e}") + # Use the original URL if processing fails + preview_path = preview_url + + new_template = { + 'title': title, + 'prompt': prompt, + 'mode': mode, + 'category': category, + 'preview': preview_path + } + + # Save to user_prompts.json + user_prompts_path = os.path.join(os.path.dirname(__file__), 'user_prompts.json') + user_prompts = [] + + if os.path.exists(user_prompts_path): + try: + with open(user_prompts_path, 'r', encoding='utf-8') as f: + content = f.read() + if content.strip(): + user_prompts = json.loads(content) + except json.JSONDecodeError: + pass + + user_prompts.append(new_template) + + with open(user_prompts_path, 'w', encoding='utf-8') as f: + json.dump(user_prompts, f, indent=4, ensure_ascii=False) + + return jsonify({'success': True, 'template': new_template}) + + except Exception as e: + print(f"Error saving template: {e}") + return jsonify({'error': str(e)}), 500 + +@app.route('/update_template', methods=['POST']) +def update_template(): + try: + import requests + from urllib.parse import urlparse + + # Get template index + template_index = request.form.get('template_index') + if template_index is None: + return jsonify({'error': 'Template index is required'}), 400 + + template_index = int(template_index) + + # Handle multipart form data + title = request.form.get('title') + prompt = request.form.get('prompt') + mode = request.form.get('mode', 'generate') + category = request.form.get('category', 'User') + + if not title or not prompt: + return jsonify({'error': 'Title and prompt are required'}), 400 + + # Handle preview image (same logic as save_template) + preview_path = None + preview_dir = os.path.join(app.static_folder, 'preview') + os.makedirs(preview_dir, exist_ok=True) + + # Check if file was uploaded + if 'preview' in request.files: + file = request.files['preview'] + if file.filename: + ext = os.path.splitext(file.filename)[1] + if not ext: + ext = '.png' + + filename = f"template_{uuid.uuid4()}{ext}" + filepath = os.path.join(preview_dir, filename) + file.save(filepath) + + preview_path = url_for('static', filename=f'preview/{filename}') + + # If no file uploaded, check if URL/path provided + if not preview_path: + preview_url = request.form.get('preview_path') + if preview_url: + try: + if preview_url.startswith('http://') or preview_url.startswith('https://'): + response = requests.get(preview_url, timeout=10) + response.raise_for_status() + + content_type = response.headers.get('content-type', '') + if 'image/png' in content_type: + ext = '.png' + elif 'image/jpeg' in content_type or 'image/jpg' in content_type: + ext = '.jpg' + elif 'image/webp' in content_type: + ext = '.webp' + else: + parsed = urlparse(preview_url) + ext = os.path.splitext(parsed.path)[1] or '.png' + + filename = f"template_{uuid.uuid4()}{ext}" + filepath = os.path.join(preview_dir, filename) + + with open(filepath, 'wb') as f: + f.write(response.content) + + preview_path = url_for('static', filename=f'preview/{filename}') + + elif preview_url.startswith('/static/'): + rel_path = preview_url.split('/static/')[1] + source_path = os.path.join(app.static_folder, rel_path) + + if os.path.exists(source_path): + ext = os.path.splitext(source_path)[1] or '.png' + filename = f"template_{uuid.uuid4()}{ext}" + dest_path = os.path.join(preview_dir, filename) + + import shutil + shutil.copy2(source_path, dest_path) + + preview_path = url_for('static', filename=f'preview/{filename}') + else: + preview_path = preview_url + else: + preview_path = preview_url + + except Exception as e: + print(f"Error processing preview image URL: {e}") + preview_path = preview_url + + # Read existing user templates + user_prompts_path = os.path.join(os.path.dirname(__file__), 'user_prompts.json') + user_prompts = [] + + if os.path.exists(user_prompts_path): + try: + with open(user_prompts_path, 'r', encoding='utf-8') as f: + content = f.read() + if content.strip(): + user_prompts = json.loads(content) + except json.JSONDecodeError: + pass + + # Check if index is valid + if template_index < 0 or template_index >= len(user_prompts): + return jsonify({'error': 'Invalid template index'}), 400 + + # Get old template to check for old preview image + old_template = user_prompts[template_index] + old_preview = old_template.get('preview', '') + + # Delete old preview image if it exists in the preview folder + if old_preview and '/preview/' in old_preview: + try: + # Extract filename from URL + old_filename = old_preview.split('/preview/')[-1] + old_filepath = os.path.join(preview_dir, old_filename) + + # Delete old file if it exists + if os.path.exists(old_filepath): + os.remove(old_filepath) + print(f"Deleted old preview image: {old_filepath}") + except Exception as e: + print(f"Error deleting old preview image: {e}") + + # Update the template + user_prompts[template_index] = { + 'title': title, + 'prompt': prompt, + 'mode': mode, + 'category': category, + 'preview': preview_path + } + + # Save back to file + with open(user_prompts_path, 'w', encoding='utf-8') as f: + json.dump(user_prompts, f, indent=4, ensure_ascii=False) + + return jsonify({'success': True, 'template': user_prompts[template_index]}) + + except Exception as e: + print(f"Error updating template: {e}") + return jsonify({'error': str(e)}), 500 + @app.route('/refine_prompt', methods=['POST']) def refine_prompt(): data = request.get_json() diff --git a/static/modules/templateGallery.js b/static/modules/templateGallery.js index 3c3e654..18004b2 100644 --- a/static/modules/templateGallery.js +++ b/static/modules/templateGallery.js @@ -105,6 +105,25 @@ export function createTemplateGallery({ container, onSelectTemplate }) { }; preview.appendChild(img); } + + // Edit button (show on all templates) + const editBtn = document.createElement('button'); + editBtn.className = 'template-edit-btn'; + editBtn.innerHTML = ` + + + + + `; + editBtn.title = 'Edit Template'; + editBtn.addEventListener('click', (e) => { + e.stopPropagation(); + if (window.openEditTemplateModal) { + window.openEditTemplateModal(template); + } + }); + preview.appendChild(editBtn); + card.appendChild(preview); // Content @@ -227,6 +246,22 @@ export function createTemplateGallery({ container, onSelectTemplate }) { }); controls.appendChild(categorySelect); + // Create Template button + const createTemplateBtn = document.createElement('button'); + createTemplateBtn.className = 'template-create-btn'; + createTemplateBtn.innerHTML = ` + + + + Create Template + `; + createTemplateBtn.addEventListener('click', () => { + if (window.openCreateTemplateModal) { + window.openCreateTemplateModal(); + } + }); + controls.appendChild(createTemplateBtn); + header.appendChild(controls); container.appendChild(header); diff --git a/static/script.js b/static/script.js index f9d74a5..1851521 100644 --- a/static/script.js +++ b/static/script.js @@ -319,6 +319,446 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // Create Template Logic + const createTemplateBtn = document.getElementById('create-template-btn'); + const createTemplateModal = document.getElementById('create-template-modal'); + const closeTemplateModalBtn = document.getElementById('close-template-modal'); + const saveTemplateBtn = document.getElementById('save-template-btn'); + + const templateTitleInput = document.getElementById('template-title'); + const templatePromptInput = document.getElementById('template-prompt'); + const templateModeSelect = document.getElementById('template-mode'); + const templateCategorySelect = document.getElementById('template-category-select'); + const templateCategoryInput = document.getElementById('template-category-input'); + const templatePreviewDropzone = document.getElementById('template-preview-dropzone'); + const templatePreviewImg = document.getElementById('template-preview-img'); + const dropzonePlaceholder = document.querySelector('.dropzone-placeholder'); + + let currentPreviewFile = null; + let currentPreviewUrl = null; + let editingTemplate = null; // Track if we're editing an existing template + + // Global function for opening edit modal (called from templateGallery.js) + window.openEditTemplateModal = async function(template) { + editingTemplate = template; + + // Pre-fill with template data + templateTitleInput.value = template.title || ''; + templatePromptInput.value = template.prompt || ''; + templateModeSelect.value = template.mode || 'generate'; + templateCategoryInput.classList.add('hidden'); + templateCategoryInput.value = ''; + + // Populate categories + try { + const response = await fetch('/prompts'); + const data = await response.json(); + + if (data.prompts) { + const categories = new Set(); + data.prompts.forEach(t => { + if (t.category) { + const categoryText = typeof t.category === 'string' + ? t.category + : (t.category.vi || t.category.en || ''); + if (categoryText) categories.add(categoryText); + } + }); + + templateCategorySelect.innerHTML = ''; + const sortedCategories = Array.from(categories).sort(); + sortedCategories.forEach(cat => { + const option = document.createElement('option'); + option.value = cat; + option.textContent = cat; + templateCategorySelect.appendChild(option); + }); + + const newOption = document.createElement('option'); + newOption.value = 'new'; + newOption.textContent = '+ New Category'; + templateCategorySelect.appendChild(newOption); + + // Set to template's category + const templateCategory = typeof template.category === 'string' + ? template.category + : (template.category.vi || template.category.en || ''); + templateCategorySelect.value = templateCategory || 'User'; + } + } catch (error) { + console.error('Failed to load categories:', error); + } + + // Set preview image + if (template.preview) { + templatePreviewImg.src = template.preview; + templatePreviewImg.classList.remove('hidden'); + dropzonePlaceholder.classList.add('hidden'); + currentPreviewUrl = template.preview; + } else { + templatePreviewImg.src = ''; + templatePreviewImg.classList.add('hidden'); + dropzonePlaceholder.classList.remove('hidden'); + currentPreviewUrl = null; + } + currentPreviewFile = null; + + // Update button text + saveTemplateBtn.innerHTML = 'Update Template
'; + + createTemplateModal.classList.remove('hidden'); + }; + + // Global function for opening create modal with empty values (called from templateGallery.js) + window.openCreateTemplateModal = async function() { + editingTemplate = null; + + // Clear all fields + templateTitleInput.value = ''; + templatePromptInput.value = ''; + templateModeSelect.value = 'generate'; + templateCategoryInput.classList.add('hidden'); + templateCategoryInput.value = ''; + + // Populate categories + try { + const response = await fetch('/prompts'); + const data = await response.json(); + + if (data.prompts) { + const categories = new Set(); + data.prompts.forEach(t => { + if (t.category) { + const categoryText = typeof t.category === 'string' + ? t.category + : (t.category.vi || t.category.en || ''); + if (categoryText) categories.add(categoryText); + } + }); + + templateCategorySelect.innerHTML = ''; + const sortedCategories = Array.from(categories).sort(); + sortedCategories.forEach(cat => { + const option = document.createElement('option'); + option.value = cat; + option.textContent = cat; + templateCategorySelect.appendChild(option); + }); + + const newOption = document.createElement('option'); + newOption.value = 'new'; + newOption.textContent = '+ New Category'; + templateCategorySelect.appendChild(newOption); + + if (sortedCategories.includes('User')) { + templateCategorySelect.value = 'User'; + } else if (sortedCategories.length > 0) { + templateCategorySelect.value = sortedCategories[0]; + } + } + } catch (error) { + console.error('Failed to load categories:', error); + } + + // Clear preview image + templatePreviewImg.src = ''; + templatePreviewImg.classList.add('hidden'); + dropzonePlaceholder.classList.remove('hidden'); + currentPreviewUrl = null; + currentPreviewFile = null; + + // Update button text + saveTemplateBtn.innerHTML = 'Save Template
'; + + createTemplateModal.classList.remove('hidden'); + }; + + if (createTemplateBtn) { + createTemplateBtn.addEventListener('click', async () => { + // Reset editing state + editingTemplate = null; + + // Pre-fill data + templateTitleInput.value = ''; + templatePromptInput.value = promptInput.value; + templateModeSelect.value = 'generate'; + templateCategoryInput.classList.add('hidden'); + templateCategoryInput.value = ''; + + // Populate categories dynamically from template library + try { + const response = await fetch('/prompts'); + const data = await response.json(); + + if (data.prompts) { + // Extract unique categories + const categories = new Set(); + data.prompts.forEach(template => { + if (template.category) { + // Handle both string and object categories + const categoryText = typeof template.category === 'string' + ? template.category + : (template.category.vi || template.category.en || ''); + if (categoryText) { + categories.add(categoryText); + } + } + }); + + // Clear existing options except "new" + templateCategorySelect.innerHTML = ''; + + // Add sorted categories + const sortedCategories = Array.from(categories).sort(); + sortedCategories.forEach(cat => { + const option = document.createElement('option'); + option.value = cat; + option.textContent = cat; + templateCategorySelect.appendChild(option); + }); + + // Add "new category" option at the end + const newOption = document.createElement('option'); + newOption.value = 'new'; + newOption.textContent = '+ New Category'; + templateCategorySelect.appendChild(newOption); + + // Set default to first category or "User" if it exists + if (sortedCategories.includes('User')) { + templateCategorySelect.value = 'User'; + } else if (sortedCategories.length > 0) { + templateCategorySelect.value = sortedCategories[0]; + } + } + } catch (error) { + console.error('Failed to load categories:', error); + // Fallback to default categories + templateCategorySelect.innerHTML = ` + + + `; + templateCategorySelect.value = 'User'; + } + + // Set preview image from current generated image + if (generatedImage.src && !generatedImage.src.endsWith('placeholder.png')) { + templatePreviewImg.src = generatedImage.src; + templatePreviewImg.classList.remove('hidden'); + dropzonePlaceholder.classList.add('hidden'); + currentPreviewUrl = generatedImage.src; + } else { + templatePreviewImg.src = ''; + templatePreviewImg.classList.add('hidden'); + dropzonePlaceholder.classList.remove('hidden'); + currentPreviewUrl = null; + } + currentPreviewFile = null; + + // Update button text + saveTemplateBtn.innerHTML = 'Save Template
'; + + createTemplateModal.classList.remove('hidden'); + }); + } + + if (closeTemplateModalBtn) { + closeTemplateModalBtn.addEventListener('click', () => { + createTemplateModal.classList.add('hidden'); + }); + } + + // Category select logic + if (templateCategorySelect) { + templateCategorySelect.addEventListener('change', (e) => { + if (e.target.value === 'new') { + templateCategoryInput.classList.remove('hidden'); + templateCategoryInput.focus(); + } else { + templateCategoryInput.classList.add('hidden'); + } + }); + } + + // Drag and drop for preview + const templatePreviewUrlInput = document.getElementById('template-preview-url'); + let isUrlInputMode = false; + + if (templatePreviewDropzone) { + // Click to toggle URL input mode + templatePreviewDropzone.addEventListener('click', (e) => { + // Don't toggle if clicking on the input itself + if (e.target === templatePreviewUrlInput) return; + + if (!isUrlInputMode) { + // Switch to URL input mode + isUrlInputMode = true; + templatePreviewImg.classList.add('hidden'); + dropzonePlaceholder.classList.add('hidden'); + templatePreviewUrlInput.classList.remove('hidden'); + templatePreviewUrlInput.focus(); + } + }); + + // Handle URL input + if (templatePreviewUrlInput) { + templatePreviewUrlInput.addEventListener('blur', async () => { + const url = templatePreviewUrlInput.value.trim(); + if (url) { + try { + // Try to load the image from URL + const img = new Image(); + img.onload = () => { + templatePreviewImg.src = url; + templatePreviewImg.classList.remove('hidden'); + dropzonePlaceholder.classList.add('hidden'); + templatePreviewUrlInput.classList.add('hidden'); + currentPreviewUrl = url; + currentPreviewFile = null; + isUrlInputMode = false; + }; + img.onerror = () => { + alert('Failed to load image from URL. Please check the URL and try again.'); + templatePreviewUrlInput.focus(); + }; + img.src = url; + } catch (error) { + alert('Invalid image URL'); + templatePreviewUrlInput.focus(); + } + } else { + // If empty, go back to placeholder + isUrlInputMode = false; + templatePreviewUrlInput.classList.add('hidden'); + dropzonePlaceholder.classList.remove('hidden'); + } + }); + + templatePreviewUrlInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + templatePreviewUrlInput.blur(); + } else if (e.key === 'Escape') { + templatePreviewUrlInput.value = ''; + templatePreviewUrlInput.blur(); + } + }); + } + + templatePreviewDropzone.addEventListener('dragover', (e) => { + e.preventDefault(); + templatePreviewDropzone.classList.add('drag-over'); + }); + + templatePreviewDropzone.addEventListener('dragleave', (e) => { + e.preventDefault(); + templatePreviewDropzone.classList.remove('drag-over'); + }); + + templatePreviewDropzone.addEventListener('drop', (e) => { + e.preventDefault(); + templatePreviewDropzone.classList.remove('drag-over'); + + const files = e.dataTransfer.files; + if (files.length > 0) { + const file = files[0]; + if (file.type.startsWith('image/')) { + currentPreviewFile = file; + const objectUrl = URL.createObjectURL(file); + templatePreviewImg.src = objectUrl; + templatePreviewImg.classList.remove('hidden'); + dropzonePlaceholder.classList.add('hidden'); + templatePreviewUrlInput.classList.add('hidden'); + isUrlInputMode = false; + } + } + }); + } + + // Save template + if (saveTemplateBtn) { + saveTemplateBtn.addEventListener('click', async () => { + const title = templateTitleInput.value.trim(); + const prompt = templatePromptInput.value.trim(); + const mode = templateModeSelect.value; + let category = templateCategorySelect.value; + + if (category === 'new') { + category = templateCategoryInput.value.trim(); + } + + if (!title) { + alert('Please enter a title for the template.'); + return; + } + if (!prompt) { + alert('Please enter a prompt.'); + return; + } + if (!category) { + alert('Please select or enter a category.'); + return; + } + + saveTemplateBtn.disabled = true; + saveTemplateBtn.textContent = editingTemplate ? 'Updating...' : 'Saving...'; + + try { + const formData = new FormData(); + formData.append('title', title); + formData.append('prompt', prompt); + formData.append('mode', mode); + formData.append('category', category); + + if (currentPreviewFile) { + formData.append('preview', currentPreviewFile); + } else if (currentPreviewUrl) { + formData.append('preview_path', currentPreviewUrl); + } + + // If editing, add the template index + const endpoint = editingTemplate ? '/update_template' : '/save_template'; + if (editingTemplate) { + formData.append('template_index', editingTemplate.userTemplateIndex); + } + + const response = await fetch(endpoint, { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + if (data.error) { + throw new Error(data.error); + } + + // Success + createTemplateModal.classList.add('hidden'); + + // Reload template gallery + await templateGallery.load(); + + // Reset editing state + editingTemplate = null; + + } catch (error) { + alert(`Failed to ${editingTemplate ? 'update' : 'save'} template: ` + error.message); + } finally { + saveTemplateBtn.disabled = false; + saveTemplateBtn.innerHTML = 'Save Template
'; + } + }); + } + + // Close modal when clicking outside + if (createTemplateModal) { + createTemplateModal.addEventListener('click', (e) => { + if (e.target === createTemplateModal) { + createTemplateModal.classList.add('hidden'); + } + }); + } + document.addEventListener('pointermove', handleCanvasPointerMove); document.addEventListener('pointerup', () => { if (isPanning && imageDisplayArea) { diff --git a/static/style.css b/static/style.css index e9bab27..e52cd5a 100644 --- a/static/style.css +++ b/static/style.css @@ -1251,3 +1251,200 @@ button#generate-btn:disabled { max-height: 140px; } } + +/* Template Create Modal */ +.template-form-grid { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.form-row { + display: flex; + gap: 1rem; +} + +.form-row .form-group { + flex: 1; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-group label { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + margin-left: 0.25rem; +} + +.template-preview-dropzone { + width: 100%; + height: 220px; + border: 2px dashed rgba(255, 255, 255, 0.15); + border-radius: 0.75rem; + background: rgba(0, 0, 0, 0.2); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; + cursor: pointer; + transition: all 0.2s ease; +} + +.template-preview-dropzone:hover { + border-color: var(--accent-color); + background: rgba(251, 191, 36, 0.05); + box-shadow: 0 0 15px rgba(251, 191, 36, 0.1); +} + +.template-preview-dropzone.drag-over { + border-color: var(--accent-hover); + background: rgba(251, 191, 36, 0.1); + transform: scale(0.99); +} + +.template-preview-dropzone img { + width: 100%; + height: 100%; + object-fit: contain; + border-radius: 0.5rem; +} + +.dropzone-placeholder { + color: var(--text-secondary); + font-size: 0.9rem; + pointer-events: none; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.dropzone-placeholder::before { + content: '+'; + font-size: 2rem; + color: var(--accent-color); + opacity: 0.5; +} + +.category-input-wrapper { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +#create-template-modal .popup-card { + background: rgba(10, 11, 22, 0.98); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); +} + +#save-template-btn { + padding: 0.6rem 1.5rem; + background: linear-gradient(135deg, var(--accent-color), var(--accent-hover)); + color: #000; + font-weight: 700; + border: none; + border-radius: 0.5rem; + cursor: pointer; + transition: transform 0.1s, box-shadow 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; +} + +#save-template-btn:hover { + transform: translateY(-1px); + box-shadow: 0 5px 15px rgba(251, 191, 36, 0.3); +} + +#save-template-btn:active { + transform: translateY(0); +} + +.template-preview-url-input { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 90%; + padding: 0.75rem; + background: var(--input-bg); + border: 1px solid var(--accent-color); + border-radius: 0.5rem; + color: var(--text-primary); + font-size: 0.875rem; + z-index: 10; +} + +.template-preview-url-input:focus { + outline: none; + border-color: var(--accent-hover); + box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.2); +} + +.template-edit-btn { + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 32px; + height: 32px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.7); + border: 1px solid rgba(255, 255, 255, 0.2); + color: var(--text-primary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + opacity: 0; + transition: all 0.2s; + z-index: 5; +} + +.template-card:hover .template-edit-btn { + opacity: 1; +} + +.template-edit-btn:hover { + background: var(--accent-color); + color: #000; + border-color: var(--accent-color); + transform: scale(1.1); +} + +.template-edit-btn svg { + width: 16px; + height: 16px; +} + +.template-create-btn { + padding: 0.75rem 1rem; + background: linear-gradient(135deg, var(--accent-color), var(--accent-hover)); + color: #000; + border: none; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + transition: transform 0.1s, box-shadow 0.2s; +} + +.template-create-btn:hover { + transform: translateY(-1px); + box-shadow: 0 5px 15px rgba(251, 191, 36, 0.3); +} + +.template-create-btn svg { + width: 16px; + height: 16px; +} diff --git a/templates/index.html b/templates/index.html index 6271043..d5403f5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -156,6 +156,17 @@ stroke-linejoin="round" /> + + + + +