update filter history
This commit is contained in:
parent
e47805dd16
commit
8df429677c
4 changed files with 333 additions and 68 deletions
|
|
@ -1,7 +1,101 @@
|
||||||
import { withCacheBuster } from './utils.js';
|
import { withCacheBuster } from './utils.js';
|
||||||
import { extractMetadataFromBlob } from './metadata.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 }) {
|
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) {
|
async function readMetadataFromImage(imageUrl) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(withCacheBuster(imageUrl));
|
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() {
|
async function load() {
|
||||||
if (!galleryGrid) return;
|
if (!galleryGrid) return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/gallery?t=${new Date().getTime()}`);
|
const response = await fetch(`/gallery?t=${new Date().getTime()}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
galleryGrid.innerHTML = '';
|
allImages = data.images || [];
|
||||||
|
renderGallery();
|
||||||
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);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load gallery:', 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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
function setViewState(state) {
|
||||||
placeholderState.classList.add('hidden');
|
placeholderState.classList.add('hidden');
|
||||||
loadingState.classList.add('hidden');
|
loadingState.classList.add('hidden');
|
||||||
|
|
|
||||||
|
|
@ -878,8 +878,79 @@ button#generate-btn:disabled {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
text-transform: uppercase;
|
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 {
|
.gallery-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,17 @@
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<section class="history-section">
|
<section class="history-section">
|
||||||
<h3>History</h3>
|
<div class="history-header">
|
||||||
|
<h3>History</h3>
|
||||||
|
<div class="history-filter-group">
|
||||||
|
<input type="text" id="history-search-input" class="history-search-input" placeholder="Day...">
|
||||||
|
<button type="button" class="history-filter-btn active" data-filter="all">All</button>
|
||||||
|
<button type="button" class="history-filter-btn" data-filter="today">Today</button>
|
||||||
|
<button type="button" class="history-filter-btn" data-filter="week">This Week</button>
|
||||||
|
<button type="button" class="history-filter-btn" data-filter="month">This Month</button>
|
||||||
|
<button type="button" class="history-filter-btn" data-filter="year">This Year</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="gallery-grid" class="gallery-grid">
|
<div id="gallery-grid" class="gallery-grid">
|
||||||
<!-- Gallery items will be injected here -->
|
<!-- Gallery items will be injected here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue