import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:worldhopper/l10n/app_localizations.dart'; import 'package:worldhopper/models/entry.dart'; import 'package:worldhopper/models/reading_progress.dart'; import 'package:worldhopper/providers/reading_progress_provider.dart'; /// Card widget displaying a publication or navigation entry class PublicationCard extends ConsumerWidget { final Entry entry; final String serverId; final VoidCallback onTap; final Widget? badge; const PublicationCard({ super.key, required this.entry, required this.serverId, required this.onTap, this.badge, }); @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); // Watch local reading progress for this entry final progressAsync = entry.isNavigation ? null : ref.watch(readingProgressProvider( ProgressKey(publicationId: entry.id, serverId: serverId), )); // Compute progress value: local DB first, then server-side StreamLink final double? progressValue = _resolveProgress(progressAsync); return Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Cover image with optional badge overlay Expanded( child: Stack( fit: StackFit.expand, children: [ _buildCover(context), if (badge != null) badge!, ], ), ), // Progress bar (thin, below cover) if (progressValue != null && progressValue > 0) LinearProgressIndicator( value: progressValue, minHeight: 3, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, ), // Title and metadata Padding( padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( entry.title, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (entry.authors.isNotEmpty) ...[ const SizedBox(height: 4), Text( entry.authors.join(', '), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.6), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], if (entry.hasStreamLink) ...[ const SizedBox(height: 4), Row( children: [ Icon( Icons.auto_stories, size: 14, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 4), Text( l10n.publicationPages(entry.streamLink!.pageCount), style: Theme.of(context) .textTheme .bodySmall ?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), ], ), ], if (entry.isNavigation) ...[ const SizedBox(height: 4), Row( children: [ Icon( Icons.folder, size: 14, color: Theme.of(context).colorScheme.secondary, ), const SizedBox(width: 4), Text( l10n.publicationCollection, style: Theme.of(context) .textTheme .bodySmall ?.copyWith( color: Theme.of(context).colorScheme.secondary, ), ), ], ), ], ], ), ), ], ), ), ); } /// Resolve progress from local DB or server-reported progress. double? _resolveProgress(AsyncValue? progressAsync) { if (progressAsync != null) { final localProgress = progressAsync.valueOrNull; if (localProgress != null) { return localProgress.progressPercentage; } } return entry.serverProgress; } Widget _buildCover(BuildContext context) { final imageUrl = entry.thumbnailUrl ?? entry.coverUrl; if (imageUrl == null) { return _buildPlaceholder(context); } return CachedNetworkImage( imageUrl: imageUrl, fit: BoxFit.cover, placeholder: (context, url) => _buildPlaceholder(context), errorWidget: (context, url, error) => _buildPlaceholder(context), ); } Widget _buildPlaceholder(BuildContext context) { return Container( color: Theme.of(context).colorScheme.surfaceContainerHighest, child: Center( child: Icon( entry.isNavigation ? Icons.folder : Icons.book, size: 48, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), ), ), ); } }