worldhopper/lib/widgets/download_badge.dart
Felipe M. 30df288552
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
feat: add offline series download with local reading support
Enable downloading entire series for offline reading. Includes database
tables and migration for downloaded series/chapters, download orchestration
with progress tracking, per-chapter EPUB and image stream downloading,
a Downloaded section in the library, and offline-first reader support.

Key changes:
- DB migration v11 with downloaded_series and downloaded_chapters tables
- Series download service with pagination, cancellation, and progress stream
- Feed screen download button with status indicators and completion feedback
- Library screen Downloaded section with series cards and detail screen
- Reader pre-caching using local files (including two-page spread mode)
- Next-in-series navigation resolves from downloaded chapters when offline
- Skip image downloads and progress sync when device is offline
- Settings screen shows download storage usage with clear option

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:40:15 +01:00

71 lines
2.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worldhopper/models/downloaded_series.dart';
import 'package:worldhopper/providers/download_provider.dart';
/// Badge overlay showing download status for a chapter
class DownloadBadge extends ConsumerWidget {
final String serverId;
final String entryId;
const DownloadBadge({
super.key,
required this.serverId,
required this.entryId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final statusAsync = ref.watch(chapterDownloadStatusProvider(
ChapterDownloadKey(serverId: serverId, entryId: entryId),
));
return statusAsync.when(
data: (chapter) {
if (chapter == null) return const SizedBox.shrink();
return _buildBadge(context, chapter.status);
},
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
);
}
Widget _buildBadge(BuildContext context, DownloadStatus status) {
final (IconData icon, Color color) = switch (status) {
DownloadStatus.complete => (Icons.download_done, Colors.green),
DownloadStatus.downloading => (Icons.downloading, Colors.blue),
DownloadStatus.pending => (Icons.download, Colors.grey),
DownloadStatus.partial => (Icons.download, Colors.orange),
DownloadStatus.failed => (Icons.error_outline, Colors.red),
};
return Positioned(
top: 6,
right: 6,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: status == DownloadStatus.downloading
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Icon(icon, size: 14, color: Colors.white),
),
);
}
}