import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:worldhopper/models/downloaded_series.dart'; import 'package:worldhopper/models/entry.dart'; part 'downloaded_chapter.freezed.dart'; part 'downloaded_chapter.g.dart'; /// Represents a downloaded chapter for offline reading @freezed class DownloadedChapter with _$DownloadedChapter { const factory DownloadedChapter({ required String id, required String seriesId, required String serverId, required String entryId, required String title, required ChapterContentType contentType, @Default(DownloadStatus.pending) DownloadStatus status, String? filePath, @Default(0) int totalPages, @Default(0) int downloadedPages, required String entryJson, String? coverUrl, String? coverPath, @Default(0) int sortOrder, String? errorMessage, DateTime? downloadedAt, required DateTime updatedAt, }) = _DownloadedChapter; const DownloadedChapter._(); factory DownloadedChapter.fromJson(Map json) => _$DownloadedChapterFromJson(json); /// Create from database row factory DownloadedChapter.fromDatabase(Map row) { return DownloadedChapter( id: row['id'] as String, seriesId: row['series_id'] as String, serverId: row['server_id'] as String, entryId: row['entry_id'] as String, title: row['title'] as String, contentType: ChapterContentType.fromName(row['content_type'] as String), status: DownloadStatus.fromName(row['status'] as String), filePath: row['file_path'] as String?, totalPages: row['total_pages'] as int? ?? 0, downloadedPages: row['downloaded_pages'] as int? ?? 0, entryJson: row['entry_json'] as String, coverUrl: row['cover_url'] as String?, coverPath: row['cover_path'] as String?, sortOrder: row['sort_order'] as int? ?? 0, errorMessage: row['error_message'] as String?, downloadedAt: row['downloaded_at'] != null ? DateTime.fromMillisecondsSinceEpoch(row['downloaded_at'] as int) : null, updatedAt: DateTime.fromMillisecondsSinceEpoch(row['updated_at'] as int), ); } /// Deserialize the stored Entry from entryJson Entry toEntry() { final json = jsonDecode(entryJson) as Map; return Entry.fromJson(json); } /// Whether the chapter download is complete bool get isComplete => status == DownloadStatus.complete; /// Whether the chapter is currently downloading bool get isDownloading => status == DownloadStatus.downloading; /// Download progress fraction for image stream chapters double get pageProgress { if (totalPages == 0) return 0.0; return (downloadedPages / totalPages).clamp(0.0, 1.0); } } extension DownloadedChapterX on DownloadedChapter { Map toDatabase() { return { 'id': id, 'series_id': seriesId, 'server_id': serverId, 'entry_id': entryId, 'title': title, 'content_type': contentType.name, 'status': status.name, 'file_path': filePath, 'total_pages': totalPages, 'downloaded_pages': downloadedPages, 'entry_json': entryJson, 'cover_url': coverUrl, 'cover_path': coverPath, 'sort_order': sortOrder, 'error_message': errorMessage, 'downloaded_at': downloadedAt?.millisecondsSinceEpoch, 'updated_at': updatedAt.millisecondsSinceEpoch, }; } }