// Shared models and source-adapter contract (port of cxyz `sources.js`). enum SourceId { comicland, mangaread, mangatown; String get id => name; String get label { switch (this) { case SourceId.comicland: return 'ComicLand'; case SourceId.mangaread: return 'MangaRead'; case SourceId.mangatown: return 'MangaTown'; } } /// Bundled favicon asset (not fetched at runtime). String get logoAsset => 'assets/sources/$name.png'; static SourceId? tryParse(String raw) { final key = raw.trim().toLowerCase(); for (final id in SourceId.values) { if (id.name == key) return id; } return null; } } /// Browse menus a source may expose on its Catalog home. enum CatalogMenuKind { popular, latest, official, uncensored, recommended, search, hotToday, hotWeek, hotMonth, directory; String get pathSegment => name; } /// Optional directory filters (MangaTown). Segment `0` means All. class CatalogBrowseParams { const CatalogBrowseParams({ this.demographic = '0', this.genre = '0', this.status = '0', this.initial = '0', }); final String demographic; final String genre; final String status; final String initial; CatalogBrowseParams copyWith({ String? demographic, String? genre, String? status, String? initial, }) { return CatalogBrowseParams( demographic: demographic ?? this.demographic, genre: genre ?? this.genre, status: status ?? this.status, initial: initial ?? this.initial, ); } @override bool operator ==(Object other) { return other is CatalogBrowseParams && other.demographic == demographic && other.genre == genre && other.status == status && other.initial == initial; } @override int get hashCode => Object.hash(demographic, genre, status, initial); } class CatalogMenu { const CatalogMenu({required this.kind, required this.title}); final CatalogMenuKind kind; final String title; } class CatalogComic { const CatalogComic({ required this.source, required this.slug, required this.title, required this.listingUrl, this.coverUrl, this.description, this.rating, }); final SourceId source; final String slug; final String title; final String listingUrl; final String? coverUrl; final String? description; final double? rating; } class ChapterLink { const ChapterLink({ required this.url, required this.title, required this.chapterKey, this.chapterNumber, }); final String url; final String title; final String chapterKey; final String? chapterNumber; } class SeriesDetail { const SeriesDetail({ required this.source, required this.listingUrl, required this.slug, required this.title, required this.chapters, this.coverUrl, this.description, }); final SourceId source; final String listingUrl; final String slug; final String title; final String? coverUrl; final String? description; final List chapters; } /// Site adapter: per-source catalog menus + series + page URLs. abstract class ComicSource { SourceId get id; String get displayName; String get blurb; /// Menus on this source’s Catalog home. Omit unsupported feeds (e.g. no search). List get catalogMenus; /// Headers for page/CDN image requests. Map get imageHeaders; Future> loadCatalog( CatalogMenuKind kind, { String query = '', int offset = 0, int limit = 30, CatalogBrowseParams? browse, }); Future fetchSeries(String listingUrl); Future> fetchPageUrls(String chapterUrl); }