worldhopper/lib/models/entry.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

87 lines
2.3 KiB
Dart

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:worldhopper/models/link.dart';
import 'package:worldhopper/models/stream_link.dart';
part 'entry.freezed.dart';
part 'entry.g.dart';
/// Represents an entry in an OPDS feed (a publication or collection)
@freezed
class Entry with _$Entry {
const factory Entry({
/// Unique identifier for the entry
required String id,
/// Entry title
required String title,
/// Entry summary/description
String? summary,
/// Entry content (HTML description)
String? content,
/// Publication date
DateTime? published,
/// Last update date
DateTime? updated,
/// Authors
@Default([]) List<String> authors,
/// Categories/tags
@Default([]) List<String> categories,
/// All links associated with this entry
@Default([]) List<Link> links,
/// PSE stream link (if available)
StreamLink? streamLink,
/// Cover image URL
String? coverUrl,
/// Thumbnail image URL
String? thumbnailUrl,
}) = _Entry;
const Entry._();
/// Create Entry from JSON
factory Entry.fromJson(Map<String, dynamic> json) => _$EntryFromJson(json);
/// Check if this entry has a stream link (can be read page by page)
bool get hasStreamLink => streamLink != null;
/// Check if this entry is a navigation entry (leads to another feed)
bool get isNavigation => links.any(
(link) => link.rel == 'subsection' || link.rel.contains('navigation'),
);
/// Get the acquisition link (for download or reading)
Link? get acquisitionLink => links.isEmpty
? null
: links.cast<Link?>().firstWhere(
(link) =>
link!.rel.contains('acquisition') ||
link.rel.contains('open-access'),
orElse: () => links.first,
);
/// Get the self link
Link? get selfLink => links.cast<Link?>().firstWhere(
(link) => link?.rel == 'self',
orElse: () => null,
);
/// Check if this entry is an EPUB publication
bool get isEpub {
final link = acquisitionLink;
if (link == null) return false;
return link.type?.toLowerCase().contains('epub') ?? false;
}
/// Check if this entry is an image stream (comic/manga)
bool get isImageStream => hasStreamLink;
}