import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:worldhopper/l10n/app_localizations.dart'; import 'package:worldhopper/models/downloaded_series.dart'; import 'package:worldhopper/providers/download_provider.dart'; import 'package:worldhopper/services/series_download_service.dart' as download_service; /// Card for displaying a downloaded series in the library class DownloadedSeriesCard extends ConsumerWidget { final DownloadedSeries series; final VoidCallback onTap; final VoidCallback? onLongPress; const DownloadedSeriesCard({ super.key, required this.series, required this.onTap, this.onLongPress, }); @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); // Watch live progress for this specific series final progressAsync = ref.watch(activeDownloadProgressProvider); final liveProgress = progressAsync.whenOrNull( data: (progress) => progress.seriesId == series.id ? progress : null, ); final isActivelyDownloading = liveProgress != null && liveProgress.status == DownloadStatus.downloading; final chaptersAsync = ref.watch(downloadedChaptersProvider(series.id)); final failedCount = chaptersAsync.whenOrNull( data: (chapters) => chapters.where((c) => c.status == DownloadStatus.failed).length, ) ?? 0; return Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, onLongPress: onLongPress, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Cover image with overlays Expanded( child: Stack( fit: StackFit.expand, children: [ _buildCover(context), _buildStatusBadge(context), if (isActivelyDownloading) _buildDownloadOverlay(context, liveProgress), ], ), ), // Title and metadata Padding( padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( series.title, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Row( children: [ Icon( Icons.auto_stories, size: 14, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 4), Expanded( child: Text( l10n.downloadChaptersCount( series.downloadedChaptersCount, series.totalChapters, ), style: Theme.of(context) .textTheme .bodySmall ?.copyWith( color: Theme.of(context).colorScheme.primary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), if (failedCount > 0) ...[ const SizedBox(height: 2), Row( children: [ const Icon( Icons.error_outline, size: 14, color: Colors.red, ), const SizedBox(width: 4), Expanded( child: Text( l10n.downloadFailedCount(failedCount), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.red, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ], ], ), ), ], ), ), ); } Widget _buildCover(BuildContext context) { // Try local path first, then network URL if (series.coverPath != null) { return Image.file( File(series.coverPath!), fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildPlaceholder(context), ); } if (series.thumbnailUrl != null || series.coverUrl != null) { return CachedNetworkImage( imageUrl: series.thumbnailUrl ?? series.coverUrl!, fit: BoxFit.cover, placeholder: (_, __) => _buildPlaceholder(context), errorWidget: (_, __, ___) => _buildPlaceholder(context), ); } return _buildPlaceholder(context); } Widget _buildPlaceholder(BuildContext context) { return Container( color: Theme.of(context).colorScheme.surfaceContainerHighest, child: Center( child: Icon( Icons.download_done, size: 48, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), ), ), ); } Widget _buildStatusBadge(BuildContext context) { if (series.isComplete) { return Positioned( top: 6, right: 6, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.green, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.3), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: const Icon(Icons.download_done, size: 14, color: Colors.white), ), ); } if (series.isDownloading) { return Positioned( top: 6, right: 6, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.3), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ), ), ); } return const SizedBox.shrink(); } Widget _buildDownloadOverlay( BuildContext context, download_service.DownloadProgress progress) { return Positioned( bottom: 0, left: 0, right: 0, child: Container( color: Colors.black.withValues(alpha: 0.7), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (progress.chapterTitle != null) Text( progress.chapterTitle!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), LinearProgressIndicator( value: progress.chapterProgress > 0 ? progress.chapterProgress : null, minHeight: 3, ), ], ), ), ); } }