diff --git a/.DS_Store b/.DS_Store index 3541bb7..fe0b8b9 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 2c49d8b..6261338 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .DS_Store .DS_Store .DS_Store +/.venv diff --git a/run_app.command b/run_app.command new file mode 100755 index 0000000..942f0f1 --- /dev/null +++ b/run_app.command @@ -0,0 +1,17 @@ +# /opt/miniconda3/bin/python is required by the user +#!/bin/zsh +cd "$(dirname "$0")" +PYTHON_BIN="/opt/miniconda3/bin/python" + +# Create a virtual environment if missing, then activate it +if [[ ! -d ".venv" ]]; then + "$PYTHON_BIN" -m venv .venv +fi + +source .venv/bin/activate + +# Ensure dependencies are available (skip reinstall if up-to-date) +pip install -r requirements.txt + +# Start the Flask app on port 8888 +exec .venv/bin/python app.py diff --git a/static/script.js b/static/script.js index ea86617..2205da3 100644 --- a/static/script.js +++ b/static/script.js @@ -23,6 +23,11 @@ document.addEventListener('DOMContentLoaded', () => { let cachedReferenceImages = []; const imageDisplayArea = document.querySelector('.image-display-area'); const canvasToolbar = document.querySelector('.canvas-toolbar'); + const popupOverlay = document.getElementById('popup-overlay'); + const popupTitleEl = document.getElementById('popup-title'); + const popupBodyEl = document.getElementById('popup-body'); + const popupCloseBtn = document.getElementById('popup-close'); + const popupButtons = document.querySelectorAll('[data-popup-target]'); const ZOOM_STEP = 0.1; const MIN_ZOOM = 0.4; const MAX_ZOOM = 4; @@ -31,6 +36,48 @@ document.addEventListener('DOMContentLoaded', () => { let isPanning = false; let lastPointer = { x: 0, y: 0 }; + const infoContent = { + title: 'Thông tin', + sections: [ + { + heading: 'Liên hệ', + items: [ + 'Người tạo: Phạm Hưng', + 'Group: SDVN - Cộng đồng AI Art', + 'Website: sdvn.vn', + ], + }, + ], + }; + + const docsContent = { + title: 'Phím tắt và mẹo', + sections: [ + { + heading: 'Phím tắt', + items: [ + 'Ctrl/Cmd + Enter → tạo ảnh mới', + 'D → tải ảnh hiện tại', + 'R → reset zoom/pan vùng hiển thị ảnh', + 'Esc → đóng popup thông tin/docs', + ], + }, + { + heading: 'Thao tác nhanh', + items: [ + 'Kéo ảnh từ lịch sử vào ô tham chiếu để tái sử dụng', + 'Tùy chỉnh tỉ lệ và độ phân giải trước khi nhấn Generate', + 'API key và prompt được lưu để lần sau không phải nhập lại', + ], + }, + ], + }; + + const POPUP_CONTENT = { + info: infoContent, + docs: docsContent, + }; + // Load gallery on start loadSettings(); initializeImageInputs(); @@ -189,11 +236,20 @@ document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.gallery-item').forEach(el => el.classList.remove('active')); div.classList.add('active'); }; - + const img = document.createElement('img'); img.src = withCacheBuster(imageUrl); img.loading = 'lazy'; - + img.draggable = true; + img.dataset.source = imageUrl; + img.addEventListener('dragstart', event => { + event.dataTransfer?.setData('text/uri-list', imageUrl); + event.dataTransfer?.setData('text/plain', imageUrl); + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'copy'; + } + }); + div.appendChild(img); galleryGrid.appendChild(div); }); @@ -456,12 +512,19 @@ document.addEventListener('DOMContentLoaded', () => { slot.classList.remove('drag-over'); }); - slot.addEventListener('drop', event => { + slot.addEventListener('drop', async event => { event.preventDefault(); slot.classList.remove('drag-over'); const file = event.dataTransfer?.files?.[0]; if (file) { handleSlotFile(index, file); + return; + } + + const imageUrl = event.dataTransfer?.getData('text/uri-list') + || event.dataTransfer?.getData('text/plain'); + if (imageUrl) { + await handleSlotDropFromHistory(index, imageUrl); } }); @@ -493,6 +556,24 @@ document.addEventListener('DOMContentLoaded', () => { reader.readAsDataURL(file); } + async function handleSlotDropFromHistory(index, imageUrl) { + try { + const response = await fetch(withCacheBuster(imageUrl)); + if (!response.ok) { + console.warn('Failed to fetch history image', response.statusText); + return; + } + + const blob = await response.blob(); + const name = imageUrl.split('/').pop()?.split('?')[0] || `history-${index + 1}.png`; + const type = blob.type || 'image/png'; + const file = new File([blob], name, { type }); + handleSlotFile(index, file); + } catch (error) { + console.error('Unable to import history image', error); + } + } + function updateSlotVisual(index) { const slotRecord = imageSlotState[index]; if (!slotRecord) return; @@ -580,4 +661,57 @@ document.addEventListener('DOMContentLoaded', () => { return null; } } + + popupButtons.forEach(button => { + button.addEventListener('click', () => { + const target = button.dataset.popupTarget; + if (target) { + showPopup(target); + } + }); + }); + + if (popupCloseBtn) { + popupCloseBtn.addEventListener('click', closePopup); + } + + if (popupOverlay) { + popupOverlay.addEventListener('click', event => { + if (event.target === popupOverlay) { + closePopup(); + } + }); + } + + document.addEventListener('keydown', event => { + if (event.key === 'Escape' && popupOverlay && !popupOverlay.classList.contains('hidden')) { + event.preventDefault(); + closePopup(); + } + }); + + function showPopup(type) { + const content = POPUP_CONTENT[type]; + if (!content || !popupOverlay || !popupBodyEl || !popupTitleEl) return; + + popupTitleEl.textContent = content.title; + popupBodyEl.innerHTML = content.sections + .map(section => { + const items = (section.items || []).map(item => `