diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index ff6b6e0..384b0ce 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -13,7 +13,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.3.5"; + final version = "1.4.0"; bool get isAndroid => Platform.isAndroid; diff --git a/lib/foundation/comic_source/category.dart b/lib/foundation/comic_source/category.dart index 7aae515..1ade4e0 100644 --- a/lib/foundation/comic_source/category.dart +++ b/lib/foundation/comic_source/category.dart @@ -34,24 +34,28 @@ class CategoryButtonData { }); } +class CategoryItem { + final String label; + + final PageJumpTarget target; + + const CategoryItem(this.label, this.target); +} + abstract class BaseCategoryPart { String get title; - List get categories; - - List? get categoryParams => null; + List get categories; bool get enableRandom; - String get categoryType; - /// Data class for building a part of category page. const BaseCategoryPart(); } class FixedCategoryPart extends BaseCategoryPart { @override - final List categories; + final List categories; @override bool get enableRandom => false; @@ -59,19 +63,12 @@ class FixedCategoryPart extends BaseCategoryPart { @override final String title; - @override - final String categoryType; - - @override - final List? categoryParams; - /// A [BaseCategoryPart] that show fixed tags on category page. - const FixedCategoryPart(this.title, this.categories, this.categoryType, - [this.categoryParams]); + const FixedCategoryPart(this.title, this.categories); } class RandomCategoryPart extends BaseCategoryPart { - final List tags; + final List all; final int randomNumber; @@ -81,67 +78,23 @@ class RandomCategoryPart extends BaseCategoryPart { @override bool get enableRandom => true; - @override - final String categoryType; - - List _categories() { - if (randomNumber >= tags.length) { - return tags; + List _categories() { + if (randomNumber >= all.length) { + return all; } - var start = math.Random().nextInt(tags.length - randomNumber); - return tags.sublist(start, start + randomNumber); + var start = math.Random().nextInt(all.length - randomNumber); + return all.sublist(start, start + randomNumber); } @override - List get categories => _categories(); + List get categories => _categories(); - /// A [BaseCategoryPart] that show random tags on category page. + /// A [BaseCategoryPart] that show a part of random tags on category page. const RandomCategoryPart( - this.title, this.tags, this.randomNumber, this.categoryType); -} - -class RandomCategoryPartWithRuntimeData extends BaseCategoryPart { - final Iterable Function() loadTags; - - final int randomNumber; - - @override - final String title; - - @override - bool get enableRandom => true; - - @override - final String categoryType; - - static final random = math.Random(); - - List _categories() { - var tags = loadTags(); - if (randomNumber >= tags.length) { - return tags.toList(); - } - final start = random.nextInt(tags.length - randomNumber); - var res = List.filled(randomNumber, ''); - int index = -1; - for (var s in tags) { - index++; - if (start > index) { - continue; - } else if (index == start + randomNumber) { - break; - } - res[index - start] = s; - } - return res; - } - - @override - List get categories => _categories(); - - /// A [BaseCategoryPart] that show random tags on category page. - RandomCategoryPartWithRuntimeData( - this.title, this.loadTags, this.randomNumber, this.categoryType); + this.title, + this.all, + this.randomNumber, + ); } CategoryData getCategoryDataWithKey(String key) { diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart index 1ef748c..3e4b524 100644 --- a/lib/foundation/comic_source/comic_source.dart +++ b/lib/foundation/comic_source/comic_source.dart @@ -11,6 +11,8 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/res.dart'; +import 'package:venera/pages/category_comics_page.dart'; +import 'package:venera/pages/search_result_page.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/init.dart'; @@ -349,7 +351,7 @@ class ExplorePagePart { /// - category:categoryName /// /// End with `@`+`param` if the category has a parameter. - final String? viewMore; + final PageJumpTarget? viewMore; const ExplorePagePart(this.title, this.comics, this.viewMore); } diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 47b20ca..9989097 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -430,3 +430,110 @@ class ComicChapters { } } } + +class PageJumpTarget { + final String sourceKey; + + final String page; + + final Map? attributes; + + const PageJumpTarget(this.sourceKey, this.page, this.attributes); + + static PageJumpTarget parse(String sourceKey, dynamic value) { + if (value is Map) { + if (value['page'] != null) { + return PageJumpTarget( + sourceKey, + value["page"] ?? "search", + value["attributes"], + ); + } else if (value["action"] != null) { + // old version `onClickTag` + var page = value["action"]; + if (page == "search") { + return PageJumpTarget( + sourceKey, + "search", + { + "text": value["keyword"], + }, + ); + } else if (page == "category") { + return PageJumpTarget( + sourceKey, + "category", + { + "category": value["keyword"], + "param": value["param"], + }, + ); + } else { + return PageJumpTarget(sourceKey, page, null); + } + } + } else if (value is String) { + // old version string encoding. search: `search:keyword`, category: `category:keyword` or `category:keyword@param` + var segments = value.split(":"); + var page = segments[0]; + if (page == "search") { + return PageJumpTarget( + sourceKey, + "search", + { + "text": segments[1], + }, + ); + } else if (page == "category") { + var c = segments[1]; + if (c.contains('@')) { + var parts = c.split('@'); + return PageJumpTarget( + sourceKey, + "category", + { + "category": parts[0], + "param": parts[1], + }, + ); + } else { + return PageJumpTarget( + sourceKey, + "category", + { + "category": c, + }, + ); + } + } else { + return PageJumpTarget(sourceKey, page, null); + } + } + return PageJumpTarget(sourceKey, "Invalid Data", null); + } + + void jump(BuildContext context) { + if (page == "search") { + context.to( + () => SearchResultPage( + text: attributes?["text"] ?? attributes?["keyword"] ?? "", + sourceKey: sourceKey, + options: List.from(attributes?["options"] ?? []), + ), + ); + } else if (page == "category") { + var key = ComicSource.find(sourceKey)!.categoryData!.key; + context.to( + () => CategoryComicsPage( + categoryKey: key, + category: attributes?["category"] ?? + (throw ArgumentError("Category name is required")), + options: List.from(attributes?["options"] ?? []), + param: attributes?["param"], + ), + ); + } else { + Log.error("Page Jump", "Unknown page: $page"); + } + } +} diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index e9ed69a..b27e9fa 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -80,9 +80,8 @@ class ComicSourceParser { Future parse(String js, String filePath) async { js = js.replaceAll("\r\n", "\n"); - var line1 = js - .split('\n') - .firstWhereOrNull((e) => e.trim().startsWith("class ")); + var line1 = + js.split('\n').firstWhereOrNull((e) => e.trim().startsWith("class ")); if (line1 == null || !line1.startsWith("class ") || !line1.contains("extends ComicSource")) { @@ -336,7 +335,7 @@ class ComicSourceParser { (e['comics'] as List).map((e) { return Comic.fromJson(e, _key!); }).toList(), - e['viewMore'], + PageJumpTarget.parse(_key!, e['viewMore']), ); }), ), @@ -404,21 +403,78 @@ class ComicSourceParser { var categoryParts = []; for (var c in doc["parts"]) { - final String name = c["name"]; - final String type = c["type"]; - final List tags = List.from(c["categories"]); - final String itemType = c["itemType"]; - List? categoryParams = ListOrNull.from(c["categoryParams"]); - final String? groupParam = c["groupParam"]; - if (groupParam != null) { - categoryParams = List.filled(tags.length, groupParam); + if (c["categories"] is! List || c["categories"].isEmpty) { + continue; } - if (type == "fixed") { - categoryParts - .add(FixedCategoryPart(name, tags, itemType, categoryParams)); - } else if (type == "random") { - categoryParts.add( - RandomCategoryPart(name, tags, c["randomNumber"] ?? 1, itemType)); + List categories = c["categories"]; + if (categories[0] is Map) { + // new format + final String name = c["name"]; + final String type = c["type"]; + final cs = categories + .map( + (e) => CategoryItem( + e['label'], + PageJumpTarget.parse(_key!, e['target']), + ), + ) + .toList(); + if (type == "fixed") { + categoryParts.add(FixedCategoryPart(name, cs)); + } else if (type == "random") { + categoryParts + .add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1)); + } + } else { + // old format + final String name = c["name"]; + final String type = c["type"]; + final List tags = List.from(c["categories"]); + final String itemType = c["itemType"]; + List? categoryParams = ListOrNull.from(c["categoryParams"]); + final String? groupParam = c["groupParam"]; + if (groupParam != null) { + categoryParams = List.filled(tags.length, groupParam); + } + var cs = []; + for (int i = 0; i < tags.length; i++) { + PageJumpTarget target; + if (itemType == 'category') { + target = PageJumpTarget( + _key!, + 'category', + { + "category": tags[i], + "param": categoryParams?.elementAtOrNull(i), + }, + ); + } else if (itemType == 'search') { + target = PageJumpTarget( + _key!, + 'search', + { + "keyword": tags[i], + }, + ); + } else if (itemType == 'search_with_namespace') { + target = PageJumpTarget( + _key!, + 'search', + { + "keyword": "$name:$tags[i]", + }, + ); + } else { + target = PageJumpTarget(_key!, itemType, null); + } + cs.add(CategoryItem(tags[i], target)); + } + if (type == "fixed") { + categoryParts.add(FixedCategoryPart(name, cs)); + } else if (type == "random") { + categoryParts + .add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1)); + } } } @@ -620,7 +676,8 @@ class ComicSourceParser { final bool multiFolder = _getValue("favorites.multiFolder"); final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort"); - final bool? singleFolderForSingleComic = _getValue("favorites.singleFolderForSingleComic"); + final bool? singleFolderForSingleComic = + _getValue("favorites.singleFolderForSingleComic"); Future> retryZone(Future> Function() func) async { if (!ComicSource.find(_key!)!.isLogged) { diff --git a/lib/pages/categories_page.dart b/lib/pages/categories_page.dart index f46d975..15ed457 100644 --- a/lib/pages/categories_page.dart +++ b/lib/pages/categories_page.dart @@ -4,12 +4,10 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/pages/ranking_page.dart'; -import 'package:venera/pages/search_result_page.dart'; import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; -import 'category_comics_page.dart'; import 'comic_source_page.dart'; class CategoriesPage extends StatefulWidget { @@ -147,43 +145,6 @@ class _CategoryPage extends StatelessWidget { return ""; } - void handleClick( - String tag, - String? param, - String type, - String namespace, - String categoryKey, - ) { - if (type == 'search') { - App.mainNavigatorKey?.currentContext?.to( - () => SearchResultPage( - text: tag, - options: const [], - sourceKey: findComicSourceKey(), - ), - ); - } else if (type == "search_with_namespace") { - if (tag.contains(" ")) { - tag = '"$tag"'; - } - App.mainNavigatorKey?.currentContext?.to( - () => SearchResultPage( - text: "$namespace:$tag", - options: const [], - sourceKey: findComicSourceKey(), - ), - ); - } else if (type == "category") { - App.mainNavigatorKey!.currentContext!.to( - () => CategoryComicsPage( - category: tag, - categoryKey: categoryKey, - param: param, - ), - ); - } - } - @override Widget build(BuildContext context) { var children = []; @@ -194,11 +155,11 @@ class _CategoryPage extends StatelessWidget { child: Wrap( children: [ if (data.enableRankingPage) - buildTag("Ranking".tl, (p0, p1) { + buildTag("Ranking".tl, () { context.to(() => RankingPage(categoryKey: data.key)); }), for (var buttonData in data.buttons) - buildTag(buttonData.label.tl, (p0, p1) => buttonData.onTap()) + buildTag(buttonData.label.tl, buttonData.onTap) ], ), )); @@ -212,36 +173,14 @@ class _CategoryPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ buildTitleWithRefresh(part.title, () => updater(() {})), - buildTagsWithParams( - part.categories, - part.categoryParams, - part.title, - (key, param) => handleClick( - key, - param, - part.categoryType, - part.title, - category, - ), - ) + buildTags(part.categories) ], ); })); } else { children.add(buildTitle(part.title)); children.add( - buildTagsWithParams( - part.categories, - part.categoryParams, - part.title, - (tag, param) => handleClick( - tag, - param, - part.categoryType, - part.title, - data.key, - ), - ), + buildTags(part.categories), ); } } @@ -280,30 +219,28 @@ class _CategoryPage extends StatelessWidget { ); } - Widget buildTagsWithParams( - List tags, - List? params, - String? namespace, - ClickTagCallback onClick, + Widget buildTags( + List categories, ) { return Padding( padding: const EdgeInsets.fromLTRB(10, 0, 10, 16), child: Wrap( children: List.generate( - tags.length, - (index) => buildTag( - tags[index], - onClick, - namespace, - params?.elementAtOrNull(index), - ), + categories.length, + (index) => buildCategory(categories[index]), ), ), ); } - Widget buildTag(String tag, ClickTagCallback onClick, - [String? namespace, String? param]) { + Widget buildCategory(CategoryItem c) { + return buildTag(c.label, () { + var context = App.mainNavigatorKey!.currentContext!; + c.target.jump(context); + }); + } + + Widget buildTag(String label, VoidCallback onClick) { return Padding( padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), child: Builder( @@ -313,10 +250,10 @@ class _CategoryPage extends StatelessWidget { color: context.colorScheme.primaryContainer.toOpacity(0.72), child: InkWell( borderRadius: const BorderRadius.all(Radius.circular(8)), - onTap: () => onClick(tag, param), + onTap: onClick, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text(tag), + child: Text(label), ), ), ); diff --git a/lib/pages/category_comics_page.dart b/lib/pages/category_comics_page.dart index 33b12cf..a8840d6 100644 --- a/lib/pages/category_comics_page.dart +++ b/lib/pages/category_comics_page.dart @@ -9,6 +9,7 @@ class CategoryComicsPage extends StatefulWidget { required this.category, this.param, required this.categoryKey, + this.options, super.key, }); @@ -18,6 +19,8 @@ class CategoryComicsPage extends StatefulWidget { final String categoryKey; + final List? options; + @override State createState() => _CategoryComicsPageState(); } @@ -40,7 +43,16 @@ class _CategoryComicsPageState extends State { } return true; }).toList(); - optionsValue = options.map((e) => e.options.keys.first).toList(); + var defaultOptionsValue = + options.map((e) => e.options.keys.first).toList(); + if (optionsValue.length != options.length) { + var newOptionsValue = List.filled(options.length, ""); + for (var i = 0; i < options.length; i++) { + newOptionsValue[i] = + optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i]; + } + optionsValue = newOptionsValue; + } sourceKey = source.key; return; } @@ -50,6 +62,11 @@ class _CategoryComicsPageState extends State { @override void initState() { + if (widget.options != null) { + optionsValue = widget.options!; + } else { + optionsValue = []; + } findData(); super.initState(); } diff --git a/lib/pages/comic_details_page/actions.dart b/lib/pages/comic_details_page/actions.dart index 90f0a35..aaa68bc 100644 --- a/lib/pages/comic_details_page/actions.dart +++ b/lib/pages/comic_details_page/actions.dart @@ -300,21 +300,8 @@ abstract mixin class _ComicPageActions { 'keyword': tag, }; var context = App.mainNavigatorKey!.currentContext!; - if (config['action'] == 'search') { - context.to(() => SearchResultPage( - text: config['keyword'] ?? '', - sourceKey: comicSource.key, - options: const [], - )); - } else if (config['action'] == 'category') { - context.to( - () => CategoryComicsPage( - category: config['keyword'] ?? '', - categoryKey: comicSource.categoryData!.key, - param: config['param'], - ), - ); - } + var target = PageJumpTarget.parse(comicSource.key, config); + target.jump(context); } void showMoreActions() { diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 96fd888..b72af0e 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -461,6 +461,7 @@ void _addAllPagesWithComicSource(ComicSource source) { var explorePages = appdata.settings['explore_pages']; var categoryPages = appdata.settings['categories']; var networkFavorites = appdata.settings['favorites']; + var searchPages = appdata.settings['searchSources']; if (source.explorePages.isNotEmpty) { for (var page in source.explorePages) { @@ -477,10 +478,15 @@ void _addAllPagesWithComicSource(ComicSource source) { !networkFavorites.contains(source.favoriteData!.key)) { networkFavorites.add(source.favoriteData!.key); } + if (source.searchPageData != null && + !searchPages.contains(source.key)) { + searchPages.add(source.key); + } appdata.settings['explore_pages'] = explorePages.toSet().toList(); appdata.settings['categories'] = categoryPages.toSet().toList(); appdata.settings['favorites'] = networkFavorites.toSet().toList(); + appdata.settings['searchSources'] = searchPages.toSet().toList(); appdata.saveData(); } diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index 415d7d0..d8dba9b 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -6,13 +6,10 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/global_state.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/pages/comic_source_page.dart'; -import 'package:venera/pages/search_result_page.dart'; import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; -import 'category_comics_page.dart'; - class ExplorePage extends StatefulWidget { const ExplorePage({super.key}); @@ -445,30 +442,7 @@ Iterable _buildExplorePagePart( TextButton( onPressed: () { var context = App.mainNavigatorKey!.currentContext!; - if (part.viewMore!.startsWith("search:")) { - context.to( - () => SearchResultPage( - text: part.viewMore!.replaceFirst("search:", ""), - options: const [], - sourceKey: sourceKey, - ), - ); - } else if (part.viewMore!.startsWith("category:")) { - var cp = part.viewMore!.replaceFirst("category:", ""); - var c = cp.split('@').first; - String? p = cp.split('@').last; - if (p == c) { - p = null; - } - context.to( - () => CategoryComicsPage( - category: c, - categoryKey: - ComicSource.find(sourceKey)!.categoryData!.key, - param: p, - ), - ); - } + part.viewMore!.jump(context); }, child: Text("View more".tl), ) diff --git a/pubspec.lock b/pubspec.lock index 8fa2170..94085e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" battery_plus: dependency: "direct main" description: @@ -182,10 +182,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -516,10 +516,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.19.0" io: dependency: transitive description: @@ -540,10 +540,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -1029,10 +1029,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0c7022f..9d9ff98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.3.5+135 +version: 1.4.0+140 environment: sdk: '>=3.6.0 <4.0.0'