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:
|
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
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 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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Set initial active state based on saved filter
|
||||||
const currentFilter = gallery.getCurrentFilter();
|
const currentFilter = gallery.getCurrentFilter();
|
||||||
historyFilterBtns.forEach(btn => {
|
historyFilterBtns.forEach(btn => {
|
||||||
if (btn.dataset.filter === currentFilter) {
|
if (btn.dataset.filter === currentFilter && !btn.classList.contains('history-favorites-btn')) {
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
} else {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add click event listeners
|
// Handle favorites button as toggle
|
||||||
|
if (historyFavoritesBtn) {
|
||||||
|
historyFavoritesBtn.addEventListener('click', () => {
|
||||||
|
const isActive = gallery.toggleFavorites();
|
||||||
|
historyFavoritesBtn.classList.toggle('active', isActive);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date filter buttons
|
||||||
historyFilterBtns.forEach(btn => {
|
historyFilterBtns.forEach(btn => {
|
||||||
|
if (!btn.classList.contains('history-favorites-btn')) {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const filterType = btn.dataset.filter;
|
const filterType = btn.dataset.filter;
|
||||||
|
|
||||||
// Update active state
|
// Remove active from all date filter buttons (not favorites)
|
||||||
historyFilterBtns.forEach(b => b.classList.remove('active'));
|
historyFilterBtns.forEach(b => {
|
||||||
|
if (!b.classList.contains('history-favorites-btn')) {
|
||||||
|
b.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
|
|
||||||
// Apply filter
|
|
||||||
gallery.setFilter(filterType);
|
gallery.setFilter(filterType);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// Setup history search input
|
|
||||||
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
|
||||||
|
|
|
||||||
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);
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue