worldhopper/lib/models/downloaded_chapter.dart
Felipe M. 58a562abe8
feat: refactor server software abstraction with Kavita REST API integration
Rename all OPDS-prefixed models to generic names (Server, Feed, Entry,
Link, StreamLink), expand the ServerSoftware interface to cover all
server interactions, and implement a full Kavita REST API client that
replaces the OPDS delegation.

- Rename OPDS* models to generic names across ~60 files
- Add database migrations 13 (opds_id → entry_id) and 14 (unify credentials)
- Create OPDSServerSoftware wrapping existing OPDS services
- Create KavitaApiClient for direct Kavita REST API calls
- Create KavitaFeedMapper to convert Kavita JSON to Feed/Entry models
- Rewrite KavitaServerSoftware to use native API (no OPDS delegation)
- Unify server credentials (remove softwareUsername/softwarePassword)
- Simplify add/edit server UI to single auth section
- Add test connection button to server add/edit screens
- Add progress indicator to PublicationCard using local and server data
- Eliminate softwareType branching in reader screens
- Add EntryProgress and fetchEntryProgress to ServerSoftware interface
- Fix Entry.acquisitionLink crash on empty links
- Add 59 new tests covering models, services, and providers
2026-04-06 17:46:48 +02:00

104 lines
3.4 KiB
Dart

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<String, dynamic> json) =>
_$DownloadedChapterFromJson(json);
/// Create from database row
factory DownloadedChapter.fromDatabase(Map<String, dynamic> 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<String, dynamic>;
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<String, dynamic> 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,
};
}
}