worldhopper/lib/services/server_software/server_software.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

105 lines
3.6 KiB
Dart

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:worldhopper/models/entry.dart';
import 'package:worldhopper/models/server.dart';
import 'package:worldhopper/models/stream_link.dart';
import 'package:worldhopper/services/opds_parser.dart' show ParsedFeed;
/// Server-reported reading progress for an entry.
///
/// This represents progress as reported by the server, not the local database.
/// Used to show progress indicators in the feed without requiring a local
/// reading history.
class EntryProgress {
/// The entry ID this progress belongs to.
final String entryId;
/// Current page (0-indexed).
final int currentPage;
/// Total pages.
final int totalPages;
/// Progress as a fraction (0.0 to 1.0).
double get percentage =>
totalPages > 0 ? (currentPage + 1) / totalPages : 0.0;
/// Whether the entry has been fully read.
bool get isCompleted => totalPages > 0 && currentPage >= totalPages - 1;
const EntryProgress({
required this.entryId,
required this.currentPage,
required this.totalPages,
});
}
/// Abstract interface for all server interactions.
///
/// Each server software type (OPDS, Kavita, etc.) provides its own
/// implementation. The UI layer should interact exclusively through this
/// interface, never branching on [ServerSoftwareType].
abstract class ServerSoftware {
/// Returns true if the server URL matches this software's pattern.
bool detectFromUrl(Server server);
/// Test if the server is reachable and credentials are valid.
///
/// Returns `true` on success. Throws on failure with details about what
/// went wrong (network error, auth failure, unexpected response, etc.).
Future<bool> testConnection(Server server, {CancelToken? cancelToken});
/// Fetch the root catalog/feed for a server.
Future<ParsedFeed> fetchRootFeed(Server server, {CancelToken? cancelToken});
/// Fetch a feed/catalog from a specific URL.
Future<ParsedFeed> fetchFeed(Server server, String url,
{CancelToken? cancelToken});
/// Fetch server-side reading progress for a list of entries.
///
/// Returns a map of entry ID → [EntryProgress]. Entries without progress
/// on the server are omitted from the result.
///
/// For OPDS servers, progress is extracted from [StreamLink.lastRead] which
/// is already present in the feed data. For servers with richer APIs (e.g.
/// Kavita), this can call dedicated progress endpoints.
Future<Map<String, EntryProgress>> fetchEntryProgress(
Server server,
List<Entry> entries,
);
/// Report page progress to the server (for reading tracking).
Future<void> reportPageProgress(
Server server, StreamLink streamLink, int page);
/// Mark an entry (volume/chapter) as fully read.
Future<void> markAsRead(Server server, Entry entry);
/// Mark an entry (volume/chapter) as unread.
Future<void> markAsUnread(Server server, Entry entry);
/// Download an EPUB file. Returns the local file.
Future<File> downloadEpub(
Server server,
Entry entry, {
void Function(double progress)? onProgress,
});
/// Download all pages of an image stream chapter. Returns the directory path.
Future<String> downloadImageStreamChapter({
required Server server,
required StreamLink streamLink,
required String seriesId,
required String chapterId,
void Function(int downloaded, int total)? onProgress,
CancelToken? cancelToken,
});
/// Get HTTP auth headers for image/resource requests (CachedNetworkImage, etc).
Map<String, String> getAuthHeaders(Server server);
/// Clear any cached state for a server (tokens, HTTP clients, etc).
void clearCachedState(String serverId);
}