worldhopper/lib/widgets/downloaded_chapter_card.dart
Felipe M. d2497f8aed
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
feat: add background download service with error handling improvements
Add Android foreground service support for downloads to prevent OS
process killing during long operations. Fix multiple error handling
issues: foreground service leak on exceptions, polling stuck forever
on failure, double-counting chapters on retry, silent retry failures
without user feedback, and app crash if background service init fails.
Replace inaccurate failed chapter count heuristic with actual data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 11:44:37 +01:00

121 lines
4.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:worldhopper/l10n/app_localizations.dart';
import 'package:worldhopper/models/downloaded_chapter.dart';
import 'package:worldhopper/models/downloaded_series.dart';
/// Card for displaying a downloaded chapter in the series detail screen
class DownloadedChapterCard extends StatelessWidget {
final DownloadedChapter chapter;
final VoidCallback onTap;
final VoidCallback? onRetry;
const DownloadedChapterCard({
super.key,
required this.chapter,
required this.onTap,
this.onRetry,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: chapter.isComplete ? onTap : null,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
_buildStatusIcon(context),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
chapter.title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
_statusText(l10n),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: _statusColor(context),
),
),
],
),
),
if (chapter.isDownloading)
SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value:
chapter.pageProgress > 0 ? chapter.pageProgress : null,
strokeWidth: 2,
),
),
if (chapter.status == DownloadStatus.failed && onRetry != null)
IconButton(
icon: const Icon(Icons.refresh),
color: Colors.red,
onPressed: onRetry,
tooltip: l10n.downloadRetryFailed,
),
],
),
),
),
);
}
Widget _buildStatusIcon(BuildContext context) {
final (IconData icon, Color color) = switch (chapter.status) {
DownloadStatus.complete => (Icons.check_circle, Colors.green),
DownloadStatus.downloading => (Icons.downloading, Colors.blue),
DownloadStatus.pending => (Icons.schedule, Colors.grey),
DownloadStatus.partial => (Icons.warning, Colors.orange),
DownloadStatus.failed => (Icons.error, Colors.red),
};
return Icon(icon, color: color, size: 28);
}
String _statusText(AppLocalizations l10n) {
return switch (chapter.status) {
DownloadStatus.complete => l10n.downloadedChapterReady,
DownloadStatus.downloading => _downloadingText(l10n),
DownloadStatus.pending => l10n.downloadedChapterPending,
DownloadStatus.partial => l10n.downloadedChapterPending,
DownloadStatus.failed => chapter.errorMessage != null
? '${l10n.downloadedChapterFailed} - ${chapter.errorMessage}'
: l10n.downloadRetryFailed,
};
}
String _downloadingText(AppLocalizations l10n) {
if (chapter.contentType == ChapterContentType.imageStream &&
chapter.totalPages > 0) {
return l10n.downloadingPages(chapter.downloadedPages, chapter.totalPages);
}
return l10n.downloadInProgress;
}
Color _statusColor(BuildContext context) {
return switch (chapter.status) {
DownloadStatus.complete => Colors.green,
DownloadStatus.downloading => Colors.blue,
DownloadStatus.pending =>
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
DownloadStatus.partial => Colors.orange,
DownloadStatus.failed => Colors.red,
};
}
}