From 9a194e8394deb5be28d2ded65b6dee7ccd1aa65f Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 2 Oct 2024 10:50:46 +0800 Subject: [PATCH] categories page --- lib/components/appbar.dart | 2 +- lib/components/comic.dart | 4 +- lib/pages/categories_page.dart | 279 +++++++++++++++++++++++++++- lib/pages/category_comics_page.dart | 120 ++++++++++++ 4 files changed, 401 insertions(+), 4 deletions(-) create mode 100644 lib/pages/category_comics_page.dart diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 1f2124c..8624075 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -335,7 +335,7 @@ class _FilledTabBarState extends State { static const tabPadding = EdgeInsets.symmetric(horizontal: 6, vertical: 6); - static const tabRadius = 12.0; + static const tabRadius = 8.0; _IndicatorPainter? painter; diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 44170f2..b75f9ba 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -634,8 +634,8 @@ class _ComicListState extends State { } else { setState(() { data[page] = res.data; - if (res.subData?['maxPage'] != null) { - maxPage = res.subData['maxPage']; + if (res.subData != null && res.subData is int) { + maxPage = res.subData; } }); } diff --git a/lib/pages/categories_page.dart b/lib/pages/categories_page.dart index 7ec0fae..d07b0fe 100644 --- a/lib/pages/categories_page.dart +++ b/lib/pages/categories_page.dart @@ -1,10 +1,287 @@ import 'package:flutter/material.dart'; +import 'package:venera/components/components.dart'; +import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/appdata.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; +import 'package:venera/foundation/state_controller.dart'; +import 'package:venera/utils/translations.dart'; + +import 'category_comics_page.dart'; class CategoriesPage extends StatelessWidget { const CategoriesPage({super.key}); @override Widget build(BuildContext context) { - return const Placeholder(); + return StateBuilder( + tag: "category", + init: SimpleController(), + builder: (controller) { + var categories = List.from(appdata.settings["categories"]); + var allCategories = ComicSource.all() + .map((e) => e.categoryData?.key) + .where((element) => element != null) + .map((e) => e!) + .toList(); + categories = categories + .where((element) => allCategories.contains(element)) + .toList(); + + return Material( + child: DefaultTabController( + length: categories.length, + key: Key(categories.toString()), + child: Column( + children: [ + FilledTabBar( + tabs: categories.map((e) { + String title = e; + try { + title = getCategoryDataWithKey(e).title; + } catch (e) { + // + } + return Tab( + text: title, + key: Key(e), + ); + }).toList(), + ), + Expanded( + child: TabBarView( + children: + categories.map((e) => _CategoryPage(e)).toList()), + ) + ], + ), + ), + ); + }, + ); } } + +typedef ClickTagCallback = void Function(String, String?); + +class _CategoryPage extends StatelessWidget { + const _CategoryPage(this.category); + + final String category; + + CategoryData get data => getCategoryDataWithKey(category); + + String findComicSourceKey() { + for (var source in ComicSource.all()) { + if (source.categoryData?.key == category) { + return source.key; + } + } + return ""; + } + + void handleClick( + String tag, + String? param, + String type, + String namespace, + String categoryKey, + ) { + if (type == 'search') { + // TODO: Implement search + /* + App.mainNavigatorKey?.currentContext?.to( + () => SearchResultPage( + keyword: tag, + options: const [], + sourceKey: findComicSourceKey(), + ), + ); + */ + } else if (type == "search_with_namespace") { + /* + if (tag.contains(" ")) { + tag = '"$tag"'; + } + App.mainNavigatorKey?.currentContext?.to( + () => SearchResultPage( + keyword: "$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 = []; + if (data.enableRankingPage || data.buttons.isNotEmpty) { + children.add(buildTitle(data.title)); + children.add(Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 16), + child: Wrap( + children: [ + if (data.enableRankingPage) + buildTag("Ranking".tl, (p0, p1) { + // TODO: Implement ranking + /* + context.to(() => RankingPage(sourceKey: findComicSourceKey())); + + */ + }), + for (var buttonData in data.buttons) + buildTag(buttonData.label.tl, (p0, p1) => buttonData.onTap()) + ], + ), + )); + } + + for (var part in data.categories) { + if (part.enableRandom) { + children.add(StatefulBuilder(builder: (context, updater) { + return Column( + mainAxisSize: MainAxisSize.min, + 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, + ), + ) + ], + ); + })); + } 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, + ), + ), + ); + } + } + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ); + } + + Widget buildTitle(String title) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 5, 10), + child: Text(title.tl, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), + ); + } + + Widget buildTitleWithRefresh(String title, void Function() onRefresh) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 5, 10), + child: Row( + children: [ + Text( + title.tl, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + IconButton(onPressed: onRefresh, icon: const Icon(Icons.refresh)) + ], + ), + ); + } + + Widget buildTagsWithParams( + List tags, + List? params, + String? namespace, + ClickTagCallback onClick, + ) { + 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), + ), + ), + ), + ); + } + + Widget buildTag(String tag, ClickTagCallback onClick, + [String? namespace, String? param]) { + String translateTag(String tag) { + /* + // TODO: Implement translation + if (enableTranslation) { + if (namespace != null) { + tag = TagsTranslation.translationTagWithNamespace(tag, namespace); + } else { + tag = tag.translateTagsToCN; + } + } + + */ + return tag; + } + + return Padding( + padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), + child: Builder( + builder: (context) { + return Material( + elevation: 0.6, + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: context.colorScheme.surfaceContainerLow, + surfaceTintColor: Colors.transparent, + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(4)), + onTap: () => onClick(tag, param), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Text(translateTag(tag)), + ), + ), + ); + }, + ), + ); + } + + bool get enableTranslation => App.locale.languageCode == 'zh'; +} diff --git a/lib/pages/category_comics_page.dart b/lib/pages/category_comics_page.dart new file mode 100644 index 0000000..0b69a7e --- /dev/null +++ b/lib/pages/category_comics_page.dart @@ -0,0 +1,120 @@ +import "package:flutter/material.dart"; +import "package:venera/components/components.dart"; +import "package:venera/foundation/app.dart"; +import "package:venera/foundation/comic_source/comic_source.dart"; +import "package:venera/utils/translations.dart"; + +class CategoryComicsPage extends StatefulWidget { + const CategoryComicsPage({ + required this.category, + this.param, + required this.categoryKey, + super.key, + }); + + final String category; + + final String? param; + + final String categoryKey; + + @override + State createState() => _CategoryComicsPageState(); +} + +class _CategoryComicsPageState extends State { + late final CategoryComicsData data; + late final List options; + late List optionsValue; + + void findData() { + for (final source in ComicSource.all()) { + if (source.categoryData?.key == widget.categoryKey) { + 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(); + optionsValue = options.map((e) => e.options.keys.first).toList(); + return; + } + } + throw "${widget.categoryKey} Not found"; + } + + @override + void initState() { + findData(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: Appbar( + title: Text(widget.category), + ), + body: Column( + children: [ + Expanded( + child: ComicList( + loadPage: (i) => data.load( + widget.category, + widget.param, + optionsValue, + i, + ), + ), + ), + ], + ), + ); + } + + Widget buildOptionItem( + String text, String value, int group, BuildContext context) { + return OptionChip( + text: text, + isSelected: value == optionsValue[group], + onTap: () { + if (value == optionsValue[group]) return; + setState(() { + optionsValue[group] = value; + }); + }, + ); + } + + 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) { + children.add(const SizedBox(height: 8)); + } + } + return SliverToBoxAdapter( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [...children, const Divider()], + ).paddingLeft(8).paddingRight(8), + ); + } +}