import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:worldhopper/models/enhanced_metadata.dart'; import 'package:worldhopper/models/opds_entry.dart'; import 'package:worldhopper/models/opds_link.dart'; import 'package:worldhopper/models/opds_stream_link.dart'; part 'publication.freezed.dart'; part 'publication.g.dart'; /// Cached publication with full metadata and local image paths @freezed class Publication with _$Publication { const factory Publication({ required String id, required String serverId, required String opdsId, required String title, @Default([]) List authors, String? summary, String? content, String? coverPath, String? thumbnailPath, String? coverUrl, String? thumbnailUrl, String? series, double? seriesPosition, String? publisher, String? language, String? isbn, @Default([]) List categories, @Default([]) List links, OPDSStreamLink? streamLink, DateTime? published, DateTime? updated, required DateTime firstCachedAt, required DateTime lastCachedAt, required DateTime lastAccessedAt, }) = _Publication; const Publication._(); factory Publication.fromJson(Map json) => _$PublicationFromJson(json); /// Create from database row factory Publication.fromDatabase(Map row) { return Publication( id: row['id'] as String, serverId: row['server_id'] as String, opdsId: row['opds_id'] as String, title: row['title'] as String, authors: _decodeJsonList(row['authors']), summary: row['summary'] as String?, content: row['content'] as String?, coverPath: row['cover_path'] as String?, thumbnailPath: row['thumbnail_path'] as String?, coverUrl: row['cover_url'] as String?, thumbnailUrl: row['thumbnail_url'] as String?, series: row['series'] as String?, seriesPosition: row['series_position'] as double?, publisher: row['publisher'] as String?, language: row['language'] as String?, isbn: row['isbn'] as String?, categories: _decodeJsonList(row['categories']), links: _decodeLinks(row['links']), streamLink: _decodeStreamLink(row['stream_link']), published: _decodeDateTime(row['published']), updated: _decodeDateTime(row['updated']), firstCachedAt: DateTime.fromMillisecondsSinceEpoch(row['first_cached_at'] as int), lastCachedAt: DateTime.fromMillisecondsSinceEpoch(row['last_cached_at'] as int), lastAccessedAt: DateTime.fromMillisecondsSinceEpoch(row['last_accessed_at'] as int), ); } /// Create from OPDS entry with enhanced metadata factory Publication.fromOPDSEntry({ required String id, required String serverId, required OPDSEntry entry, required EnhancedMetadata metadata, String? coverPath, String? thumbnailPath, }) { final now = DateTime.now(); return Publication( id: id, serverId: serverId, opdsId: entry.id, title: entry.title, authors: entry.authors, summary: entry.summary, content: entry.content, coverPath: coverPath, thumbnailPath: thumbnailPath, coverUrl: entry.coverUrl, thumbnailUrl: entry.thumbnailUrl, series: metadata.series, seriesPosition: metadata.seriesPosition, publisher: metadata.publisher, language: metadata.language, isbn: metadata.isbn, categories: entry.categories, links: entry.links, streamLink: entry.streamLink, published: entry.published, updated: entry.updated, firstCachedAt: now, lastCachedAt: now, lastAccessedAt: now, ); } static List _decodeJsonList(dynamic value) { if (value == null) return []; final list = jsonDecode(value as String) as List; return list.map((e) => e.toString()).toList(); } static List _decodeLinks(dynamic value) { if (value == null) return []; final list = jsonDecode(value as String) as List; return list .map((e) => OPDSLink.fromJson(e as Map)) .toList(); } static OPDSStreamLink? _decodeStreamLink(dynamic value) { if (value == null) return null; final json = jsonDecode(value as String) as Map; return OPDSStreamLink.fromJson(json); } static DateTime? _decodeDateTime(dynamic value) { if (value == null) return null; return DateTime.fromMillisecondsSinceEpoch(value as int); } /// Convert to OPDSEntry OPDSEntry toOPDSEntry() { return OPDSEntry( id: opdsId, title: title, authors: authors, summary: summary, content: content, coverUrl: coverUrl, thumbnailUrl: thumbnailUrl, categories: categories, links: links, streamLink: streamLink, published: published, updated: updated, ); } } extension PublicationX on Publication { Map toDatabase() { return { 'id': id, 'server_id': serverId, 'opds_id': opdsId, 'title': title, 'authors': jsonEncode(authors), 'summary': summary, 'content': content, 'cover_path': coverPath, 'thumbnail_path': thumbnailPath, 'cover_url': coverUrl, 'thumbnail_url': thumbnailUrl, 'series': series, 'series_position': seriesPosition, 'publisher': publisher, 'language': language, 'isbn': isbn, 'categories': jsonEncode(categories), 'links': jsonEncode(links.map((l) => l.toJson()).toList()), 'stream_link': streamLink?.toJson() != null ? jsonEncode(streamLink!.toJson()) : null, 'published': published?.millisecondsSinceEpoch, 'updated': updated?.millisecondsSinceEpoch, 'first_cached_at': firstCachedAt.millisecondsSinceEpoch, 'last_cached_at': lastCachedAt.millisecondsSinceEpoch, 'last_accessed_at': lastAccessedAt.millisecondsSinceEpoch, }; } }