From ae2d5071deb05838ab25ab8c9107a05ccc06fc92 Mon Sep 17 00:00:00 2001 From: phamhungd Date: Fri, 28 Nov 2025 15:02:21 +0700 Subject: [PATCH] update code --- .DS_Store | Bin 10244 -> 10244 bytes __pycache__/app.cpython-311.pyc | Bin 0 -> 57561 bytes app.py | 100 ++++++++++++++++++++++++------- static/modules/gallery.js | 103 ++++++++++++++++++++++++++++++-- static/script.js | 42 +++++++++++++ static/style.css | 47 +++++++++++---- templates/index.html | 7 ++- 7 files changed, 260 insertions(+), 39 deletions(-) create mode 100644 __pycache__/app.cpython-311.pyc diff --git a/.DS_Store b/.DS_Store index ea62dcef066ec716a6b66e4ff5ce26455d6b4cc4..384f77c574e6c1e61d8062b4cebadd2cd298422b 100644 GIT binary patch delta 531 zcmZn(XbG6$F8U^hRb&SV||N9Gs3XD0^;NU)v`ymIB} zEcM(B@jzI>P|1+YkO;&X45=Uy&z$_^q@4UD1_lNJ1_qWzKw9rV7ywxe3^;VmLJVI?ubu$3RpRDstYoZo$-NT^GXphR+Y&q zZ9wg848;s3K%bT{WHKbfTmdqcfuW({*yP8Oip&jfk4|QnQec{yFj-UTAXC$!$qv$D pf@o@Y?wxEaExB1o`YZEhc7@+8^*}Qii1ISXGV1%755vo)OaQUukx&2t delta 123 zcmZn(XbG6$mJU^hRb`eYsfN9IS#U6TU@Bv=K{PhmJbIafd$%IX!6VOqwpd9?r| z%jQXkWX;#5xe(8fujbfAkNJ~t9CC$UkaC^_@ WpVE9R6C0jwW>@&lviXH56Egt#Ln^}n diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..075c1153b90b0c7ecc7a670ff203b1a89e3d09a9 GIT binary patch literal 57561 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 P+(&JpQH%Lwfd~B$Qn;AC literal 0 HcmV?d00001 diff --git a/app.py b/app.py index 7cee721..7c44c5e 100644 --- a/app.py +++ b/app.py @@ -191,6 +191,47 @@ os.makedirs(GENERATED_DIR, exist_ok=True) # Ensure uploads directory exists UPLOADS_DIR = os.path.join(app.static_folder, 'uploads') os.makedirs(UPLOADS_DIR, exist_ok=True) +ALLOWED_GALLERY_EXTS = ('.png', '.jpg', '.jpeg', '.webp') + + +def normalize_gallery_path(path): + """Return a clean path relative to /static without traversal.""" + if not path: + return '' + cleaned = path.replace('\\', '/') + cleaned = cleaned.split('?', 1)[0] + if cleaned.startswith('/'): + cleaned = cleaned[1:] + if cleaned.startswith('static/'): + cleaned = cleaned[len('static/'):] + normalized = os.path.normpath(cleaned) + if normalized.startswith('..'): + return '' + return normalized + + +def resolve_gallery_target(source, filename=None, relative_path=None): + """Resolve the gallery source (generated/uploads) and absolute filepath.""" + cleaned_path = normalize_gallery_path(relative_path) + candidate_name = cleaned_path or (filename or '') + if not candidate_name: + return None, None, None + + normalized_name = os.path.basename(candidate_name) + + inferred_source = (source or '').lower() + if cleaned_path: + first_segment = cleaned_path.split('/')[0] + if first_segment in ('generated', 'uploads'): + inferred_source = first_segment + + if inferred_source not in ('generated', 'uploads'): + inferred_source = 'generated' + + base_dir = UPLOADS_DIR if inferred_source == 'uploads' else GENERATED_DIR + filepath = os.path.join(base_dir, normalized_name) + storage_key = f"{inferred_source}/{normalized_name}" + return inferred_source, filepath, storage_key def process_prompt_with_placeholders(prompt, note): """ @@ -544,20 +585,29 @@ def generate_image(): @app.route('/delete_image', methods=['POST']) def delete_image(): - data = request.get_json() + data = request.get_json() or {} filename = data.get('filename') - - if not filename: + source = data.get('source') + rel_path = data.get('path') or data.get('relative_path') + + resolved_source, filepath, storage_key = resolve_gallery_target(source, filename, rel_path) + if not filepath: return jsonify({'error': 'Filename is required'}), 400 - - # Security check: ensure filename is just a basename, no paths - filename = os.path.basename(filename) - filepath = os.path.join(GENERATED_DIR, filename) if os.path.exists(filepath): try: send2trash(filepath) - return jsonify({'success': True}) + + # Clean up favorites entry if it exists + favorites = load_gallery_favorites() + cleaned_favorites = [ + item for item in favorites + if item != storage_key and item != os.path.basename(filepath) + ] + if cleaned_favorites != favorites: + save_gallery_favorites(cleaned_favorites) + + return jsonify({'success': True, 'source': resolved_source}) except Exception as e: return jsonify({'error': str(e)}), 500 else: @@ -565,12 +615,19 @@ def delete_image(): @app.route('/gallery') def get_gallery(): - # List all png files in generated dir, sorted by modification time (newest first) - files = glob.glob(os.path.join(GENERATED_DIR, '*.png')) + # List all images in the chosen source directory, sorted by modification time (newest first) + source_param = (request.args.get('source') or 'generated').lower() + base_dir = UPLOADS_DIR if source_param == 'uploads' else GENERATED_DIR + resolved_source = 'uploads' if base_dir == UPLOADS_DIR else 'generated' + + files = [ + f for f in glob.glob(os.path.join(base_dir, '*')) + if os.path.splitext(f)[1].lower() in ALLOWED_GALLERY_EXTS + ] files.sort(key=os.path.getmtime, reverse=True) - image_urls = [url_for('static', filename=f'generated/{os.path.basename(f)}') for f in files] - response = jsonify({'images': image_urls}) + image_urls = [url_for('static', filename=f'{resolved_source}/{os.path.basename(f)}') for f in files] + response = jsonify({'images': image_urls, 'source': resolved_source}) response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" return response @@ -652,22 +709,25 @@ def get_gallery_favorites(): def toggle_gallery_favorite(): data = request.get_json() or {} filename = data.get('filename') + source = data.get('source') + rel_path = data.get('path') or data.get('relative_path') - if not filename: + resolved_source, _, storage_key = resolve_gallery_target(source, filename, rel_path) + if not storage_key: return jsonify({'error': 'Filename is required'}), 400 - # Security: ensure filename is just a basename - filename = os.path.basename(filename) - favorites = load_gallery_favorites() + legacy_key = os.path.basename(storage_key) - if filename in favorites: - favorites = [item for item in favorites if item != filename] + if storage_key in favorites or legacy_key in favorites: + favorites = [item for item in favorites if item not in (storage_key, legacy_key)] + is_favorite = False else: - favorites.append(filename) + favorites.append(storage_key) + is_favorite = True save_gallery_favorites(favorites) - return jsonify({'favorites': favorites, 'is_favorite': filename in favorites}) + return jsonify({'favorites': favorites, 'is_favorite': is_favorite, 'source': resolved_source}) @app.route('/save_template', methods=['POST']) def save_template(): diff --git a/static/modules/gallery.js b/static/modules/gallery.js index 6f6acb4..b99e730 100644 --- a/static/modules/gallery.js +++ b/static/modules/gallery.js @@ -3,10 +3,13 @@ import { extractMetadataFromBlob } from './metadata.js'; const FILTER_STORAGE_KEY = 'gemini-app-history-filter'; const SEARCH_STORAGE_KEY = 'gemini-app-history-search'; +const SOURCE_STORAGE_KEY = 'gemini-app-history-source'; +const VALID_SOURCES = ['generated', 'uploads']; export function createGallery({ galleryGrid, onSelect }) { let currentFilter = 'all'; let searchQuery = ''; + let currentSource = 'generated'; let allImages = []; let favorites = []; let showOnlyFavorites = false; // New toggle state @@ -18,6 +21,11 @@ export function createGallery({ galleryGrid, onSelect }) { const savedSearch = localStorage.getItem(SEARCH_STORAGE_KEY); if (savedSearch) searchQuery = savedSearch; + + const savedSource = localStorage.getItem(SOURCE_STORAGE_KEY); + if (savedSource && VALID_SOURCES.includes(savedSource)) { + currentSource = savedSource; + } } catch (e) { console.warn('Failed to load history filter/search', e); } @@ -33,9 +41,34 @@ export function createGallery({ galleryGrid, onSelect }) { } } - function isFavorite(imageUrl) { + function extractRelativePath(imageUrl) { + if (!imageUrl) return ''; + try { + const url = new URL(imageUrl, window.location.origin); + const path = url.pathname; + const staticIndex = path.indexOf('/static/'); + if (staticIndex !== -1) { + return path.slice(staticIndex + '/static/'.length).replace(/^\//, ''); + } + return path.replace(/^\//, ''); + } catch (error) { + const parts = imageUrl.split('/static/'); + if (parts[1]) return parts[1].split('?')[0]; + return imageUrl.split('/').pop().split('?')[0]; + } + } + + function getFavoriteKey(imageUrl) { + const relative = extractRelativePath(imageUrl); + if (relative) return relative; const filename = imageUrl.split('/').pop().split('?')[0]; - return favorites.includes(filename); + return `${currentSource}/${filename}`; + } + + function isFavorite(imageUrl) { + const key = getFavoriteKey(imageUrl); + const filename = imageUrl.split('/').pop().split('?')[0]; + return favorites.includes(key) || favorites.includes(filename); } // Date comparison utilities @@ -121,12 +154,17 @@ export function createGallery({ galleryGrid, onSelect }) { async function toggleFavorite(imageUrl) { const filename = imageUrl.split('/').pop().split('?')[0]; + const relativePath = extractRelativePath(imageUrl); try { const response = await fetch('/toggle_gallery_favorite', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename }) + body: JSON.stringify({ + filename, + path: relativePath, + source: currentSource + }) }); const data = await response.json(); @@ -219,11 +257,16 @@ export function createGallery({ galleryGrid, onSelect }) { const filename = imageUrl.split('/').pop().split('?')[0]; + const relativePath = extractRelativePath(imageUrl); try { const res = await fetch('/delete_image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename }) + body: JSON.stringify({ + filename, + path: relativePath, + source: currentSource + }) }); if (res.ok) { @@ -251,7 +294,7 @@ export function createGallery({ galleryGrid, onSelect }) { if (!galleryGrid) return; try { await loadFavorites(); - const response = await fetch(`/gallery?t=${new Date().getTime()}`); + const response = await fetch(`/gallery?source=${currentSource}&t=${new Date().getTime()}`); const data = await response.json(); allImages = data.images || []; renderGallery(); @@ -293,6 +336,28 @@ export function createGallery({ galleryGrid, onSelect }) { return showOnlyFavorites; } + function setSource(source, { resetFilters = false } = {}) { + const normalized = VALID_SOURCES.includes(source) ? source : 'generated'; + currentSource = normalized; + try { + localStorage.setItem(SOURCE_STORAGE_KEY, currentSource); + } catch (e) { + console.warn('Failed to save history source', e); + } + if (resetFilters) { + currentFilter = 'all'; + showOnlyFavorites = false; + searchQuery = ''; + try { + localStorage.setItem(FILTER_STORAGE_KEY, currentFilter); + localStorage.setItem(SEARCH_STORAGE_KEY, searchQuery); + } catch (e) { + console.warn('Failed to reset history filters', e); + } + } + return load(); + } + function getCurrentFilter() { return currentFilter; } @@ -301,10 +366,24 @@ export function createGallery({ galleryGrid, onSelect }) { return searchQuery; } + function getCurrentSource() { + return currentSource; + } + function isFavoritesActive() { return showOnlyFavorites; } + function setFavoritesActive(active) { + showOnlyFavorites = Boolean(active); + renderGallery(); + return showOnlyFavorites; + } + + function setSearchQuery(value) { + setSearch(value); + } + function navigate(direction) { const activeItem = galleryGrid.querySelector('.gallery-item.active'); @@ -343,5 +422,17 @@ export function createGallery({ galleryGrid, onSelect }) { } }); - return { load, setFilter, getCurrentFilter, setSearch, getSearchQuery, toggleFavorites, isFavoritesActive }; + return { + load, + setFilter, + getCurrentFilter, + setSearch, + getSearchQuery, + toggleFavorites, + isFavoritesActive, + setSource, + getCurrentSource, + setFavoritesActive, + setSearchQuery + }; } diff --git a/static/script.js b/static/script.js index c61fb20..c3eb5df 100644 --- a/static/script.js +++ b/static/script.js @@ -1588,6 +1588,48 @@ document.addEventListener('DOMContentLoaded', () => { // Setup history filter buttons const historyFilterBtns = document.querySelectorAll('.history-filter-btn'); const historyFavoritesBtn = document.querySelector('.history-favorites-btn'); + const historySourceBtns = document.querySelectorAll('.history-source-btn'); + const initialSource = gallery.getCurrentSource ? gallery.getCurrentSource() : 'generated'; + + historySourceBtns.forEach(btn => { + const isActive = btn.dataset.source === initialSource; + btn.classList.toggle('active', isActive); + btn.setAttribute('aria-pressed', String(isActive)); + + btn.addEventListener('click', async () => { + const targetSource = btn.dataset.source || 'generated'; + historySourceBtns.forEach(b => { + const active = b === btn; + b.classList.toggle('active', active); + b.setAttribute('aria-pressed', String(active)); + }); + await gallery.setSource(targetSource, { resetFilters: true }); + + // Reset filters UI to show all when switching source + historyFilterBtns.forEach(b => { + if (!b.classList.contains('history-favorites-btn')) { + b.classList.toggle('active', b.dataset.filter === 'all'); + } + }); + + // Disable favorites toggle on source change + if (historyFavoritesBtn) { + historyFavoritesBtn.classList.remove('active'); + } + if (gallery.setFavoritesActive) { + gallery.setFavoritesActive(false); + } + + // Clear search box + const historySearchInputEl = document.getElementById('history-search-input'); + if (historySearchInputEl) { + historySearchInputEl.value = ''; + } + if (gallery.setSearchQuery) { + gallery.setSearchQuery(''); + } + }); + }); // Set initial active state based on saved filter const currentFilter = gallery.getCurrentFilter(); diff --git a/static/style.css b/static/style.css index f0d56eb..119aa25 100644 --- a/static/style.css +++ b/static/style.css @@ -1051,16 +1051,6 @@ button#generate-btn:disabled { background: var(--panel-backdrop); } -.history-section h3 { - font-size: 0.875rem; - color: var(--text-secondary); - margin-bottom: 0.5rem; - font-weight: 600; - letter-spacing: 0.5px; - text-transform: uppercase; - margin: 0; -} - .history-header { display: flex; justify-content: space-between; @@ -1069,6 +1059,39 @@ button#generate-btn:disabled { gap: 0.75rem; } +.history-source-toggle { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.history-source-btn { + padding: 0.4rem 0.9rem; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.5px; + text-transform: uppercase; + background: rgba(255, 255, 255, 0.05); + color: var(--text-secondary); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.history-source-btn:hover { + color: var(--text-primary); + border-color: rgba(255, 255, 255, 0.25); + background: rgba(255, 255, 255, 0.08); +} + +.history-source-btn.active { + background: linear-gradient(135deg, var(--accent-color), var(--accent-hover)); + color: #111; + box-shadow: 0 4px 14px rgba(251, 191, 36, 0.35); + border-color: transparent; +} + .history-filter-group { display: flex; gap: 0.15rem; @@ -1149,7 +1172,7 @@ button#generate-btn:disabled { transition: all 0.2s; flex-shrink: 0; background: rgba(255, 255, 255, 0.02); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5); + box-shadow: 0 0px 0px rgba(0, 0, 0, 0.5); } .gallery-item:hover { @@ -1158,7 +1181,7 @@ button#generate-btn:disabled { .gallery-item.active { border-color: var(--accent-color); - box-shadow: 0 0 25px rgba(251, 191, 36, 0.4); + box-shadow: 0 0 10px rgba(251, 191, 36, 0.4); } /* New styles start here */ diff --git a/templates/index.html b/templates/index.html index 19808d8..94f714c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -241,7 +241,12 @@
-

History

+
+ + +