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

80 lines
1.9 KiB
Dart

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:worldhopper/models/entry.dart';
import 'package:worldhopper/models/link.dart';
part 'feed.freezed.dart';
part 'feed.g.dart';
/// Represents an OPDS feed (navigation or acquisition)
@freezed
class Feed with _$Feed {
const factory Feed({
/// Feed ID
required String id,
/// Feed title
required String title,
/// Feed subtitle
String? subtitle,
/// Feed icon URL
String? icon,
/// Last update timestamp
DateTime? updated,
/// Feed author
String? author,
/// All links in the feed
@Default([]) List<Link> links,
/// All entries in the feed
@Default([]) List<Entry> entries,
/// Total results (for paginated feeds)
int? totalResults,
/// Items per page (for paginated feeds)
int? itemsPerPage,
/// Start index (for paginated feeds)
int? startIndex,
}) = _Feed;
const Feed._();
/// Create Feed from JSON
factory Feed.fromJson(Map<String, dynamic> json) => _$FeedFromJson(json);
/// Check if this is a navigation feed
bool get isNavigationFeed => entries.any((entry) => entry.isNavigation);
/// Check if this is an acquisition feed (contains publications)
bool get isAcquisitionFeed => !isNavigationFeed;
/// Get the start (root) link
Link? get startLink => links.cast<Link?>().firstWhere(
(link) => link?.rel == 'start',
orElse: () => null,
);
/// Get the self link
Link? get selfLink => links.cast<Link?>().firstWhere(
(link) => link?.rel == 'self',
orElse: () => null,
);
/// Get the next page link
Link? get nextLink => links.cast<Link?>().firstWhere(
(link) => link?.rel == 'next',
orElse: () => null,
);
/// Get the previous page link
Link? get previousLink => links.cast<Link?>().firstWhere(
(link) => link?.rel == 'previous' || link?.rel == 'prev',
orElse: () => null,
);
}