update code
This commit is contained in:
parent
77ed116fe7
commit
ae2d5071de
7 changed files with 260 additions and 39 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
BIN
__pycache__/app.cpython-311.pyc
Normal file
BIN
__pycache__/app.cpython-311.pyc
Normal file
Binary file not shown.
100
app.py
100
app.py
|
|
@ -191,6 +191,47 @@ os.makedirs(GENERATED_DIR, exist_ok=True)
|
||||||
# Ensure uploads directory exists
|
# Ensure uploads directory exists
|
||||||
UPLOADS_DIR = os.path.join(app.static_folder, 'uploads')
|
UPLOADS_DIR = os.path.join(app.static_folder, 'uploads')
|
||||||
os.makedirs(UPLOADS_DIR, exist_ok=True)
|
os.makedirs(UPLOADS_DIR, exist_ok=True)
|
||||||
|
ALLOWED_GALLERY_EXTS = ('.png', '.jpg', '.jpeg', '.webp')
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_gallery_path(path):
|
||||||
|
"""Return a clean path relative to /static without traversal."""
|
||||||
|
if not path:
|
||||||
|
return ''
|
||||||
|
cleaned = path.replace('\\', '/')
|
||||||
|
cleaned = cleaned.split('?', 1)[0]
|
||||||
|
if cleaned.startswith('/'):
|
||||||
|
cleaned = cleaned[1:]
|
||||||
|
if cleaned.startswith('static/'):
|
||||||
|
cleaned = cleaned[len('static/'):]
|
||||||
|
normalized = os.path.normpath(cleaned)
|
||||||
|
if normalized.startswith('..'):
|
||||||
|
return ''
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_gallery_target(source, filename=None, relative_path=None):
|
||||||
|
"""Resolve the gallery source (generated/uploads) and absolute filepath."""
|
||||||
|
cleaned_path = normalize_gallery_path(relative_path)
|
||||||
|
candidate_name = cleaned_path or (filename or '')
|
||||||
|
if not candidate_name:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
normalized_name = os.path.basename(candidate_name)
|
||||||
|
|
||||||
|
inferred_source = (source or '').lower()
|
||||||
|
if cleaned_path:
|
||||||
|
first_segment = cleaned_path.split('/')[0]
|
||||||
|
if first_segment in ('generated', 'uploads'):
|
||||||
|
inferred_source = first_segment
|
||||||
|
|
||||||
|
if inferred_source not in ('generated', 'uploads'):
|
||||||
|
inferred_source = 'generated'
|
||||||
|
|
||||||
|
base_dir = UPLOADS_DIR if inferred_source == 'uploads' else GENERATED_DIR
|
||||||
|
filepath = os.path.join(base_dir, normalized_name)
|
||||||
|
storage_key = f"{inferred_source}/{normalized_name}"
|
||||||
|
return inferred_source, filepath, storage_key
|
||||||
|
|
||||||
def process_prompt_with_placeholders(prompt, note):
|
def process_prompt_with_placeholders(prompt, note):
|
||||||
"""
|
"""
|
||||||
|
|
@ -544,20 +585,29 @@ def generate_image():
|
||||||
|
|
||||||
@app.route('/delete_image', methods=['POST'])
|
@app.route('/delete_image', methods=['POST'])
|
||||||
def delete_image():
|
def delete_image():
|
||||||
data = request.get_json()
|
data = request.get_json() or {}
|
||||||
filename = data.get('filename')
|
filename = data.get('filename')
|
||||||
|
source = data.get('source')
|
||||||
if not filename:
|
rel_path = data.get('path') or data.get('relative_path')
|
||||||
|
|
||||||
|
resolved_source, filepath, storage_key = resolve_gallery_target(source, filename, rel_path)
|
||||||
|
if not filepath:
|
||||||
return jsonify({'error': 'Filename is required'}), 400
|
return jsonify({'error': 'Filename is required'}), 400
|
||||||
|
|
||||||
# Security check: ensure filename is just a basename, no paths
|
|
||||||
filename = os.path.basename(filename)
|
|
||||||
filepath = os.path.join(GENERATED_DIR, filename)
|
|
||||||
|
|
||||||
if os.path.exists(filepath):
|
if os.path.exists(filepath):
|
||||||
try:
|
try:
|
||||||
send2trash(filepath)
|
send2trash(filepath)
|
||||||
return jsonify({'success': True})
|
|
||||||
|
# Clean up favorites entry if it exists
|
||||||
|
favorites = load_gallery_favorites()
|
||||||
|
cleaned_favorites = [
|
||||||
|
item for item in favorites
|
||||||
|
if item != storage_key and item != os.path.basename(filepath)
|
||||||
|
]
|
||||||
|
if cleaned_favorites != favorites:
|
||||||
|
save_gallery_favorites(cleaned_favorites)
|
||||||
|
|
||||||
|
return jsonify({'success': True, 'source': resolved_source})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
else:
|
else:
|
||||||
|
|
@ -565,12 +615,19 @@ def delete_image():
|
||||||
|
|
||||||
@app.route('/gallery')
|
@app.route('/gallery')
|
||||||
def get_gallery():
|
def get_gallery():
|
||||||
# List all png files in generated dir, sorted by modification time (newest first)
|
# List all images in the chosen source directory, sorted by modification time (newest first)
|
||||||
files = glob.glob(os.path.join(GENERATED_DIR, '*.png'))
|
source_param = (request.args.get('source') or 'generated').lower()
|
||||||
|
base_dir = UPLOADS_DIR if source_param == 'uploads' else GENERATED_DIR
|
||||||
|
resolved_source = 'uploads' if base_dir == UPLOADS_DIR else 'generated'
|
||||||
|
|
||||||
|
files = [
|
||||||
|
f for f in glob.glob(os.path.join(base_dir, '*'))
|
||||||
|
if os.path.splitext(f)[1].lower() in ALLOWED_GALLERY_EXTS
|
||||||
|
]
|
||||||
files.sort(key=os.path.getmtime, reverse=True)
|
files.sort(key=os.path.getmtime, reverse=True)
|
||||||
|
|
||||||
image_urls = [url_for('static', filename=f'generated/{os.path.basename(f)}') for f in files]
|
image_urls = [url_for('static', filename=f'{resolved_source}/{os.path.basename(f)}') for f in files]
|
||||||
response = jsonify({'images': image_urls})
|
response = jsonify({'images': image_urls, 'source': resolved_source})
|
||||||
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
@ -652,22 +709,25 @@ def get_gallery_favorites():
|
||||||
def toggle_gallery_favorite():
|
def toggle_gallery_favorite():
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
filename = data.get('filename')
|
filename = data.get('filename')
|
||||||
|
source = data.get('source')
|
||||||
|
rel_path = data.get('path') or data.get('relative_path')
|
||||||
|
|
||||||
if not filename:
|
resolved_source, _, storage_key = resolve_gallery_target(source, filename, rel_path)
|
||||||
|
if not storage_key:
|
||||||
return jsonify({'error': 'Filename is required'}), 400
|
return jsonify({'error': 'Filename is required'}), 400
|
||||||
|
|
||||||
# Security: ensure filename is just a basename
|
|
||||||
filename = os.path.basename(filename)
|
|
||||||
|
|
||||||
favorites = load_gallery_favorites()
|
favorites = load_gallery_favorites()
|
||||||
|
legacy_key = os.path.basename(storage_key)
|
||||||
|
|
||||||
if filename in favorites:
|
if storage_key in favorites or legacy_key in favorites:
|
||||||
favorites = [item for item in favorites if item != filename]
|
favorites = [item for item in favorites if item not in (storage_key, legacy_key)]
|
||||||
|
is_favorite = False
|
||||||
else:
|
else:
|
||||||
favorites.append(filename)
|
favorites.append(storage_key)
|
||||||
|
is_favorite = True
|
||||||
|
|
||||||
save_gallery_favorites(favorites)
|
save_gallery_favorites(favorites)
|
||||||
return jsonify({'favorites': favorites, 'is_favorite': filename in favorites})
|
return jsonify({'favorites': favorites, 'is_favorite': is_favorite, 'source': resolved_source})
|
||||||
|
|
||||||
@app.route('/save_template', methods=['POST'])
|
@app.route('/save_template', methods=['POST'])
|
||||||
def save_template():
|
def save_template():
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@ import { extractMetadataFromBlob } from './metadata.js';
|
||||||
|
|
||||||
const FILTER_STORAGE_KEY = 'gemini-app-history-filter';
|
const FILTER_STORAGE_KEY = 'gemini-app-history-filter';
|
||||||
const SEARCH_STORAGE_KEY = 'gemini-app-history-search';
|
const SEARCH_STORAGE_KEY = 'gemini-app-history-search';
|
||||||
|
const SOURCE_STORAGE_KEY = 'gemini-app-history-source';
|
||||||
|
const VALID_SOURCES = ['generated', 'uploads'];
|
||||||
|
|
||||||
export function createGallery({ galleryGrid, onSelect }) {
|
export function createGallery({ galleryGrid, onSelect }) {
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
let searchQuery = '';
|
let searchQuery = '';
|
||||||
|
let currentSource = 'generated';
|
||||||
let allImages = [];
|
let allImages = [];
|
||||||
let favorites = [];
|
let favorites = [];
|
||||||
let showOnlyFavorites = false; // New toggle state
|
let showOnlyFavorites = false; // New toggle state
|
||||||
|
|
@ -18,6 +21,11 @@ export function createGallery({ galleryGrid, onSelect }) {
|
||||||
|
|
||||||
const savedSearch = localStorage.getItem(SEARCH_STORAGE_KEY);
|
const savedSearch = localStorage.getItem(SEARCH_STORAGE_KEY);
|
||||||
if (savedSearch) searchQuery = savedSearch;
|
if (savedSearch) searchQuery = savedSearch;
|
||||||
|
|
||||||
|
const savedSource = localStorage.getItem(SOURCE_STORAGE_KEY);
|
||||||
|
if (savedSource && VALID_SOURCES.includes(savedSource)) {
|
||||||
|
currentSource = savedSource;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to load history filter/search', e);
|
console.warn('Failed to load history filter/search', e);
|
||||||
}
|
}
|
||||||
|
|
@ -33,9 +41,34 @@ export function createGallery({ galleryGrid, onSelect }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFavorite(imageUrl) {
|
function extractRelativePath(imageUrl) {
|
||||||
|
if (!imageUrl) return '';
|
||||||
|
try {
|
||||||
|
const url = new URL(imageUrl, window.location.origin);
|
||||||
|
const path = url.pathname;
|
||||||
|
const staticIndex = path.indexOf('/static/');
|
||||||
|
if (staticIndex !== -1) {
|
||||||
|
return path.slice(staticIndex + '/static/'.length).replace(/^\//, '');
|
||||||
|
}
|
||||||
|
return path.replace(/^\//, '');
|
||||||
|
} catch (error) {
|
||||||
|
const parts = imageUrl.split('/static/');
|
||||||
|
if (parts[1]) return parts[1].split('?')[0];
|
||||||
|
return imageUrl.split('/').pop().split('?')[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFavoriteKey(imageUrl) {
|
||||||
|
const relative = extractRelativePath(imageUrl);
|
||||||
|
if (relative) return relative;
|
||||||
const filename = imageUrl.split('/').pop().split('?')[0];
|
const filename = imageUrl.split('/').pop().split('?')[0];
|
||||||
return favorites.includes(filename);
|
return `${currentSource}/${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFavorite(imageUrl) {
|
||||||
|
const key = getFavoriteKey(imageUrl);
|
||||||
|
const filename = imageUrl.split('/').pop().split('?')[0];
|
||||||
|
return favorites.includes(key) || favorites.includes(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date comparison utilities
|
// Date comparison utilities
|
||||||
|
|
@ -121,12 +154,17 @@ export function createGallery({ galleryGrid, onSelect }) {
|
||||||
|
|
||||||
async function toggleFavorite(imageUrl) {
|
async function toggleFavorite(imageUrl) {
|
||||||
const filename = imageUrl.split('/').pop().split('?')[0];
|
const filename = imageUrl.split('/').pop().split('?')[0];
|
||||||
|
const relativePath = extractRelativePath(imageUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/toggle_gallery_favorite', {
|
const response = await fetch('/toggle_gallery_favorite', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ filename })
|
body: JSON.stringify({
|
||||||
|
filename,
|
||||||
|
path: relativePath,
|
||||||
|
source: currentSource
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -219,11 +257,16 @@ export function createGallery({ galleryGrid, onSelect }) {
|
||||||
|
|
||||||
|
|
||||||
const filename = imageUrl.split('/').pop().split('?')[0];
|
const filename = imageUrl.split('/').pop().split('?')[0];
|
||||||
|
const relativePath = extractRelativePath(imageUrl);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/delete_image', {
|
const res = await fetch('/delete_image', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ filename })
|
body: JSON.stringify({
|
||||||
|
filename,
|
||||||
|
path: relativePath,
|
||||||
|
source: currentSource
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
@ -251,7 +294,7 @@ export function createGallery({ galleryGrid, onSelect }) {
|
||||||
if (!galleryGrid) return;
|
if (!galleryGrid) return;
|
||||||
try {
|
try {
|
||||||
await loadFavorites();
|
await loadFavorites();
|
||||||
const response = await fetch(`/gallery?t=${new Date().getTime()}`);
|
const response = await fetch(`/gallery?source=${currentSource}&t=${new Date().getTime()}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
allImages = data.images || [];
|
allImages = data.images || [];
|
||||||
renderGallery();
|
renderGallery();
|
||||||
|
|
@ -293,6 +336,28 @@ export function createGallery({ galleryGrid, onSelect }) {
|
||||||
return showOnlyFavorites;
|
return showOnlyFavorites;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSource(source, { resetFilters = false } = {}) {
|
||||||
|
const normalized = VALID_SOURCES.includes(source) ? source : 'generated';
|
||||||
|
currentSource = normalized;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(SOURCE_STORAGE_KEY, currentSource);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to save history source', e);
|
||||||
|
}
|
||||||
|
if (resetFilters) {
|
||||||
|
currentFilter = 'all';
|
||||||
|
showOnlyFavorites = false;
|
||||||
|
searchQuery = '';
|
||||||
|
try {
|
||||||
|
localStorage.setItem(FILTER_STORAGE_KEY, currentFilter);
|
||||||
|
localStorage.setItem(SEARCH_STORAGE_KEY, searchQuery);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to reset history filters', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return load();
|
||||||
|
}
|
||||||
|
|
||||||
function getCurrentFilter() {
|
function getCurrentFilter() {
|
||||||
return currentFilter;
|
return currentFilter;
|
||||||
}
|
}
|
||||||
|
|
@ -301,10 +366,24 @@ export function createGallery({ galleryGrid, onSelect }) {
|
||||||
return searchQuery;
|
return searchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentSource() {
|
||||||
|
return currentSource;
|
||||||
|
}
|
||||||
|
|
||||||
function isFavoritesActive() {
|
function isFavoritesActive() {
|
||||||
return showOnlyFavorites;
|
return showOnlyFavorites;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFavoritesActive(active) {
|
||||||
|
showOnlyFavorites = Boolean(active);
|
||||||
|
renderGallery();
|
||||||
|
return showOnlyFavorites;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSearchQuery(value) {
|
||||||
|
setSearch(value);
|
||||||
|
}
|
||||||
|
|
||||||
function navigate(direction) {
|
function navigate(direction) {
|
||||||
const activeItem = galleryGrid.querySelector('.gallery-item.active');
|
const activeItem = galleryGrid.querySelector('.gallery-item.active');
|
||||||
|
|
||||||
|
|
@ -343,5 +422,17 @@ export function createGallery({ galleryGrid, onSelect }) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { load, setFilter, getCurrentFilter, setSearch, getSearchQuery, toggleFavorites, isFavoritesActive };
|
return {
|
||||||
|
load,
|
||||||
|
setFilter,
|
||||||
|
getCurrentFilter,
|
||||||
|
setSearch,
|
||||||
|
getSearchQuery,
|
||||||
|
toggleFavorites,
|
||||||
|
isFavoritesActive,
|
||||||
|
setSource,
|
||||||
|
getCurrentSource,
|
||||||
|
setFavoritesActive,
|
||||||
|
setSearchQuery
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1588,6 +1588,48 @@ 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');
|
||||||
const historyFavoritesBtn = document.querySelector('.history-favorites-btn');
|
const historyFavoritesBtn = document.querySelector('.history-favorites-btn');
|
||||||
|
const historySourceBtns = document.querySelectorAll('.history-source-btn');
|
||||||
|
const initialSource = gallery.getCurrentSource ? gallery.getCurrentSource() : 'generated';
|
||||||
|
|
||||||
|
historySourceBtns.forEach(btn => {
|
||||||
|
const isActive = btn.dataset.source === initialSource;
|
||||||
|
btn.classList.toggle('active', isActive);
|
||||||
|
btn.setAttribute('aria-pressed', String(isActive));
|
||||||
|
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const targetSource = btn.dataset.source || 'generated';
|
||||||
|
historySourceBtns.forEach(b => {
|
||||||
|
const active = b === btn;
|
||||||
|
b.classList.toggle('active', active);
|
||||||
|
b.setAttribute('aria-pressed', String(active));
|
||||||
|
});
|
||||||
|
await gallery.setSource(targetSource, { resetFilters: true });
|
||||||
|
|
||||||
|
// Reset filters UI to show all when switching source
|
||||||
|
historyFilterBtns.forEach(b => {
|
||||||
|
if (!b.classList.contains('history-favorites-btn')) {
|
||||||
|
b.classList.toggle('active', b.dataset.filter === 'all');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable favorites toggle on source change
|
||||||
|
if (historyFavoritesBtn) {
|
||||||
|
historyFavoritesBtn.classList.remove('active');
|
||||||
|
}
|
||||||
|
if (gallery.setFavoritesActive) {
|
||||||
|
gallery.setFavoritesActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear search box
|
||||||
|
const historySearchInputEl = document.getElementById('history-search-input');
|
||||||
|
if (historySearchInputEl) {
|
||||||
|
historySearchInputEl.value = '';
|
||||||
|
}
|
||||||
|
if (gallery.setSearchQuery) {
|
||||||
|
gallery.setSearchQuery('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Set initial active state based on saved filter
|
// Set initial active state based on saved filter
|
||||||
const currentFilter = gallery.getCurrentFilter();
|
const currentFilter = gallery.getCurrentFilter();
|
||||||
|
|
|
||||||
|
|
@ -1051,16 +1051,6 @@ button#generate-btn:disabled {
|
||||||
background: var(--panel-backdrop);
|
background: var(--panel-backdrop);
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-section h3 {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-header {
|
.history-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -1069,6 +1059,39 @@ button#generate-btn:disabled {
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-source-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-source-btn {
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-source-btn:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: rgba(255, 255, 255, 0.25);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-source-btn.active {
|
||||||
|
background: linear-gradient(135deg, var(--accent-color), var(--accent-hover));
|
||||||
|
color: #111;
|
||||||
|
box-shadow: 0 4px 14px rgba(251, 191, 36, 0.35);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.history-filter-group {
|
.history-filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.15rem;
|
gap: 0.15rem;
|
||||||
|
|
@ -1149,7 +1172,7 @@ button#generate-btn:disabled {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0px 0px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-item:hover {
|
.gallery-item:hover {
|
||||||
|
|
@ -1158,7 +1181,7 @@ button#generate-btn:disabled {
|
||||||
|
|
||||||
.gallery-item.active {
|
.gallery-item.active {
|
||||||
border-color: var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
box-shadow: 0 0 25px rgba(251, 191, 36, 0.4);
|
box-shadow: 0 0 10px rgba(251, 191, 36, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* New styles start here */
|
/* New styles start here */
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,12 @@
|
||||||
</main>
|
</main>
|
||||||
<section class="history-section">
|
<section class="history-section">
|
||||||
<div class="history-header">
|
<div class="history-header">
|
||||||
<h3>History</h3>
|
<div class="history-source-toggle" role="group" aria-label="Chọn nguồn lịch sử">
|
||||||
|
<button type="button" class="history-source-btn active" data-source="generated"
|
||||||
|
aria-pressed="true">History</button>
|
||||||
|
<button type="button" class="history-source-btn" data-source="uploads"
|
||||||
|
aria-pressed="false">Upload</button>
|
||||||
|
</div>
|
||||||
<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"
|
<button type="button" class="history-filter-btn history-favorites-btn" data-filter="favorites"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue