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:
7418andClaude Opus 4.6 committed 2026-02-27 11:00:58 +08:00
1 parent 5ac4eb5eb6
commit a5e2cedc12
13 files changed
+284 -262

No files matched your search

+31
View File
@@ -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 },
);
}
}
+7
View File
@@ -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) {
+4
View File
@@ -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);