This commit is contained in:
phamhungd 2025-11-26 11:49:11 +07:00
parent bc44af8fe5
commit 3818841abd
6 changed files with 251 additions and 56 deletions

46
app.py
View file

@ -128,6 +128,26 @@ def save_template_favorites(favorites):
except Exception as e: except Exception as e:
print(f"Failed to persist template favorites: {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): def parse_tags_field(value):
tags = [] tags = []
if isinstance(value, list): if isinstance(value, list):
@ -476,6 +496,32 @@ def template_favorite():
save_template_favorites(favorites) save_template_favorites(favorites)
return jsonify({'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']) @app.route('/save_template', methods=['POST'])
def save_template(): def save_template():
try: try:

9
gallery_favorites.json Normal file
View file

@ -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"
]

View file

@ -8,6 +8,8 @@ export function createGallery({ galleryGrid, onSelect }) {
let currentFilter = 'all'; let currentFilter = 'all';
let searchQuery = ''; let searchQuery = '';
let allImages = []; let allImages = [];
let favorites = [];
let showOnlyFavorites = false; // New toggle state
// Load saved filter and search from localStorage // Load saved filter and search from localStorage
try { try {
@ -20,6 +22,22 @@ export function createGallery({ galleryGrid, onSelect }) {
console.warn('Failed to load history filter/search', e); 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 // Date comparison utilities
function getFileTimestamp(imageUrl) { function getFileTimestamp(imageUrl) {
// Extract date from filename format: model_yyyymmdd_id.png // Extract date from filename format: model_yyyymmdd_id.png
@ -76,6 +94,11 @@ export function createGallery({ galleryGrid, onSelect }) {
// First check text search // First check text search
if (!matchesSearch(imageUrl)) return false; if (!matchesSearch(imageUrl)) return false;
// Check favorites toggle - if enabled, only show favorites
if (showOnlyFavorites && !isFavorite(imageUrl)) {
return false;
}
// Then check date filter // Then check date filter
if (currentFilter === 'all') return true; 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) { async function readMetadataFromImage(imageUrl) {
try { try {
const response = await fetch(withCacheBuster(imageUrl)); const response = await fetch(withCacheBuster(imageUrl));
@ -129,8 +172,8 @@ export function createGallery({ galleryGrid, onSelect }) {
// Click to select // Click to select
div.addEventListener('click', async (e) => { div.addEventListener('click', async (e) => {
// Don't select if clicking delete button // Don't select if clicking delete or favorite button
if (e.target.closest('.delete-btn')) return; if (e.target.closest('.delete-btn') || e.target.closest('.favorite-btn')) return;
const metadata = await readMetadataFromImage(imageUrl); const metadata = await readMetadataFromImage(imageUrl);
await onSelect?.({ imageUrl, metadata }); 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 = `<svg width="16" height="16" viewBox="0 0 24 24" fill="${isFavorite(imageUrl) ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>`;
favoriteBtn.title = isFavorite(imageUrl) ? 'Remove from favorites' : 'Add to favorites';
favoriteBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await toggleFavorite(imageUrl);
});
// Delete button // Delete button
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn'; deleteBtn.className = 'delete-btn';
@ -176,8 +238,11 @@ export function createGallery({ galleryGrid, onSelect }) {
} }
}); });
toolbar.appendChild(favoriteBtn);
toolbar.appendChild(deleteBtn);
div.appendChild(img); div.appendChild(img);
div.appendChild(deleteBtn); div.appendChild(toolbar);
galleryGrid.appendChild(div); galleryGrid.appendChild(div);
}); });
} }
@ -185,6 +250,7 @@ export function createGallery({ galleryGrid, onSelect }) {
async function load() { async function load() {
if (!galleryGrid) return; if (!galleryGrid) return;
try { try {
await loadFavorites();
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();
allImages = data.images || []; allImages = data.images || [];
@ -221,6 +287,12 @@ export function createGallery({ galleryGrid, onSelect }) {
renderGallery(); renderGallery();
} }
function toggleFavorites() {
showOnlyFavorites = !showOnlyFavorites;
renderGallery();
return showOnlyFavorites;
}
function getCurrentFilter() { function getCurrentFilter() {
return currentFilter; return currentFilter;
} }
@ -229,5 +301,9 @@ export function createGallery({ galleryGrid, onSelect }) {
return searchQuery; return searchQuery;
} }
return { load, setFilter, getCurrentFilter, setSearch, getSearchQuery }; function isFavoritesActive() {
return showOnlyFavorites;
}
return { load, setFilter, getCurrentFilter, setSearch, getSearchQuery, toggleFavorites, isFavoritesActive };
} }

View file

@ -1192,33 +1192,41 @@ document.addEventListener('DOMContentLoaded', () => {
// Setup history filter buttons // Setup history filter buttons
const historyFilterBtns = document.querySelectorAll('.history-filter-btn'); const historyFilterBtns = document.querySelectorAll('.history-filter-btn');
if (historyFilterBtns.length > 0) { const historyFavoritesBtn = document.querySelector('.history-favorites-btn');
// 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 // Set initial active state based on saved filter
historyFilterBtns.forEach(btn => { const currentFilter = gallery.getCurrentFilter();
btn.addEventListener('click', () => { historyFilterBtns.forEach(btn => {
const filterType = btn.dataset.filter; if (btn.dataset.filter === currentFilter && !btn.classList.contains('history-favorites-btn')) {
btn.classList.add('active');
// Update active state }
historyFilterBtns.forEach(b => b.classList.remove('active')); });
btn.classList.add('active');
// Handle favorites button as toggle
// Apply filter if (historyFavoritesBtn) {
gallery.setFilter(filterType); 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'); const historySearchInput = document.getElementById('history-search-input');
if (historySearchInput) { if (historySearchInput) {
// Set initial value from saved search // Set initial value from saved search

View file

@ -981,41 +981,89 @@ button#generate-btn:disabled {
box-shadow: 0 0 25px rgba(251, 191, 36, 0.4); 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 { .gallery-item img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; 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 */
.template-browser-box { .template-browser-box {
display: flex; display: flex;

View file

@ -240,6 +240,14 @@
<h3>History</h3> <h3>History</h3>
<div class="history-filter-group"> <div class="history-filter-group">
<input type="text" id="history-search-input" class="history-search-input" placeholder="Day..."> <input type="text" id="history-search-input" class="history-search-input" placeholder="Day...">
<button type="button" class="history-filter-btn history-favorites-btn" data-filter="favorites"
title="Favorites">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
</button>
<button type="button" class="history-filter-btn active" data-filter="all">All</button> <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="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="week">This Week</button>