diff --git a/app.py b/app.py index 05656f4..32c4ba9 100644 --- a/app.py +++ b/app.py @@ -128,6 +128,26 @@ def save_template_favorites(favorites): except Exception as e: print(f"Failed to persist template favorites: {e}") +GALLERY_FAVORITES_FILE = os.path.join(os.path.dirname(__file__), 'gallery_favorites.json') + +def load_gallery_favorites(): + if os.path.exists(GALLERY_FAVORITES_FILE): + try: + with open(GALLERY_FAVORITES_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + if isinstance(data, list): + return [item for item in data if isinstance(item, str)] + except json.JSONDecodeError: + pass + return [] + +def save_gallery_favorites(favorites): + try: + with open(GALLERY_FAVORITES_FILE, 'w', encoding='utf-8') as f: + json.dump(favorites, f, indent=4, ensure_ascii=False) + except Exception as e: + print(f"Failed to persist gallery favorites: {e}") + def parse_tags_field(value): tags = [] if isinstance(value, list): @@ -476,6 +496,32 @@ def template_favorite(): save_template_favorites(favorites) return jsonify({'favorites': favorites}) +@app.route('/gallery_favorites', methods=['GET']) +def get_gallery_favorites(): + favorites = load_gallery_favorites() + return jsonify({'favorites': favorites}) + +@app.route('/toggle_gallery_favorite', methods=['POST']) +def toggle_gallery_favorite(): + data = request.get_json() or {} + filename = data.get('filename') + + if not filename: + return jsonify({'error': 'Filename is required'}), 400 + + # Security: ensure filename is just a basename + filename = os.path.basename(filename) + + favorites = load_gallery_favorites() + + if filename in favorites: + favorites = [item for item in favorites if item != filename] + else: + favorites.append(filename) + + save_gallery_favorites(favorites) + return jsonify({'favorites': favorites, 'is_favorite': filename in favorites}) + @app.route('/save_template', methods=['POST']) def save_template(): try: diff --git a/gallery_favorites.json b/gallery_favorites.json new file mode 100644 index 0000000..800a1b4 --- /dev/null +++ b/gallery_favorites.json @@ -0,0 +1,9 @@ +[ + "gemini-3-pro-image-preview_20251126_12.png", + "gemini-3-pro-image-preview_20251125_46.png", + "gemini-3-pro-image-preview_20251125_42.png", + "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" +] \ No newline at end of file diff --git a/static/modules/gallery.js b/static/modules/gallery.js index ffdef1a..28a4c45 100644 --- a/static/modules/gallery.js +++ b/static/modules/gallery.js @@ -8,6 +8,8 @@ export function createGallery({ galleryGrid, onSelect }) { let currentFilter = 'all'; let searchQuery = ''; let allImages = []; + let favorites = []; + let showOnlyFavorites = false; // New toggle state // Load saved filter and search from localStorage try { @@ -20,6 +22,22 @@ export function createGallery({ galleryGrid, onSelect }) { console.warn('Failed to load history filter/search', e); } + // Load favorites from backend + async function loadFavorites() { + try { + const response = await fetch('/gallery_favorites'); + const data = await response.json(); + favorites = data.favorites || []; + } catch (error) { + console.warn('Failed to load gallery favorites', error); + } + } + + function isFavorite(imageUrl) { + const filename = imageUrl.split('/').pop().split('?')[0]; + return favorites.includes(filename); + } + // Date comparison utilities function getFileTimestamp(imageUrl) { // Extract date from filename format: model_yyyymmdd_id.png @@ -76,6 +94,11 @@ export function createGallery({ galleryGrid, onSelect }) { // First check text search if (!matchesSearch(imageUrl)) return false; + // Check favorites toggle - if enabled, only show favorites + if (showOnlyFavorites && !isFavorite(imageUrl)) { + return false; + } + // Then check date filter if (currentFilter === 'all') return true; @@ -96,6 +119,26 @@ export function createGallery({ galleryGrid, onSelect }) { } } + async function toggleFavorite(imageUrl) { + const filename = imageUrl.split('/').pop().split('?')[0]; + + try { + const response = await fetch('/toggle_gallery_favorite', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename }) + }); + + const data = await response.json(); + if (data.favorites) { + favorites = data.favorites; + renderGallery(); + } + } catch (error) { + console.error('Failed to toggle favorite', error); + } + } + async function readMetadataFromImage(imageUrl) { try { const response = await fetch(withCacheBuster(imageUrl)); @@ -129,8 +172,8 @@ export function createGallery({ galleryGrid, onSelect }) { // Click to select div.addEventListener('click', async (e) => { - // Don't select if clicking delete button - if (e.target.closest('.delete-btn')) return; + // Don't select if clicking delete or favorite button + if (e.target.closest('.delete-btn') || e.target.closest('.favorite-btn')) return; const metadata = await readMetadataFromImage(imageUrl); await onSelect?.({ imageUrl, metadata }); @@ -147,6 +190,25 @@ export function createGallery({ galleryGrid, onSelect }) { } }); + // Toolbar for buttons + const toolbar = document.createElement('div'); + toolbar.className = 'gallery-item-toolbar'; + + // Favorite button + const favoriteBtn = document.createElement('button'); + favoriteBtn.className = 'favorite-btn'; + if (isFavorite(imageUrl)) { + favoriteBtn.classList.add('active'); + } + favoriteBtn.innerHTML = ` + + `; + favoriteBtn.title = isFavorite(imageUrl) ? 'Remove from favorites' : 'Add to favorites'; + favoriteBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + await toggleFavorite(imageUrl); + }); + // Delete button const deleteBtn = document.createElement('button'); deleteBtn.className = 'delete-btn'; @@ -176,8 +238,11 @@ export function createGallery({ galleryGrid, onSelect }) { } }); + toolbar.appendChild(favoriteBtn); + toolbar.appendChild(deleteBtn); + div.appendChild(img); - div.appendChild(deleteBtn); + div.appendChild(toolbar); galleryGrid.appendChild(div); }); } @@ -185,6 +250,7 @@ export function createGallery({ galleryGrid, onSelect }) { async function load() { if (!galleryGrid) return; try { + await loadFavorites(); const response = await fetch(`/gallery?t=${new Date().getTime()}`); const data = await response.json(); allImages = data.images || []; @@ -221,6 +287,12 @@ export function createGallery({ galleryGrid, onSelect }) { renderGallery(); } + function toggleFavorites() { + showOnlyFavorites = !showOnlyFavorites; + renderGallery(); + return showOnlyFavorites; + } + function getCurrentFilter() { return currentFilter; } @@ -229,5 +301,9 @@ export function createGallery({ galleryGrid, onSelect }) { return searchQuery; } - return { load, setFilter, getCurrentFilter, setSearch, getSearchQuery }; + function isFavoritesActive() { + return showOnlyFavorites; + } + + return { load, setFilter, getCurrentFilter, setSearch, getSearchQuery, toggleFavorites, isFavoritesActive }; } diff --git a/static/script.js b/static/script.js index 25684b1..814e39e 100644 --- a/static/script.js +++ b/static/script.js @@ -1192,33 +1192,41 @@ document.addEventListener('DOMContentLoaded', () => { // Setup history filter buttons const historyFilterBtns = document.querySelectorAll('.history-filter-btn'); - if (historyFilterBtns.length > 0) { - // Set initial active state based on saved filter - const currentFilter = gallery.getCurrentFilter(); - historyFilterBtns.forEach(btn => { - if (btn.dataset.filter === currentFilter) { - btn.classList.add('active'); - } else { - btn.classList.remove('active'); - } - }); + const historyFavoritesBtn = document.querySelector('.history-favorites-btn'); - // Add click event listeners - historyFilterBtns.forEach(btn => { - btn.addEventListener('click', () => { - const filterType = btn.dataset.filter; - - // Update active state - historyFilterBtns.forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - - // Apply filter - gallery.setFilter(filterType); - }); + // Set initial active state based on saved filter + const currentFilter = gallery.getCurrentFilter(); + historyFilterBtns.forEach(btn => { + if (btn.dataset.filter === currentFilter && !btn.classList.contains('history-favorites-btn')) { + btn.classList.add('active'); + } + }); + + // Handle favorites button as toggle + if (historyFavoritesBtn) { + historyFavoritesBtn.addEventListener('click', () => { + const isActive = gallery.toggleFavorites(); + historyFavoritesBtn.classList.toggle('active', isActive); }); } - // Setup history search input + // Handle date filter buttons + historyFilterBtns.forEach(btn => { + if (!btn.classList.contains('history-favorites-btn')) { + btn.addEventListener('click', () => { + const filterType = btn.dataset.filter; + + // Remove active from all date filter buttons (not favorites) + historyFilterBtns.forEach(b => { + if (!b.classList.contains('history-favorites-btn')) { + b.classList.remove('active'); + } + }); + btn.classList.add('active'); + gallery.setFilter(filterType); + }); + } + }); const historySearchInput = document.getElementById('history-search-input'); if (historySearchInput) { // Set initial value from saved search diff --git a/static/style.css b/static/style.css index 06154f3..e0afab3 100644 --- a/static/style.css +++ b/static/style.css @@ -981,41 +981,89 @@ button#generate-btn:disabled { box-shadow: 0 0 25px rgba(251, 191, 36, 0.4); } +/* New styles start here */ +.gallery-item-toolbar { + position: absolute; + top: 6px; + right: 6px; + display: flex; + gap: 4px; + opacity: 0; + transition: opacity 0.2s; + z-index: 10; +} + +.gallery-item:hover .gallery-item-toolbar { + opacity: 1; +} + +.gallery-item .favorite-btn, +.gallery-item .delete-btn { + width: 24px; + height: 24px; + border-radius: 50%; + border: none; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(8px); + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + font-size: 14px; + line-height: 1; + padding: 0; +} + +.gallery-item .favorite-btn svg { + width: 12px; + height: 12px; +} + +.gallery-item .favorite-btn { + color: rgba(255, 255, 255, 0.7); +} + +.gallery-item .favorite-btn.active { + color: #ff6b6b; +} + +.gallery-item .favorite-btn:hover { + background: rgba(0, 0, 0, 0.8); + transform: scale(1.1); +} + +.gallery-item .delete-btn:hover { + background: var(--danger-color); + transform: scale(1.1); +} + +.history-favorites-btn { + min-width: 36px !important; + padding: 0 8px !important; + display: flex; + align-items: center; + justify-content: center; + height: 24.19px; +} + +.history-favorites-btn.active { + background: var(--accent-color) !important; + height: 24.19px; +} + +.history-favorites-btn.active svg { + fill: currentColor; +} +/* New styles end here */ + .gallery-item img { width: 100%; height: 100%; object-fit: cover; } -.gallery-item .delete-btn { - position: absolute; - top: 4px; - right: 4px; - width: 20px; - height: 20px; - border-radius: 50%; - background: rgba(0, 0, 0, 0.6); - color: white; - border: none; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - opacity: 0; - transition: opacity 0.2s, background 0.2s; - font-size: 14px; - line-height: 1; - z-index: 10; -} - -.gallery-item:hover .delete-btn { - opacity: 1; -} - -.gallery-item .delete-btn:hover { - background: rgba(255, 68, 68, 0.9); -} - /* Template Browser Box */ .template-browser-box { display: flex; diff --git a/templates/index.html b/templates/index.html index 35e8978..23d2e8c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -240,6 +240,14 @@

History

+