worldhopper/lib/models/entry.dart
2026-04-17 11:45:41 +02:00

123 lines
3.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.01.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;
}