From 8df429677c84d7d641407a8a57ac467a9e623fc8 Mon Sep 17 00:00:00 2001 From: phamhungd Date: Tue, 25 Nov 2025 22:23:15 +0700 Subject: [PATCH] update filter history --- static/modules/gallery.js | 273 ++++++++++++++++++++++++++++---------- static/script.js | 45 +++++++ static/style.css | 71 ++++++++++ templates/index.html | 12 +- 4 files changed, 333 insertions(+), 68 deletions(-) diff --git a/static/modules/gallery.js b/static/modules/gallery.js index 35ff8fd..ffdef1a 100644 --- a/static/modules/gallery.js +++ b/static/modules/gallery.js @@ -1,7 +1,101 @@ import { withCacheBuster } from './utils.js'; import { extractMetadataFromBlob } from './metadata.js'; +const FILTER_STORAGE_KEY = 'gemini-app-history-filter'; +const SEARCH_STORAGE_KEY = 'gemini-app-history-search'; + export function createGallery({ galleryGrid, onSelect }) { + let currentFilter = 'all'; + let searchQuery = ''; + let allImages = []; + + // Load saved filter and search from localStorage + try { + const savedFilter = localStorage.getItem(FILTER_STORAGE_KEY); + if (savedFilter) currentFilter = savedFilter; + + const savedSearch = localStorage.getItem(SEARCH_STORAGE_KEY); + if (savedSearch) searchQuery = savedSearch; + } catch (e) { + console.warn('Failed to load history filter/search', e); + } + + // Date comparison utilities + function getFileTimestamp(imageUrl) { + // Extract date from filename format: model_yyyymmdd_id.png + // Example: gemini-3-pro-image-preview_20251125_1.png + const filename = imageUrl.split('/').pop().split('?')[0]; + const match = filename.match(/_(\d{8})_/); // Match yyyymmdd between underscores + if (match) { + const dateStr = match[1]; // e.g., "20251125" + const year = parseInt(dateStr.substring(0, 4), 10); + const month = parseInt(dateStr.substring(4, 6), 10) - 1; // Month is 0-indexed + const day = parseInt(dateStr.substring(6, 8), 10); + return new Date(year, month, day).getTime(); + } + return null; + } + + function isToday(timestamp) { + if (!timestamp) return false; + const date = new Date(timestamp); + const today = new Date(); + return date.toDateString() === today.toDateString(); + } + + function isThisWeek(timestamp) { + if (!timestamp) return false; + const date = new Date(timestamp); + const today = new Date(); + const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); + return date >= weekAgo && date <= today; + } + + function isThisMonth(timestamp) { + if (!timestamp) return false; + const date = new Date(timestamp); + const today = new Date(); + return date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear(); + } + + function isThisYear(timestamp) { + if (!timestamp) return false; + const date = new Date(timestamp); + const today = new Date(); + return date.getFullYear() === today.getFullYear(); + } + + function matchesSearch(imageUrl) { + if (!searchQuery) return true; + const filename = imageUrl.split('/').pop().split('?')[0]; + return filename.toLowerCase().includes(searchQuery.toLowerCase()); + } + + function shouldShowImage(imageUrl) { + // First check text search + if (!matchesSearch(imageUrl)) return false; + + // Then check date filter + if (currentFilter === 'all') return true; + + const timestamp = getFileTimestamp(imageUrl); + if (!timestamp) return currentFilter === 'all'; + + switch (currentFilter) { + case 'today': + return isToday(timestamp); + case 'week': + return isThisWeek(timestamp); + case 'month': + return isThisMonth(timestamp); + case 'year': + return isThisYear(timestamp); + default: + return true; + } + } + async function readMetadataFromImage(imageUrl) { try { const response = await fetch(withCacheBuster(imageUrl)); @@ -14,81 +108,126 @@ export function createGallery({ galleryGrid, onSelect }) { } } + function renderGallery() { + if (!galleryGrid) return; + galleryGrid.innerHTML = ''; + + const filteredImages = allImages.filter(shouldShowImage); + + filteredImages.forEach(imageUrl => { + const div = document.createElement('div'); + div.className = 'gallery-item'; + + // Image container for positioning + div.style.position = 'relative'; + + const img = document.createElement('img'); + img.src = withCacheBuster(imageUrl); + img.loading = 'lazy'; + img.draggable = true; + img.dataset.source = imageUrl; + + // Click to select + div.addEventListener('click', async (e) => { + // Don't select if clicking delete button + if (e.target.closest('.delete-btn')) return; + + const metadata = await readMetadataFromImage(imageUrl); + await onSelect?.({ imageUrl, metadata }); + const siblings = galleryGrid.querySelectorAll('.gallery-item'); + siblings.forEach(el => el.classList.remove('active')); + div.classList.add('active'); + }); + + img.addEventListener('dragstart', event => { + event.dataTransfer?.setData('text/uri-list', imageUrl); + event.dataTransfer?.setData('text/plain', imageUrl); + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'copy'; + } + }); + + // Delete button + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'delete-btn'; + deleteBtn.innerHTML = '×'; + deleteBtn.title = 'Delete image'; + deleteBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + + + const filename = imageUrl.split('/').pop().split('?')[0]; + try { + const res = await fetch('/delete_image', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename }) + }); + + if (res.ok) { + div.remove(); + // Also remove from allImages array + allImages = allImages.filter(url => url !== imageUrl); + } else { + console.error('Failed to delete image'); + } + } catch (err) { + console.error('Error deleting image:', err); + } + }); + + div.appendChild(img); + div.appendChild(deleteBtn); + galleryGrid.appendChild(div); + }); + } + async function load() { if (!galleryGrid) return; try { const response = await fetch(`/gallery?t=${new Date().getTime()}`); const data = await response.json(); - galleryGrid.innerHTML = ''; - - data.images.forEach(imageUrl => { - const div = document.createElement('div'); - div.className = 'gallery-item'; - - // Image container for positioning - div.style.position = 'relative'; - - const img = document.createElement('img'); - img.src = withCacheBuster(imageUrl); - img.loading = 'lazy'; - img.draggable = true; - img.dataset.source = imageUrl; - - // Click to select - div.addEventListener('click', async (e) => { - // Don't select if clicking delete button - if (e.target.closest('.delete-btn')) return; - - const metadata = await readMetadataFromImage(imageUrl); - await onSelect?.({ imageUrl, metadata }); - const siblings = galleryGrid.querySelectorAll('.gallery-item'); - siblings.forEach(el => el.classList.remove('active')); - div.classList.add('active'); - }); - - img.addEventListener('dragstart', event => { - event.dataTransfer?.setData('text/uri-list', imageUrl); - event.dataTransfer?.setData('text/plain', imageUrl); - if (event.dataTransfer) { - event.dataTransfer.effectAllowed = 'copy'; - } - }); - - // Delete button - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'delete-btn'; - deleteBtn.innerHTML = '×'; - deleteBtn.title = 'Delete image'; - deleteBtn.addEventListener('click', async (e) => { - e.stopPropagation(); - - - const filename = imageUrl.split('/').pop().split('?')[0]; - try { - const res = await fetch('/delete_image', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename }) - }); - - if (res.ok) { - div.remove(); - } else { - console.error('Failed to delete image'); - } - } catch (err) { - console.error('Error deleting image:', err); - } - }); - - div.appendChild(img); - div.appendChild(deleteBtn); - galleryGrid.appendChild(div); - }); + allImages = data.images || []; + renderGallery(); } catch (error) { console.error('Failed to load gallery:', error); } } - return { load }; + function setFilter(filterType) { + if (currentFilter === filterType) return; + currentFilter = filterType; + + // Save to localStorage + try { + localStorage.setItem(FILTER_STORAGE_KEY, filterType); + } catch (e) { + console.warn('Failed to save history filter', e); + } + + renderGallery(); + } + + function setSearch(query) { + searchQuery = query || ''; + + // Save to localStorage + try { + localStorage.setItem(SEARCH_STORAGE_KEY, searchQuery); + } catch (e) { + console.warn('Failed to save history search', e); + } + + renderGallery(); + } + + function getCurrentFilter() { + return currentFilter; + } + + function getSearchQuery() { + return searchQuery; + } + + return { load, setFilter, getCurrentFilter, setSearch, getSearchQuery }; } diff --git a/static/script.js b/static/script.js index 9d4f4eb..194f3cc 100644 --- a/static/script.js +++ b/static/script.js @@ -1192,6 +1192,51 @@ 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'); + } + }); + + // 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); + }); + }); + } + + // Setup history search input + const historySearchInput = document.getElementById('history-search-input'); + if (historySearchInput) { + // Set initial value from saved search + historySearchInput.value = gallery.getSearchQuery(); + + // Search on input with debounce + let searchTimeout; + historySearchInput.addEventListener('input', (e) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + gallery.setSearch(e.target.value); + }, 300); // 300ms debounce + }); + } + + function setViewState(state) { placeholderState.classList.add('hidden'); loadingState.classList.add('hidden'); diff --git a/static/style.css b/static/style.css index 053f7c4..06154f3 100644 --- a/static/style.css +++ b/static/style.css @@ -878,8 +878,79 @@ button#generate-btn:disabled { font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; + margin: 0; } +.history-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + gap: 0.75rem; +} + +.history-filter-group { + display: flex; + gap: 0.15rem; + padding: 0.25rem; + border-radius: 0.5rem; + border: 1px solid rgba(255, 255, 255, 0.08); + align-items: center; +} + +.history-search-input { + padding: 0.35rem 0.75rem; + min-width: 120px; + background: transparent; + border: none; + border-right: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 0; + color: var(--text-primary); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.3px; + transition: background 0.2s; + height:10px; + margin-right:5px; +} + +.history-search-input::placeholder { + color: var(--text-secondary); + font-weight: 500; +} + +.history-search-input:focus { + outline: none; + background: rgba(255, 255, 255, 0.05); +} + +.history-filter-btn { + padding: 0.35rem 0.75rem; + font-size: 0.7rem; + font-weight: 600; + background: transparent; + color: var(--text-secondary); + border: none; + border-radius: 0.35rem; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + letter-spacing: 0.3px; +} + +.history-filter-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: var(--text-primary); +} + +.history-filter-btn.active { + background: linear-gradient(135deg, var(--accent-color), var(--accent-hover)); + color: #111; + box-shadow: 0 2px 8px rgba(251, 191, 36, 0.3); +} + + + .gallery-grid { display: flex; gap: 0.75rem; diff --git a/templates/index.html b/templates/index.html index f1d6468..ff5df60 100644 --- a/templates/index.html +++ b/templates/index.html @@ -236,7 +236,17 @@
-

History

+
+

History

+
+ + + + + + +
+