import '../source.dart'; import 'constants.dart'; /// Hot ranking period for `/hot` feeds. enum MangaTownHotPeriod { total, today, week, month, } String? hotPeriodQuery(MangaTownHotPeriod period) { switch (period) { case MangaTownHotPeriod.total: return null; case MangaTownHotPeriod.today: return 'tviews.za'; case MangaTownHotPeriod.week: return 'wviews.za'; case MangaTownHotPeriod.month: return 'mviews.za'; } } MangaTownHotPeriod? hotPeriodForMenu(CatalogMenuKind kind) { switch (kind) { case CatalogMenuKind.popular: return MangaTownHotPeriod.total; case CatalogMenuKind.hotToday: return MangaTownHotPeriod.today; case CatalogMenuKind.hotWeek: return MangaTownHotPeriod.week; case CatalogMenuKind.hotMonth: return MangaTownHotPeriod.month; default: return null; } } /// Page 1 uses the bare path; page 2+ appends `/{n}.html`. String _pagedPath(String path, int page) { final base = path.replaceAll(RegExp(r'/+$'), ''); if (page <= 1) return base; return '$base/$page.html'; } Uri _withOptionalQuery(String path, String? queryToken) { final uri = Uri.parse('$mangaTownOrigin$path'); if (queryToken == null || queryToken.isEmpty) return uri; // Site uses bare tokens like `?wviews.za` (no `=` value). return Uri.parse('${uri.toString()}?$queryToken'); } String catalogLatestUrl({int page = 1}) { return _withOptionalQuery(_pagedPath('/latest', page), null).toString(); } String catalogHotUrl({ MangaTownHotPeriod period = MangaTownHotPeriod.total, int page = 1, }) { return _withOptionalQuery( _pagedPath('/hot', page), hotPeriodQuery(period), ).toString(); } /// `/directory/{demographic}-{genre}-0-{status}-{initial}-0` String directoryUrl( CatalogBrowseParams browse, { int page = 1, }) { final demo = _seg(browse.demographic); final genre = _seg(browse.genre); final status = _seg(browse.status); final initial = _seg(browse.initial); final path = _pagedPath( '/directory/$demo-$genre-0-$status-$initial-0', page, ); return '$mangaTownOrigin$path'; } String catalogSearchUrl({required String query, int page = 1}) { final path = _pagedPath('/search', page); return Uri.parse('$mangaTownOrigin$path').replace( queryParameters: {'name': query}, ).toString(); } String _seg(String raw) { final t = raw.trim(); if (t.isEmpty) return '0'; return t; } String mangaSlugFromUrl(String pageUrl) { try { final parts = Uri.parse(pageUrl).pathSegments.where((s) => s.isNotEmpty).toList(); final mi = parts.indexOf('manga'); if (mi != -1 && mi + 1 < parts.length) { return parts[mi + 1]; } } catch (_) {} return 'comic'; } String listingUrlForSlug(String slug, {String origin = mangaTownOrigin}) { final base = origin.replaceAll(RegExp(r'/+$'), ''); return '$base/manga/$slug'; } String mangaBaseUrlFromListingUrl(String pageUrl) { final u = Uri.parse(pageUrl); final parts = u.pathSegments.where((s) => s.isNotEmpty).toList(); final mi = parts.indexOf('manga'); if (mi == -1 || mi + 1 >= parts.length) { throw StateError( 'Could not find /manga/{slug}/ in URL; use a manga or chapter link from MangaTown.', ); } final slug = parts[mi + 1]; return Uri(scheme: u.scheme, host: u.host, path: '/manga/$slug').toString(); } /// Strip reader page suffixes like `/2.html` so we keep `/manga/{slug}/c005/`. String chapterBaseUrl(String chapterUrl) { final u = Uri.parse(chapterUrl); final parts = u.pathSegments.where((s) => s.isNotEmpty).toList(); if (parts.isEmpty) return chapterUrl; final last = parts.last; if (RegExp(r'^\d+\.html$', caseSensitive: false).hasMatch(last)) { parts.removeLast(); } final path = '/${parts.join('/')}/'; return Uri(scheme: u.scheme, host: u.host, path: path).toString(); } /// 1-based reader page URL under a chapter base. String chapterPageUrl(String chapterBase, int pageIndex) { final base = chapterBaseUrl(chapterBase); if (pageIndex <= 1) return base; final trimmed = base.replaceAll(RegExp(r'/+$'), ''); return '$trimmed/$pageIndex.html'; } String chapterKeyFromUrl(String chapterUrl) { try { final segs = Uri.parse(chapterBaseUrl(chapterUrl)) .pathSegments .where((s) => s.isNotEmpty) .toList(); return segs.isEmpty ? 'chapter' : segs.last; } catch (_) { return 'chapter'; } } String? chapterNumberFromTitle(String title) { final cleaned = title.trim().replaceAll(RegExp(r'\s+'), ' '); final m = RegExp( r'(?:Chapter|Ch\.?|C\.?)\s*([\d.]+)', caseSensitive: false, ).firstMatch(cleaned); return m?.group(1); } String absoluteUrl(String href, String baseHref) { final resolved = Uri.parse(baseHref).resolve(href.trim()).toString(); if (resolved.startsWith('//')) { return 'https:$resolved'; } return resolved; } /// Demographics offered by MangaTown directory filters. const mangaTownDemographics = >[ MapEntry('0', 'All'), MapEntry('josei', 'Josei'), MapEntry('seinen', 'Seinen'), MapEntry('shoujo', 'Shoujo'), MapEntry('shoujo_ai', 'Shoujo Ai'), MapEntry('shounen', 'Shounen'), MapEntry('shounen_ai', 'Shounen Ai'), MapEntry('yaoi', 'Yaoi'), MapEntry('yuri', 'Yuri'), ]; /// Genre slug → label for directory filters. const mangaTownGenres = >[ MapEntry('0', 'All'), MapEntry('4_koma', '4 Koma'), MapEntry('action', 'Action'), MapEntry('adventure', 'Adventure'), MapEntry('comedy', 'Comedy'), MapEntry('cooking', 'Cooking'), MapEntry('doujinshi', 'Doujinshi'), MapEntry('drama', 'Drama'), MapEntry('ecchi', 'Ecchi'), MapEntry('fantasy', 'Fantasy'), MapEntry('gender_bender', 'Gender Bender'), MapEntry('harem', 'Harem'), MapEntry('historical', 'Historical'), MapEntry('horror', 'Horror'), MapEntry('martial_arts', 'Martial Arts'), MapEntry('mature', 'Mature'), MapEntry('mecha', 'Mecha'), MapEntry('music', 'Music'), MapEntry('mystery', 'Mystery'), MapEntry('one_shot', 'One Shot'), MapEntry('psychological', 'Psychological'), MapEntry('reverse_harem', 'Reverse Harem'), MapEntry('romance', 'Romance'), MapEntry('school_life', 'School Life'), MapEntry('sci_fi', 'Sci Fi'), MapEntry('shotacon', 'Shotacon'), MapEntry('slice_of_life', 'Slice Of Life'), MapEntry('smut', 'Smut'), MapEntry('sports', 'Sports'), MapEntry('supernatural', 'Supernatural'), MapEntry('suspense', 'Suspense'), MapEntry('tragedy', 'Tragedy'), MapEntry('vampire', 'Vampire'), MapEntry('webtoons', 'Webtoons'), MapEntry('youkai', 'Youkai'), ]; const mangaTownStatuses = >[ MapEntry('0', 'All'), MapEntry('ongoing', 'Ongoing'), MapEntry('new', 'New'), MapEntry('completed', 'Completed'), ]; /// A–Z plus `0-9`. List> mangaTownInitials() { final out = >[const MapEntry('0', 'All')]; for (var c = 0; c < 26; c++) { final letter = String.fromCharCode('a'.codeUnitAt(0) + c); out.add(MapEntry(letter, letter.toUpperCase())); } out.add(const MapEntry('0-9', '0-9')); return out; }