feat: gallery masonry layout, favorites system, left-right detail dialog, bump v0.17.0
Gallery UI overhaul: - GalleryGrid: replace equal-size grid with CSS columns masonry layout (6 columns), pure image cards without text/date info, hover gray ring highlight, favorited items show red heart badge - GalleryDetail: left-right layout dialog (70% image / 30% info), nearly full-screen size with minimal margins, black background for image area, absolute-positioned image container for correct sizing across all aspect ratios, favorite toggle button replaces tag manager, removed outer border - Gallery page: added "favorites only" filter button, replaced load-more button with IntersectionObserver infinite scroll, removed tag system references Favorites system: - db.ts: added `favorited` column to media_generations with migration - New API: PUT /api/media/[id]/favorite for toggle - gallery/route.ts: support favoritesOnly=1 filter, return favorited field - i18n: added favorites-related translations (en/zh) Other: - Default image model changed from Nano Banana Pro to Nano Banana 2 - providers/models/route.ts: skip media provider types in chat model selector Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
5ac4eb5eb6
commit
a5e2cedc12
13 files changed
+284
-262
No files matched your search
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getDb } from '@/lib/db';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function PUT(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const db = getDb();
|
||||
|
||||
const row = db.prepare('SELECT favorited FROM media_generations WHERE id = ?').get(id) as { favorited: number } | undefined;
|
||||
if (!row) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const newValue = row.favorited ? 0 : 1;
|
||||
db.prepare('UPDATE media_generations SET favorited = ? WHERE id = ?').run(newValue, id);
|
||||
|
||||
return NextResponse.json({ favorited: newValue });
|
||||
} catch (error) {
|
||||
console.error('[media/favorite] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to toggle favorite' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ interface DbRow {
|
||||
local_path: string;
|
||||
tags: string;
|
||||
metadata: string;
|
||||
favorited: number;
|
||||
created_at: string;
|
||||
session_id: string | null;
|
||||
[key: string]: unknown;
|
||||
@@ -56,6 +57,7 @@ function mapRow(row: DbRow) {
|
||||
aspectRatio: row.aspect_ratio,
|
||||
imageSize: row.image_size,
|
||||
tags,
|
||||
favorited: !!row.favorited,
|
||||
created_at: row.created_at,
|
||||
session_id: row.session_id || undefined,
|
||||
referenceImages,
|
||||
@@ -68,6 +70,7 @@ export async function GET(request: NextRequest) {
|
||||
const tags = searchParams.get('tags');
|
||||
const dateFrom = searchParams.get('dateFrom');
|
||||
const dateTo = searchParams.get('dateTo');
|
||||
const favoritesOnly = searchParams.get('favoritesOnly') === '1';
|
||||
const sort = searchParams.get('sort') || 'newest';
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
const offset = parseInt(searchParams.get('offset') || '0', 10);
|
||||
@@ -77,6 +80,10 @@ export async function GET(request: NextRequest) {
|
||||
const conditions: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (favoritesOnly) {
|
||||
conditions.push("mg.favorited = 1");
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
const tagList = tags.split(',').map(t => t.trim()).filter(Boolean);
|
||||
for (const tag of tagList) {
|
||||
|
||||
@@ -84,8 +84,12 @@ export async function GET() {
|
||||
});
|
||||
}
|
||||
|
||||
// Provider types that are not LLMs (e.g. image generation) — skip in chat model selector
|
||||
const MEDIA_PROVIDER_TYPES = new Set(['gemini-image']);
|
||||
|
||||
// Build a group for each configured provider
|
||||
for (const provider of providers) {
|
||||
if (MEDIA_PROVIDER_TYPES.has(provider.provider_type)) continue;
|
||||
const matched = PROVIDER_MODEL_LABELS[provider.base_url];
|
||||
const rawModels = matched || DEFAULT_MODELS;
|
||||
const models = deduplicateModels(rawModels);
|
||||
|
||||
Reference in new issue
Block a user