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
104 lines
3.4 KiB
Dart
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,
|
|
};
|
|
}
|
|
}
|