diff --git a/.DS_Store b/.DS_Store index 384f77c..19d8a8d 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app.py b/app.py index 7c44c5e..48e7ac1 100644 --- a/app.py +++ b/app.py @@ -554,8 +554,11 @@ def generate_image(): image_url = url_for('static', filename=rel_path) metadata = { - 'prompt': api_prompt, # Store the processed prompt - 'note': '', # Note is already merged into prompt + # Keep the exact user input before placeholder expansion + 'prompt': prompt, + 'note': note, + # Also store the expanded prompt for reference + 'processed_prompt': api_prompt, 'aspect_ratio': aspect_ratio or 'Auto', 'resolution': resolution, 'reference_images': final_reference_paths, diff --git a/gallery_favorites.json b/gallery_favorites.json index 800a1b4..e707baa 100644 --- a/gallery_favorites.json +++ b/gallery_favorites.json @@ -5,5 +5,7 @@ "gemini-3-pro-image-preview_20251125_41.png", "gemini-3-pro-image-preview_20251125_37.png", "gemini-3-pro-image-preview_20251125_26.png", - "gemini-3-pro-image-preview_20251125_24.png" + "gemini-3-pro-image-preview_20251125_24.png", + "generated/gemini-3-pro-image-preview_20251124_10.png", + "generated/gemini-3-pro-image-preview_20251124_9.png" ] \ No newline at end of file diff --git a/static/script.js b/static/script.js index c3eb5df..64e3a5b 100644 --- a/static/script.js +++ b/static/script.js @@ -89,6 +89,7 @@ document.addEventListener('DOMContentLoaded', () => { const apiKeyToggleBtn = document.getElementById('toggle-api-key-visibility'); const apiKeyEyeIcon = apiKeyToggleBtn?.querySelector('.icon-eye'); const apiKeyEyeOffIcon = apiKeyToggleBtn?.querySelector('.icon-eye-off'); + const bodyFontSelect = document.getElementById('body-font'); const placeholderState = document.getElementById('placeholder-state'); const loadingState = document.getElementById('loading-state'); @@ -150,6 +151,10 @@ document.addEventListener('DOMContentLoaded', () => { toggleResolutionVisibility(); } currentTheme = settings.theme || DEFAULT_THEME; + applyBodyFont(settings.bodyFont || DEFAULT_BODY_FONT); + if (bodyFontSelect && settings.bodyFont) { + bodyFontSelect.value = settings.bodyFont; + } return settings; } } catch (e) { @@ -173,6 +178,7 @@ document.addEventListener('DOMContentLoaded', () => { model: apiModelSelect ? apiModelSelect.value : 'gemini-3-pro-image-preview', referenceImages, theme: currentTheme || DEFAULT_THEME, + bodyFont: bodyFontSelect ? bodyFontSelect.value : DEFAULT_BODY_FONT, }; try { localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings)); @@ -216,6 +222,7 @@ document.addEventListener('DOMContentLoaded', () => { ]; const DEFAULT_THEME = 'theme-sdvn'; + const DEFAULT_BODY_FONT = 'Be Vietnam Pro'; const themeOptionsData = [ { id: 'theme-sdvn', name: 'SDVN', gradient: 'linear-gradient(to bottom, #5858e6, #151523)' }, @@ -306,6 +313,65 @@ document.addEventListener('DOMContentLoaded', () => { noteHighlight.scrollLeft = promptNoteInput.scrollLeft; } + function triggerInputUpdate(targetInput) { + if (!targetInput) return; + targetInput.dispatchEvent(new Event('input', { bubbles: true })); + } + + function getFieldInput(target) { + if (target === 'note') return promptNoteInput; + return promptInput; + } + + async function copyToClipboard(text) { + if (!navigator.clipboard?.writeText) { + const temp = document.createElement('textarea'); + temp.value = text; + document.body.appendChild(temp); + temp.select(); + document.execCommand('copy'); + temp.remove(); + return; + } + await navigator.clipboard.writeText(text); + } + + async function pasteIntoInput(targetInput) { + if (!targetInput) return; + if (!navigator.clipboard?.readText) { + alert('Clipboard paste không được hỗ trợ trong trình duyệt này.'); + return; + } + const text = await navigator.clipboard.readText(); + if (!text && text !== '') return; + const start = targetInput.selectionStart ?? targetInput.value.length; + const end = targetInput.selectionEnd ?? start; + targetInput.value = targetInput.value.slice(0, start) + text + targetInput.value.slice(end); + const cursor = start + text.length; + requestAnimationFrame(() => { + targetInput.setSelectionRange(cursor, cursor); + targetInput.focus(); + }); + triggerInputUpdate(targetInput); + } + + async function handleFieldAction(action, target) { + const targetInput = getFieldInput(target); + if (!targetInput) return; + if (action === 'copy') { + await copyToClipboard(targetInput.value); + return; + } + if (action === 'paste') { + await pasteIntoInput(targetInput); + return; + } + if (action === 'clear') { + targetInput.value = ''; + triggerInputUpdate(targetInput); + } + } + function updateThemeSelectionUi() { if (!themeOptionsContainer) return; themeOptionsContainer.querySelectorAll('.theme-option').forEach(btn => { @@ -334,6 +400,19 @@ document.addEventListener('DOMContentLoaded', () => { } } + function applyBodyFont(fontName) { + const fontMap = { + 'Be Vietnam Pro': "'Be Vietnam Pro', sans-serif", + 'Playwrite AU SA': "'Playwrite AU SA', cursive", + 'JetBrains Mono': "'JetBrains Mono', monospace", + }; + const cssFont = fontMap[fontName] || fontMap[DEFAULT_BODY_FONT]; + document.body.style.fontFamily = cssFont; + if (bodyFontSelect && fontName) { + bodyFontSelect.value = fontName; + } + } + function renderThemeOptions(initialTheme) { if (!themeOptionsContainer) return; themeOptionsContainer.innerHTML = ''; @@ -430,6 +509,7 @@ document.addEventListener('DOMContentLoaded', () => { applyThemeClass(currentTheme || DEFAULT_THEME); slotManager.initialize(savedSettings.referenceImages || []); refreshPromptHighlight(); + applyBodyFont(savedSettings.bodyFont || DEFAULT_BODY_FONT); apiKeyInput.addEventListener('input', persistSettings); let isApiKeyVisible = false; @@ -472,8 +552,28 @@ document.addEventListener('DOMContentLoaded', () => { }); aspectRatioInput.addEventListener('change', persistSettings); resolutionInput.addEventListener('change', persistSettings); + if (bodyFontSelect) { + bodyFontSelect.addEventListener('change', () => { + applyBodyFont(bodyFontSelect.value); + persistSettings(); + }); + } window.addEventListener('beforeunload', persistSettings); + document.querySelectorAll('.field-action-btn').forEach(btn => { + btn.addEventListener('click', async () => { + const action = btn.dataset.action; + const target = btn.closest('.field-action-buttons')?.dataset.target; + if (!action || !target) return; + try { + await handleFieldAction(action, target); + } catch (err) { + console.warn('Field action failed', err); + alert('Không thể thực hiện thao tác clipboard. Vui lòng thử lại.'); + } + }); + }); + const queueCounter = document.getElementById('queue-counter'); const queueCountText = document.getElementById('queue-count-text'); @@ -1770,6 +1870,7 @@ document.addEventListener('DOMContentLoaded', () => { } else { promptNoteInput.value = ''; } + refreshNoteHighlight(); if (metadata.aspect_ratio) aspectRatioInput.value = metadata.aspect_ratio; if (metadata.resolution) resolutionInput.value = metadata.resolution; diff --git a/static/style.css b/static/style.css index 119aa25..e07e2ef 100644 --- a/static/style.css +++ b/static/style.css @@ -438,7 +438,7 @@ body.theme-amin { --bd-bg: linear-gradient(to right, #4A00E0, #8E2DE2); } .prompt-highlight { position: absolute; inset: 0; - padding: 0.75rem; + padding: 0.75rem 0.75rem 2.25rem 0.75rem; overflow: auto; pointer-events: none; white-space: pre-wrap; @@ -472,7 +472,7 @@ body.theme-amin { --bd-bg: linear-gradient(to right, #4A00E0, #8E2DE2); } background: transparent; color: transparent; caret-color: var(--accent-color); - padding: 0.75rem; + padding: 0.75rem 0.75rem 2.25rem 0.75rem; font-family: inherit; font-size: 0.875rem; line-height: 1.4; @@ -493,7 +493,7 @@ body.theme-amin { --bd-bg: linear-gradient(to right, #4A00E0, #8E2DE2); } .note-highlight { position: absolute; inset: 0; - padding: 0.75rem; + padding: 0.75rem 0.75rem 2.25rem 0.75rem; overflow: auto; pointer-events: none; white-space: pre-wrap; @@ -526,7 +526,7 @@ body.theme-amin { --bd-bg: linear-gradient(to right, #4A00E0, #8E2DE2); } background: transparent; color: transparent; caret-color: var(--accent-color); - padding: 0.75rem; + padding: 0.75rem 0.75rem 2.25rem 0.75rem; font-family: inherit; font-size: 0.875rem; line-height: 1.4; @@ -536,6 +536,41 @@ body.theme-amin { --bd-bg: linear-gradient(to right, #4A00E0, #8E2DE2); } height: 100%; } +.field-action-buttons { + position: absolute; + right: 0.35rem; + bottom: 0.35rem; + display: inline-flex; + gap: 0.25rem; + padding: 0.1rem; + border-radius: 0.5rem; + z-index: 3; + background: transparent; +} + +.field-action-btn { + border: none; + background: transparent; + color: var(--text-secondary); + width: 24px; + height: 24px; + border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.field-action-btn:hover { + background: rgba(255, 255, 255, 0.06); + color: var(--accent-color); +} + +.field-action-btn:active { + transform: translateY(1px); +} + .theme-option-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); diff --git a/templates/index.html b/templates/index.html index 94f714c..b999d3e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -10,7 +10,7 @@ @@ -43,6 +43,41 @@