diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart index d0a3080..fa914be 100644 --- a/lib/foundation/comic_source/comic_source.dart +++ b/lib/foundation/comic_source/comic_source.dart @@ -401,9 +401,14 @@ class SearchOptions { typedef CategoryComicsLoader = Future>> Function( String category, String? param, List options, int page); +typedef CategoryOptionsLoader = Future>> Function( + String category, String? param); + class CategoryComicsData { /// options - final List options; + final List? options; + + final CategoryOptionsLoader? optionsLoader; /// [category] is the one clicked by the user on the category page. /// @@ -414,7 +419,7 @@ class CategoryComicsData { final RankingData? rankingData; - const CategoryComicsData(this.options, this.load, {this.rankingData}); + const CategoryComicsData({this.options, this.optionsLoader, required this.load, this.rankingData}); } class RankingData { @@ -429,6 +434,9 @@ class RankingData { } class CategoryComicsOptions { + // The label will not be displayed if it is empty. + final String label; + /// Use a [LinkedHashMap] to describe an option list. /// key is for loading comics, value is the name displayed on screen. /// Default value will be the first of the Map. @@ -439,7 +447,7 @@ class CategoryComicsOptions { final List? showWhen; - const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen); + const CategoryComicsOptions(this.label, this.options, this.notShowWhen, this.showWhen); } class LinkHandler { diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index 923f948..c0da172 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -64,8 +64,13 @@ class ComicSourceParser { if (file.existsSync()) { int i = 0; while (file.existsSync()) { - file = File(FilePath.join(App.dataPath, "comic_source", - "${fileName.split('.').first}($i).js")); + file = File( + FilePath.join( + App.dataPath, + "comic_source", + "${fileName.split('.').first}($i).js", + ), + ); i++; } } @@ -80,8 +85,9 @@ 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")) { @@ -93,19 +99,23 @@ class ComicSourceParser { this['temp'] = new $className() }).call() """, className); - _name = JsEngine().runCode("this['temp'].name") ?? + _name = + JsEngine().runCode("this['temp'].name") ?? (throw ComicSourceParseException('name is required')); - var key = JsEngine().runCode("this['temp'].key") ?? + var key = + JsEngine().runCode("this['temp'].key") ?? (throw ComicSourceParseException('key is required')); - var version = JsEngine().runCode("this['temp'].version") ?? + var version = + JsEngine().runCode("this['temp'].version") ?? (throw ComicSourceParseException('version is required')); var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion"); var url = JsEngine().runCode("this['temp'].url"); if (minAppVersion != null) { if (compareSemVer(minAppVersion, App.version.split('-').first)) { throw ComicSourceParseException( - "minAppVersion @version is required" - .tlParams({"version": minAppVersion}), + "minAppVersion @version is required".tlParams({ + "version": minAppVersion, + }), ); } } @@ -174,8 +184,10 @@ class ComicSourceParser { } bool _checkExists(String index) { - return JsEngine().runCode("ComicSource.sources.$_key.$index !== null " - "&& ComicSource.sources.$_key.$index !== undefined"); + return JsEngine().runCode( + "ComicSource.sources.$_key.$index !== null " + "&& ComicSource.sources.$_key.$index !== undefined", + ); } dynamic _getValue(String index) { @@ -276,16 +288,24 @@ class ComicSourceParser { if (type == "singlePageWithMultiPart") { loadMultiPart = () async { try { - var res = await JsEngine() - .runCode("ComicSource.sources.$_key.explore[$i].load()"); - return Res(List.from(res.keys - .map((e) => ExplorePagePart( - e, - (res[e] as List) - .map((e) => Comic.fromJson(e, _key!)) - .toList(), - null)) - .toList())); + var res = await JsEngine().runCode( + "ComicSource.sources.$_key.explore[$i].load()", + ); + return Res( + List.from( + res.keys + .map( + (e) => ExplorePagePart( + e, + (res[e] as List) + .map((e) => Comic.fromJson(e, _key!)) + .toList(), + null, + ), + ) + .toList(), + ), + ); } catch (e, s) { Log.error("Data Analysis", "$e\n$s"); return Res.error(e.toString()); @@ -296,11 +316,15 @@ class ComicSourceParser { loadPage = (int page) async { try { var res = await JsEngine().runCode( - "ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})"); + "ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})", + ); return Res( - List.generate(res["comics"].length, - (index) => Comic.fromJson(res["comics"][index], _key!)), - subData: res["maxPage"]); + List.generate( + res["comics"].length, + (index) => Comic.fromJson(res["comics"][index], _key!), + ), + subData: res["maxPage"], + ); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); @@ -310,10 +334,13 @@ class ComicSourceParser { loadNext = (next) async { try { var res = await JsEngine().runCode( - "ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})"); + "ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})", + ); return Res( - List.generate(res["comics"].length, - (index) => Comic.fromJson(res["comics"][index], _key!)), + List.generate( + res["comics"].length, + (index) => Comic.fromJson(res["comics"][index], _key!), + ), subData: res["next"], ); } catch (e, s) { @@ -325,8 +352,9 @@ class ComicSourceParser { } else if (type == "multiPartPage") { loadMultiPart = () async { try { - var res = await JsEngine() - .runCode("ComicSource.sources.$_key.explore[$i].load()"); + var res = await JsEngine().runCode( + "ComicSource.sources.$_key.explore[$i].load()", + ); return Res( List.from( (res as List).map((e) { @@ -349,19 +377,22 @@ class ComicSourceParser { loadMixed = (index) async { try { var res = await JsEngine().runCode( - "ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})"); + "ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})", + ); var list = []; for (var data in (res['data'] as List)) { if (data is List) { list.add(data.map((e) => Comic.fromJson(e, _key!)).toList()); } else if (data is Map) { - list.add(ExplorePagePart( - data['title'], - (data['comics'] as List).map((e) { - return Comic.fromJson(e, _key!); - }).toList(), - data['viewMore'], - )); + list.add( + ExplorePagePart( + data['title'], + (data['comics'] as List).map((e) { + return Comic.fromJson(e, _key!); + }).toList(), + data['viewMore'], + ), + ); } } return Res(list, subData: res['maxPage']); @@ -371,21 +402,25 @@ class ComicSourceParser { } }; } - pages.add(ExplorePageData( - title, - switch (type) { - "singlePageWithMultiPart" => ExplorePageType.singlePageWithMultiPart, - "multiPartPage" => ExplorePageType.singlePageWithMultiPart, - "multiPageComicList" => ExplorePageType.multiPageComicList, - "mixed" => ExplorePageType.mixed, - _ => - throw ComicSourceParseException("Unknown explore page type $type") - }, - loadPage, - loadNext, - loadMultiPart, - loadMixed, - )); + pages.add( + ExplorePageData( + title, + switch (type) { + "singlePageWithMultiPart" => + ExplorePageType.singlePageWithMultiPart, + "multiPartPage" => ExplorePageType.singlePageWithMultiPart, + "multiPageComicList" => ExplorePageType.multiPageComicList, + "mixed" => ExplorePageType.mixed, + _ => throw ComicSourceParseException( + "Unknown explore page type $type", + ), + }, + loadPage, + loadNext, + loadMultiPart, + loadMixed, + ), + ); } return pages; } @@ -425,18 +460,17 @@ class ComicSourceParser { if (type == "fixed") { categoryParts.add(FixedCategoryPart(name, cs!)); } else if (type == "random") { - categoryParts - .add(RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1)); + categoryParts.add( + RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1), + ); } else if (type == "dynamic" && categories == null) { var loader = c["loader"]; if (loader is! JSInvokable) { throw "DynamicCategoryPart loader must be a function"; } - categoryParts.add(DynamicCategoryPart( - name, - JSAutoFreeFunction(loader), - _key!, - )); + categoryParts.add( + DynamicCategoryPart(name, JSAutoFreeFunction(loader), _key!), + ); } } else { // old format @@ -453,30 +487,16 @@ class ComicSourceParser { for (int i = 0; i < tags.length; i++) { PageJumpTarget target; if (itemType == 'category') { - target = PageJumpTarget( - _key!, - 'category', - { - "category": tags[i], - "param": categoryParams?.elementAtOrNull(i), - }, - ); + target = PageJumpTarget(_key!, 'category', { + "category": tags[i], + "param": categoryParams?.elementAtOrNull(i), + }); } else if (itemType == 'search') { - target = PageJumpTarget( - _key!, - 'search', - { - "keyword": tags[i], - }, - ); + target = PageJumpTarget(_key!, 'search', {"keyword": tags[i]}); } else if (itemType == 'search_with_namespace') { - target = PageJumpTarget( - _key!, - 'search', - { - "keyword": "$name:$tags[i]", - }, - ); + target = PageJumpTarget(_key!, 'search', { + "keyword": "$name:$tags[i]", + }); } else { target = PageJumpTarget(_key!, itemType, null); } @@ -485,38 +505,96 @@ class ComicSourceParser { if (type == "fixed") { categoryParts.add(FixedCategoryPart(name, cs)); } else if (type == "random") { - categoryParts - .add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1)); + categoryParts.add( + RandomCategoryPart(name, cs, c["randomNumber"] ?? 1), + ); } } } return CategoryData( - title: title, - categories: categoryParts, - enableRankingPage: enableRankingPage ?? false, - key: title); + title: title, + categories: categoryParts, + enableRankingPage: enableRankingPage ?? false, + key: title, + ); } CategoryComicsData? _loadCategoryComicsData() { if (!_checkExists("categoryComics")) return null; - var options = []; - for (var element in _getValue("categoryComics.optionList") ?? []) { - LinkedHashMap map = LinkedHashMap(); - for (var option in element["options"]) { - if (option.isEmpty || !option.contains("-")) { - continue; + + List? options; + if (_checkExists("categoryComics.optionList")) { + options = []; + for (var element in _getValue("categoryComics.optionList") ?? []) { + LinkedHashMap map = LinkedHashMap(); + for (var option in element["options"]) { + if (option.isEmpty || !option.contains("-")) { + continue; + } + var split = option.split("-"); + var key = split.removeAt(0); + var value = split.join("-"); + map[key] = value; } - var split = option.split("-"); - var key = split.removeAt(0); - var value = split.join("-"); - map[key] = value; + options.add( + CategoryComicsOptions( + element["label"] ?? "", + map, + List.from(element["notShowWhen"] ?? []), + element["showWhen"] == null ? null : List.from(element["showWhen"]), + ), + ); } - options.add(CategoryComicsOptions( - map, - List.from(element["notShowWhen"] ?? []), - element["showWhen"] == null ? null : List.from(element["showWhen"]))); } + + CategoryOptionsLoader? optionLoader; + if (_checkExists("categoryComics.optionLoader")) { + optionLoader = (category, param) async { + try { + dynamic res = JsEngine().runCode(""" + ComicSource.sources.$_key.categoryComics.optionLoader( + ${jsonEncode(category)}, ${jsonEncode(param)}) + """); + if (res is Future) { + res = await res; + } + if (res is! List) { + return Res.error("Invalid data:\nExpected: List\nGot: ${res.runtimeType}"); + } + var options = []; + for (var element in res) { + if (element is! Map) { + return Res.error("Invalid option data:\nExpected: Map\nGot: ${element.runtimeType}"); + } + LinkedHashMap map = LinkedHashMap(); + for (var option in element["options"] ?? []) { + if (option.isEmpty || !option.contains("-")) { + continue; + } + var split = option.split("-"); + var key = split.removeAt(0); + var value = split.join("-"); + map[key] = value; + } + options.add( + CategoryComicsOptions( + element["label"] ?? "", + map, + List.from(element["notShowWhen"] ?? []), + element["showWhen"] == null ? null : List.from(element["showWhen"]), + ), + ); + } + return Res(options); + } + catch(e) { + Log.error("Data Analysis", "Failed to load category options.\n$e"); + return Res.error(e.toString()); + } + }; + } + RankingData? rankingData; if (_checkExists("categoryComics.ranking")) { var options = {}; @@ -531,7 +609,7 @@ class ComicSourceParser { } Future>> Function(String option, int page)? load; Future>> Function(String option, String? next)? - loadWithNext; + loadWithNext; if (_checkExists("categoryComics.ranking.load")) { load = (option, page) async { try { @@ -540,9 +618,12 @@ class ComicSourceParser { ${jsonEncode(option)}, ${jsonEncode(page)}) """); return Res( - List.generate(res["comics"].length, - (index) => Comic.fromJson(res["comics"][index], _key!)), - subData: res["maxPage"]); + List.generate( + res["comics"].length, + (index) => Comic.fromJson(res["comics"][index], _key!), + ), + subData: res["maxPage"], + ); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); @@ -556,8 +637,10 @@ class ComicSourceParser { ${jsonEncode(option)}, ${jsonEncode(next)}) """); return Res( - List.generate(res["comics"].length, - (index) => Comic.fromJson(res["comics"][index], _key!)), + List.generate( + res["comics"].length, + (index) => Comic.fromJson(res["comics"][index], _key!), + ), subData: res["next"], ); } catch (e, s) { @@ -568,25 +651,38 @@ class ComicSourceParser { } rankingData = RankingData(options, load, loadWithNext); } - return CategoryComicsData(options, (category, param, options, page) async { - try { - var res = await JsEngine().runCode(""" - ComicSource.sources.$_key.categoryComics.load( - ${jsonEncode(category)}, - ${jsonEncode(param)}, - ${jsonEncode(options)}, - ${jsonEncode(page)} - ) - """); - return Res( - List.generate(res["comics"].length, - (index) => Comic.fromJson(res["comics"][index], _key!)), - subData: res["maxPage"]); - } catch (e, s) { - Log.error("Network", "$e\n$s"); - return Res.error(e.toString()); - } - }, rankingData: rankingData); + + if (options == null && optionLoader == null) { + options = []; + } + + return CategoryComicsData( + options: options, + optionsLoader: optionLoader, + load: (category, param, options, page) async { + try { + var res = await JsEngine().runCode(""" + ComicSource.sources.$_key.categoryComics.load( + ${jsonEncode(category)}, + ${jsonEncode(param)}, + ${jsonEncode(options)}, + ${jsonEncode(page)} + ) + """); + return Res( + List.generate( + res["comics"].length, + (index) => Comic.fromJson(res["comics"][index], _key!), + ), + subData: res["maxPage"], + ); + } catch (e, s) { + Log.error("Network", "$e\n$s"); + return Res.error(e.toString()); + } + }, + rankingData: rankingData, + ); } SearchPageData? _loadSearchData() { @@ -603,12 +699,14 @@ class ComicSourceParser { var value = split.join("-"); map[key] = value; } - options.add(SearchOptions( - map, - element["label"], - element['type'] ?? 'select', - element['default'] == null ? null : jsonEncode(element['default']), - )); + options.add( + SearchOptions( + map, + element["label"], + element['type'] ?? 'select', + element['default'] == null ? null : jsonEncode(element['default']), + ), + ); } SearchFunction? loadPage; @@ -623,9 +721,12 @@ class ComicSourceParser { ${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(page)}) """); return Res( - List.generate(res["comics"].length, - (index) => Comic.fromJson(res["comics"][index], _key!)), - subData: res["maxPage"]); + List.generate( + res["comics"].length, + (index) => Comic.fromJson(res["comics"][index], _key!), + ), + subData: res["maxPage"], + ); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); @@ -639,8 +740,10 @@ class ComicSourceParser { ${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(next)}) """); return Res( - List.generate(res["comics"].length, - (index) => Comic.fromJson(res["comics"][index], _key!)), + List.generate( + res["comics"].length, + (index) => Comic.fromJson(res["comics"][index], _key!), + ), subData: res["next"], ); } catch (e, s) { @@ -689,8 +792,9 @@ 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) { @@ -743,9 +847,12 @@ class ComicSourceParser { ${jsonEncode(page)}, ${jsonEncode(folder)}) """); return Res( - List.generate(res["comics"].length, - (index) => Comic.fromJson(res["comics"][index], _key!)), - subData: res["maxPage"]); + List.generate( + res["comics"].length, + (index) => Comic.fromJson(res["comics"][index], _key!), + ), + subData: res["maxPage"], + ); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); @@ -765,8 +872,10 @@ class ComicSourceParser { ${jsonEncode(next)}, ${jsonEncode(folder)}) """); return Res( - List.generate(res["comics"].length, - (index) => Comic.fromJson(res["comics"][index], _key!)), + List.generate( + res["comics"].length, + (index) => Comic.fromJson(res["comics"][index], _key!), + ), subData: res["next"], ); } catch (e, s) { @@ -857,8 +966,9 @@ class ComicSourceParser { ${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(page)}, ${jsonEncode(replyTo)}) """); return Res( - (res["comments"] as List).map((e) => Comment.fromJson(e)).toList(), - subData: res["maxPage"]); + (res["comments"] as List).map((e) => Comment.fromJson(e)).toList(), + subData: res["maxPage"], + ); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); @@ -1113,7 +1223,8 @@ class ComicSourceParser { ComicSource.sources.$_key.comic.archive.getArchives(${jsonEncode(cid)}) """); return Res( - (res as List).map((e) => ArchiveInfo.fromJson(e)).toList()); + (res as List).map((e) => ArchiveInfo.fromJson(e)).toList(), + ); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); diff --git a/lib/pages/category_comics_page.dart b/lib/pages/category_comics_page.dart index f29aaaf..3b8ce59 100644 --- a/lib/pages/category_comics_page.dart +++ b/lib/pages/category_comics_page.dart @@ -27,9 +27,11 @@ class CategoryComicsPage extends StatefulWidget { class _CategoryComicsPageState extends State { late final CategoryComicsData data; - late final List options; + late List? options; + late final CategoryOptionsLoader? optionsLoader; late List optionsValue; late String sourceKey; + String? error; void findData() { for (final source in ComicSource.all()) { @@ -38,24 +40,23 @@ class _CategoryComicsPageState extends State { throw "The comic source ${source.name} does not support category comics"; } data = source.categoryComicsData!; - options = data.options.where((element) { - if (element.notShowWhen.contains(widget.category)) { - return false; - } else if (element.showWhen != null) { - return element.showWhen!.contains(widget.category); - } - return true; - }).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; + if (data.options != null) { + options = data.options!.where((element) { + if (element.notShowWhen.contains(widget.category)) { + return false; + } else if (element.showWhen != null) { + return element.showWhen!.contains(widget.category); + } + return true; + }).toList(); + } else { + options = null; } + if (data.optionsLoader != null) { + optionsLoader = data.optionsLoader; + loadOptions(); + } + resetOptionsValue(); sourceKey = source.key; return; } @@ -63,6 +64,36 @@ class _CategoryComicsPageState extends State { throw "${widget.categoryKey} Not found"; } + void resetOptionsValue() { + if (options == null) return; + 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; + } + } + + void loadOptions() async { + final res = await optionsLoader!(widget.category, widget.param); + if (res.error) { + setState(() { + error = res.errorMessage; + }); + } else { + setState(() { + options = res.data; + resetOptionsValue(); + error = null; + }); + } + } + @override void initState() { if (widget.options != null) { @@ -77,27 +108,44 @@ class _CategoryComicsPageState extends State { @override Widget build(BuildContext context) { var topPadding = context.padding.top + 56.0; + + Widget body; + + if (options == null) { + body = Center(child: CircularProgressIndicator()); + } else if (error != null) { + body = NetworkError( + message: error!, + retry: () { + setState(() { + error = null; + }); + loadOptions(); + }, + ); + } else { + body = ComicList( + key: Key(widget.category + optionsValue.toString()), + errorLeading: buildOptions().paddingTop(topPadding), + leadingSliver: buildOptions().paddingTop(topPadding).toSliver(), + loadPage: (i) => + data.load(widget.category, widget.param, optionsValue, i), + ); + } + return Scaffold( extendBodyBehindAppBar: true, - appBar: Appbar( - title: Text(widget.category), - ), - body: ComicList( - key: Key(widget.category + optionsValue.toString()), - errorLeading: SizedBox(height: topPadding), - leadingSliver: buildOptions().paddingTop(topPadding).toSliver(), - loadPage: (i) => data.load( - widget.category, - widget.param, - optionsValue, - i, - ), - ), + appBar: Appbar(title: Text(widget.category)), + body: body, ); } Widget buildOptionItem( - String text, String value, int group, BuildContext context) { + String text, + String value, + int group, + BuildContext context, + ) { return OptionChip( text: text.ts(sourceKey), isSelected: value == optionsValue[group], @@ -112,23 +160,57 @@ class _CategoryComicsPageState extends State { Widget buildOptions() { List children = []; - for (var optionList in options) { - children.add(Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (var option in optionList.options.entries) - buildOptionItem( - option.value.tl, - option.key, - options.indexOf(optionList), - context, - ) - ], - )); - if (options.last != optionList) { + var group = 0; + for (var optionList in options!) { + if (optionList.label.isNotEmpty) { + children.add(Padding( + padding: const EdgeInsets.only( + bottom: 8.0, + left: 4.0, + ), + child: Text( + optionList.label.ts(sourceKey), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + )); + } + if (optionList.options.length <= 8) { + children.add( + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var option in optionList.options.entries) + buildOptionItem( + option.value.tl, + option.key, + group, + context, + ), + ], + ), + ); + } else { + var g = group; + children.add(Select( + current: optionList.options[optionsValue[g]], + values: optionList.options.values.toList(), + onTap: (i) { + var key = optionList.options.keys.elementAt(i); + if (key == optionsValue[g]) return; + setState(() { + optionsValue[g] = key; + }); + }, + )); + } + if (options!.last != optionList) { children.add(const SizedBox(height: 8)); } + group++; } return Column( mainAxisSize: MainAxisSize.min,