feat: convert filtered items to grid view and fix navigation

- Change from ListView to GridView with 3 columns for better book browsing
- Redesign LibraryItemCard as grid cards with cover images and metadata
- Fix navigation to use Routes.libraryItem instead of Routes.you
- Use correct path parameter 'itemId' for book detail navigation
- Remove subtitle from cards to simplify grid layout
- Books now open properly when tapped, same as home page
This commit is contained in:
Claude 2025-11-20 20:31:50 +00:00
parent 9e0f25f0e2
commit cf0778e263
No known key found for this signature in database

View file

@ -73,12 +73,18 @@ class FilteredLibraryItemsPage extends HookConsumerWidget {
); );
} }
return ListView.builder( return GridView.builder(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 0.65,
crossAxisSpacing: 12,
mainAxisSpacing: 16,
),
itemCount: response.results.length, itemCount: response.results.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = response.results[index]; final item = response.results[index];
return LibraryItemListTile(item: item); return LibraryItemCard(item: item);
}, },
); );
}, },
@ -95,8 +101,8 @@ class FilteredLibraryItemsPage extends HookConsumerWidget {
} }
} }
class LibraryItemListTile extends ConsumerWidget { class LibraryItemCard extends ConsumerWidget {
const LibraryItemListTile({ const LibraryItemCard({
super.key, super.key,
required this.item, required this.item,
}); });
@ -110,7 +116,6 @@ class LibraryItemListTile extends ConsumerWidget {
// Extract book info // Extract book info
String title = ''; String title = '';
String? subtitle;
String? authorName; String? authorName;
// Use map to handle Media variants // Use map to handle Media variants
@ -120,19 +125,16 @@ class LibraryItemListTile extends ConsumerWidget {
metadata.mapOrNull( metadata.mapOrNull(
book: (m) { book: (m) {
title = m.title ?? 'Unknown'; title = m.title ?? 'Unknown';
subtitle = m.subtitle;
if (m.authors.isNotEmpty) { if (m.authors.isNotEmpty) {
authorName = m.authors.map((a) => a.name).join(', '); authorName = m.authors.map((a) => a.name).join(', ');
} }
}, },
bookMinified: (m) { bookMinified: (m) {
title = m.title ?? 'Unknown'; title = m.title ?? 'Unknown';
subtitle = m.subtitle;
authorName = m.authorName; authorName = m.authorName;
}, },
bookExpanded: (m) { bookExpanded: (m) {
title = m.title ?? 'Unknown'; title = m.title ?? 'Unknown';
subtitle = m.subtitle;
if (m.authors.isNotEmpty) { if (m.authors.isNotEmpty) {
authorName = m.authors.map((a) => a.name).join(', '); authorName = m.authors.map((a) => a.name).join(', ');
} }
@ -144,19 +146,16 @@ class LibraryItemListTile extends ConsumerWidget {
metadata.mapOrNull( metadata.mapOrNull(
book: (m) { book: (m) {
title = m.title ?? 'Unknown'; title = m.title ?? 'Unknown';
subtitle = m.subtitle;
if (m.authors.isNotEmpty) { if (m.authors.isNotEmpty) {
authorName = m.authors.map((a) => a.name).join(', '); authorName = m.authors.map((a) => a.name).join(', ');
} }
}, },
bookMinified: (m) { bookMinified: (m) {
title = m.title ?? 'Unknown'; title = m.title ?? 'Unknown';
subtitle = m.subtitle;
authorName = m.authorName; authorName = m.authorName;
}, },
bookExpanded: (m) { bookExpanded: (m) {
title = m.title ?? 'Unknown'; title = m.title ?? 'Unknown';
subtitle = m.subtitle;
if (m.authors.isNotEmpty) { if (m.authors.isNotEmpty) {
authorName = m.authors.map((a) => a.name).join(', '); authorName = m.authors.map((a) => a.name).join(', ');
} }
@ -168,19 +167,16 @@ class LibraryItemListTile extends ConsumerWidget {
metadata.mapOrNull( metadata.mapOrNull(
book: (m) { book: (m) {
title = m.title ?? 'Unknown'; title = m.title ?? 'Unknown';
subtitle = m.subtitle;
if (m.authors.isNotEmpty) { if (m.authors.isNotEmpty) {
authorName = m.authors.map((a) => a.name).join(', '); authorName = m.authors.map((a) => a.name).join(', ');
} }
}, },
bookMinified: (m) { bookMinified: (m) {
title = m.title ?? 'Unknown'; title = m.title ?? 'Unknown';
subtitle = m.subtitle;
authorName = m.authorName; authorName = m.authorName;
}, },
bookExpanded: (m) { bookExpanded: (m) {
title = m.title ?? 'Unknown'; title = m.title ?? 'Unknown';
subtitle = m.subtitle;
if (m.authors.isNotEmpty) { if (m.authors.isNotEmpty) {
authorName = m.authors.map((a) => a.name).join(', '); authorName = m.authors.map((a) => a.name).join(', ');
} }
@ -193,72 +189,68 @@ class LibraryItemListTile extends ConsumerWidget {
? '${apiSettings.activeServer!.serverUrl}/api/items/${item.id}/cover' ? '${apiSettings.activeServer!.serverUrl}/api/items/${item.id}/cover'
: null; : null;
return Card( return InkWell(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), onTap: () {
child: ListTile( // Navigate to item detail page
leading: imageUrl != null context.goNamed(
? ClipRRect( Routes.libraryItem.name,
borderRadius: BorderRadius.circular(4), pathParameters: {'itemId': item.id},
child: CachedNetworkImage( );
imageUrl: imageUrl, },
width: 48, child: Column(
height: 48, crossAxisAlignment: CrossAxisAlignment.stretch,
fit: BoxFit.cover, children: [
httpHeaders: { // Book cover
if (apiSettings.activeUser?.authToken != null) Expanded(
'Authorization': child: ClipRRect(
'Bearer ${apiSettings.activeUser!.authToken}', borderRadius: BorderRadius.circular(8),
}, child: imageUrl != null
placeholder: (context, url) => Container( ? CachedNetworkImage(
width: 48, imageUrl: imageUrl,
height: 48, fit: BoxFit.cover,
color: Theme.of(context).colorScheme.surfaceContainerHighest, httpHeaders: {
child: const Icon(Icons.book, size: 24), if (apiSettings.activeUser?.authToken != null)
), 'Authorization':
errorWidget: (context, url, error) => Container( 'Bearer ${apiSettings.activeUser!.authToken}',
width: 48, },
height: 48, placeholder: (context, url) => Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest, color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Icon(Icons.book, size: 24), child: const Center(
), child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) => Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Icon(Icons.book, size: 48),
),
)
: Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Icon(Icons.book, size: 48),
),
),
),
const SizedBox(height: 8),
// Book title
Text(
title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
), ),
) maxLines: 2,
: Container( overflow: TextOverflow.ellipsis,
width: 48, ),
height: 48, // Author name
decoration: BoxDecoration( if (authorName != null)
color: Theme.of(context).colorScheme.surfaceContainerHighest, Text(
borderRadius: BorderRadius.circular(4), authorName!,
), style: Theme.of(context).textTheme.bodySmall?.copyWith(
child: const Icon(Icons.book, size: 24), color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
title: Text( maxLines: 1,
title, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium, ),
maxLines: 2, ],
overflow: TextOverflow.ellipsis,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (authorName != null) Text(authorName!),
if (subtitle != null)
Text(
subtitle!,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// Navigate to item detail page
context.goNamed(
Routes.you.name,
pathParameters: {'libraryItemId': item.id},
);
},
), ),
); );
} }