update
This commit is contained in:
parent
bc44af8fe5
commit
3818841abd
6 changed files with 251 additions and 56 deletions
46
app.py
46
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:
|
||||
|
|
|
|||
9
gallery_favorites.json
Normal file
9
gallery_favorites.json
Normal 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"
|
||||
]
|
||||
|
|
@ -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 = `<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
|
||||
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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
|
||||
// Update active state
|
||||
historyFilterBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
// Apply filter
|
||||
gallery.setFilter(filterType);
|
||||
});
|
||||
// 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
|
||||
|
|
|
|||
106
static/style.css
106
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;
|
||||
|
|
|
|||
|
|
@ -240,6 +240,14 @@
|
|||
<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 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" data-filter="today">Today</button>
|
||||
<button type="button" class="history-filter-btn" data-filter="week">This Week</button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue