123 lines
3.6 KiB
Dart
123 lines
3.6 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,
|
||
|
||
/// Server-reported pages read (0-indexed current position). For EPUB
|
||
/// entries this is the only place progress is exposed; for image streams
|
||
/// it mirrors [StreamLink.lastRead].
|
||
int? pagesRead,
|
||
|
||
/// Server-reported total page count.
|
||
int? totalPages,
|
||
}) = _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;
|
||
|
||
/// Server-reported current page (0-indexed). Prefers the direct Entry field
|
||
/// populated by Kavita for both EPUB and image streams; falls back to
|
||
/// StreamLink.lastRead (OPDS PSE).
|
||
int? get serverCurrentPage {
|
||
if (pagesRead != null && pagesRead! >= 0) return pagesRead;
|
||
return streamLink?.lastRead;
|
||
}
|
||
|
||
/// Server-reported total pages. Prefers Entry.totalPages; falls back to
|
||
/// StreamLink.pageCount.
|
||
int? get serverTotalPages {
|
||
if (totalPages != null && totalPages! > 0) return totalPages;
|
||
final pc = streamLink?.pageCount;
|
||
return (pc != null && pc > 0) ? pc : null;
|
||
}
|
||
|
||
/// Server-reported progress as a percentage (0.0–1.0), or null when no
|
||
/// usable progress is available. Treats a current page of 0 as "not
|
||
/// started" so we do not show a spurious 1/N progress bar.
|
||
double? get serverProgress {
|
||
final cur = serverCurrentPage;
|
||
final tot = serverTotalPages;
|
||
if (cur == null || tot == null || tot <= 0 || cur <= 0) return null;
|
||
return ((cur + 1) / tot).clamp(0.0, 1.0);
|
||
}
|
||
|
||
bool get hasServerProgress => serverProgress != null;
|
||
}
|