All checks were successful
ci/woodpecker/tag/release Pipeline was successful
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>
121 lines
4.1 KiB
Dart
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,
|
|
};
|
|
}
|
|
}
|