import 'package:mangayomi/bridge_lib.dart'; class MangaBox extends MProvider { MangaBox({required this.source}); MSource source; final Client client = Client(); @override Future getPopular(int page) async { final res = (await client.get( Uri.parse("${source.baseUrl}/${popularUrlPath(source.name, page)}"), headers: getHeader(source.baseUrl), )).body; return mangaRes(res); } @override Future getLatestUpdates(int page) async { final res = (await client.get( Uri.parse("${source.baseUrl}/${latestUrlPath(source.name, page)}"), headers: getHeader(source.baseUrl), )).body; return mangaRes(res); } @override Future search(String query, int page, FilterList filterList) async { final filters = filterList.filters; String url = ""; if (query.isNotEmpty) { url = "${source.baseUrl}/${simpleQueryPath(source.name, page, query)}"; } else { String genre = "all"; String sort = "latest"; String status = "all"; for (var filter in filters) { if (filter.type == "GenreListFilter") { genre = filter.values[filter.state].value; } else if (filter.type == "SortFilter") { sort = filter.values[filter.state].value; } else if (filter.type == "StatusFilter") { status = filter.values[filter.state].value; } } url = "${source.baseUrl}/genre/$genre?type=$sort&state=$status&page=$page"; } final res = (await client.get( Uri.parse(url), headers: getHeader(source.baseUrl), )).body; List mangaList = []; List urls = []; urls = xpath( res, '//*[ @class^="genres-item" or @class="list-truyen-item-wrap" or @class="story-item" or @class="story_item_right"]/h3/a/@href', ); List names = []; names = xpath( res, '//*[ @class^="genres-item" or @class="list-truyen-item-wrap" or @class="story-item" or @class="story_item_right"]/h3/a/text()', ); final images = xpath( res, '//*[@class="search-story-item" or @class="story_item" or @class="content-genres-item" or @class="list-story-item" or @class="story-item" or @class="list-truyen-item-wrap"]/a/img/@src', ); if (names.isEmpty) { urls = xpath( res, '//*[ @class^="genres-item" or @class="list-truyen-item-wrap" or @class="story-item"]/h2/a/@href', ); names = xpath( res, '//*[ @class^="genres-item" or @class="list-truyen-item-wrap" or @class="story-item"]/h2/a/text()', ); } if (names.isEmpty) { names = xpath( res, '//*[@class="search-story-item" or @class="list-story-item"]/a/@title', ); urls = xpath( res, '//*[@class="search-story-item" or @class="list-story-item"]/a/@href', ); } for (var i = 0; i < names.length; i++) { MManga manga = MManga(); manga.name = names[i]; manga.imageUrl = images[i]; manga.link = urls[i]; mangaList.add(manga); } return MPages(mangaList, true); } @override Future getDetail(String url) async { final statusList = [ {"Ongoing": 0, "Completed": 1}, ]; MManga manga = MManga(); final res = (await client.get( Uri.parse(url), headers: getHeader(source.baseUrl), )).body; final document = parseHtml(res); manga.author = document.xpathFirst( '//*[@class="table-label" and contains(text(), "Author")]/parent::tr/td[2]/text()|//li[contains(text(), "Author")]/a/text()', ) ?? ""; final alternative = document.xpathFirst( '//*[@class="table-label" and contains(text(), "Alternative")]/parent::tr/td[2]/text()', ) ?? ""; final description = document.xpathFirst( '//*[@id="contentBox" ]/text() | //*[@id="story_discription" ]/text() | //div[@id="noidungm"]/text()', ) ?? ""; if (description.isNotEmpty) { manga.description = description .split("summary:", ' ') .last .split("Summary:", ' ') .last .replaceAll("\n", ' ') .replaceAll("Description :", ""); if (alternative.isNotEmpty) { manga.description = "${manga.description}\n\nAlternative Name: $alternative"; } } final status = document.xpathFirst( '//*[@class="table-label" and contains(text(), "Status")]/parent::tr/td[2]/text() | //li[contains(text(), "Status")]/text() | //li[contains(text(), "Status")]/a/text()', ) ?? ""; if (status.isNotEmpty) { manga.status = parseStatus(status.split(":").last.trim(), statusList); } manga.genre = document.xpath( '//*[@class="table-label" and contains(text(), "Genres")]/parent::tr/td[2]/a/text() | //li[contains(text(), "Genres")]/a/text()', ); final chaptersElements = document.select( "div.chapter-list div.row, ul.row-content-chapter li, div#chapter_list li", ); List? chaptersList = []; for (var element in chaptersElements) { final a = element.selectFirst("a"); MChapter chapter = MChapter(); chapter.name = a.text; final dates = element.select("span"); String dateStr = ""; if (dates != null && dates.isNotEmpty) { dateStr = dates.last.text; } else { dateStr = element.selectFirst("p")?.text ?? ""; } chapter.url = a.getHref; chapter.dateUpload = dateStr.isEmpty ? DateTime.now().millisecondsSinceEpoch.toString() : parseDates( [dateStr], source.dateFormat, source.dateFormatLocale, )[0]; chaptersList.add(chapter); } manga.chapters = chaptersList; return manga; } @override Future> getPageList(String url) async { final res = (await client.get( Uri.parse(url), headers: getHeader(source.baseUrl), )).body; List pageUrls = []; final urls = xpath( res, '//div[@class="container-chapter-reader" or @class="panel-read-story"]/img/@src', ); for (var url in urls) { if (url.startsWith("https://convert_image_digi.mgicdn.com")) { pageUrls.add( "https://images.weserv.nl/?url=${substringAfter(url, "//")}", ); } else { pageUrls.add(url); } } return pageUrls; } MPages mangaRes(String res) { List mangaList = []; List urls = []; urls = xpath( res, '//*[ @class^="genres-item" or @class="list-truyen-item-wrap" or @class="story-item"]/h3/a/@href', ); List names = []; names = xpath( res, '//*[ @class^="genres-item" or @class="list-truyen-item-wrap" or @class="story-item"]/h3/a/text()', ); final images = xpath( res, '//*[ @class="content-genres-item" or @class="list-story-item" or @class="story-item" or @class="list-truyen-item-wrap"]/a/img/@src', ); if (names.isEmpty) { names = xpath(res, '//*[@class="list-story-item"]/a/@title'); urls = xpath(res, '//*[@class="list-story-item"]/a/@href'); } for (var i = 0; i < names.length; i++) { MManga manga = MManga(); manga.name = names[i]; manga.imageUrl = images[i]; manga.link = urls[i]; mangaList.add(manga); } return MPages(mangaList, true); } String popularUrlPath(String sourceName, int page) { if (sourceName != "Mangairo") { return "manga-list/hot-manga?page=$page"; } else { return "manga-list/type-topview/ctg-all/state-all/page-$page"; } } String latestUrlPath(String sourceName, int page) { if (sourceName != "Mangairo") { return "manga-list/latest-manga?page=$page"; } else { return "manga-list/type-latest/ctg-all/state-all/page-$page"; } } String simpleQueryPath(String sourceName, int page, String query) { if (sourceName == "Mangairo") { return "list/search/${normalizeSearchQuery(query)}?page=$page"; } else { return "search/story/${normalizeSearchQuery(query)}?page=$page"; } } String normalizeSearchQuery(String query) { String str = query.toLowerCase(); str = str.replaceAll(RegExp(r'[àáạảãâầấậẩẫăằắặẳẵ]'), 'a'); str = str.replaceAll(RegExp(r'[èéẹẻẽêềếệểễ]'), 'e'); str = str.replaceAll(RegExp(r'[ìíịỉĩ]'), 'i'); str = str.replaceAll(RegExp(r'[òóọỏõôồốộổỗơờớợởỡ]'), 'o'); str = str.replaceAll(RegExp(r'[ùúụủũưừứựửữ]'), 'u'); str = str.replaceAll(RegExp(r'[ỳýỵỷỹ]'), 'y'); str = str.replaceAll(RegExp(r'đ'), 'd'); str = str.replaceAll( RegExp( r"""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""", ), "_", ); str = str.replaceAll(RegExp(r'_+'), '_'); str = str.replaceAll(RegExp(r'^_+|_+$'), ''); return str; } @override List getFilterList() { if (source.name == "Mangairo") { return []; } return [ HeaderFilter("NOTE: The filter is ignored when using text search."), SelectFilter("SortFilter", "Order by:", 0, [ SelectFilterOption("Latest", "latest"), SelectFilterOption("Newest", "newest"), SelectFilterOption("Top read", "topview"), ]), SelectFilter("StatusFilter", "Status:", 0, [ SelectFilterOption("ALL", "all"), SelectFilterOption("Completed", "completed"), SelectFilterOption("Ongoing", "ongoing"), SelectFilterOption("Dropped", "drop"), ]), SelectFilter("GenreListFilter", "Category:", 0, [ SelectFilterOption("ALL", "all"), SelectFilterOption("Action", "action"), SelectFilterOption("Adult", "adult"), SelectFilterOption("Adventure", "adventure"), SelectFilterOption("Comedy", "comedy"), SelectFilterOption("Cooking", "cooking"), SelectFilterOption("Doujinshi", "doujinshi"), SelectFilterOption("Drama", "drama"), SelectFilterOption("Ecchi", "ecchi"), SelectFilterOption("Fantasy", "fantasy"), SelectFilterOption("Gender Bender", "gender-bender"), SelectFilterOption("Harem", "harem"), SelectFilterOption("Historical", "historical"), SelectFilterOption("Horror", "horror"), SelectFilterOption("Isekai", "isekai"), SelectFilterOption("Josei", "josei"), SelectFilterOption("Manhua", "manhua"), SelectFilterOption("Manhwa", "manhwa"), SelectFilterOption("Martial arts", "martial-arts"), SelectFilterOption("Mature", "mature"), SelectFilterOption("Mecha", "mecha"), SelectFilterOption("Medical", "medical"), SelectFilterOption("Mystery", "mystery"), SelectFilterOption("One shot", "one-shot"), SelectFilterOption("Psychological", "psychological"), SelectFilterOption("Reincarnation", "reincarnation"), SelectFilterOption("Romance", "romance"), SelectFilterOption("School life", "school-life"), SelectFilterOption("Sci fi", "sci-fi"), SelectFilterOption("Seinen", "seinen"), SelectFilterOption("Shoujo", "shoujo"), SelectFilterOption("Shoujo ai", "shoujo-ai"), SelectFilterOption("Shounen", "shounen"), SelectFilterOption("Shounen ai", "shounen-ai"), SelectFilterOption("Slice of life", "slice-of-life"), SelectFilterOption("Smut", "smut"), SelectFilterOption("Sports", "sports"), SelectFilterOption("Supernatural", "supernatural"), SelectFilterOption("Survival", "survival"), SelectFilterOption("System", "system"), SelectFilterOption("Thriller", "thriller"), SelectFilterOption("Tragedy", "tragedy"), SelectFilterOption("Webtoons", "webtoons"), SelectFilterOption("Yaoi", "yaoi"), SelectFilterOption("Yuri", "yuri"), ]), ]; } String ll(String url) { if (url.contains("?")) { return "&"; } return "?"; } } Map getHeader(String url) { final Map headers = { "Referer": "$url/", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", }; return headers; } MangaBox main(MSource source) { return MangaBox(source: source); }