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/res.dart'; import 'package:venera/foundation/state_controller.dart'; import 'package:venera/utils/translations.dart'; class ExplorePage extends StatefulWidget { const ExplorePage({super.key}); @override State createState() => _ExplorePageState(); } class _ExplorePageState extends State with TickerProviderStateMixin { late TabController controller; bool showFB = true; double location = 0; late List pages; @override void initState() { pages = List.from(appdata.settings["explore_pages"]); var all = ComicSource.all() .map((e) => e.explorePages) .expand((e) => e.map((e) => e.title)) .toList(); pages = pages.where((e) => all.contains(e)).toList(); controller = TabController( length: pages.length, vsync: this, ); super.initState(); } void refresh() { int page = controller.index; String currentPageId = pages[page]; StateController.find(tag: currentPageId).refresh(); } Widget buildFAB() => Material( color: Colors.transparent, child: FloatingActionButton( key: const Key("FAB"), onPressed: refresh, child: const Icon(Icons.refresh), ), ); Tab buildTab(String i) { var comicSource = ComicSource.all() .firstWhere((e) => e.explorePages.any((e) => e.title == i)); return Tab(text: i.ts(comicSource.key), key: Key(i)); } Widget buildBody(String i) => _SingleExplorePage(i, key: Key(i)); @override Widget build(BuildContext context) { Widget tabBar = Material( child: FilledTabBar( tabs: pages.map((e) => buildTab(e)).toList(), controller: controller, ), ).paddingTop(context.padding.top); return Stack( children: [ Positioned.fill( child: Column( children: [ tabBar, Expanded( child: NotificationListener( onNotification: (notifications) { if (notifications.metrics.axis == Axis.horizontal) { if (!showFB) { setState(() { showFB = true; }); } return true; } var current = notifications.metrics.pixels; if ((current > location && current != 0) && showFB) { setState(() { showFB = false; }); } else if ((current < location || current == 0) && !showFB) { setState(() { showFB = true; }); } location = current; return false; }, child: MediaQuery.removePadding( context: context, removeTop: true, child: TabBarView( controller: controller, children: pages.map((e) => buildBody(e)).toList(), ), ), ), ) ], )), Positioned( right: 16, bottom: 16, child: AnimatedSwitcher( duration: const Duration(milliseconds: 150), reverseDuration: const Duration(milliseconds: 150), child: showFB ? buildFAB() : const SizedBox(), transitionBuilder: (widget, animation) { var tween = Tween( begin: const Offset(0, 1), end: const Offset(0, 0)); return SlideTransition( position: tween.animate(animation), child: widget, ); }, ), ) ], ); } } class _SingleExplorePage extends StatefulWidget { const _SingleExplorePage(this.title, {super.key}); final String title; @override State<_SingleExplorePage> createState() => _SingleExplorePageState(); } class _SingleExplorePageState extends StateWithController<_SingleExplorePage> { late final ExplorePageData data; bool loading = true; String? message; List? parts; late final String comicSourceKey; int key = 0; @override void initState() { super.initState(); for (var source in ComicSource.all()) { for (var d in source.explorePages) { if (d.title == widget.title) { data = d; comicSourceKey = source.key; return; } } } throw "Explore Page ${widget.title} Not Found!"; } @override Widget build(BuildContext context) { if (data.loadMultiPart != null) { return buildMultiPart(); } else if (data.loadPage != null || data.loadNext != null) { return buildComicList(); } else if (data.loadMixed != null) { return _MixedExplorePage( data, comicSourceKey, key: ValueKey(key), ); } else { return const Center( child: Text("Empty Page"), ); } } Widget buildComicList() { return ComicList( loadPage: data.loadPage, loadNext: data.loadNext, key: ValueKey(key), ); } void load() async { var res = await data.loadMultiPart!(); loading = false; if (mounted) { setState(() { if (res.error) { message = res.errorMessage; } else { parts = res.data; } }); } } Widget buildMultiPart() { if (loading) { load(); return const Center( child: CircularProgressIndicator(), ); } else if (message != null) { return NetworkError( message: message!, retry: refresh, withAppbar: false, ); } else { return buildPage(); } } Widget buildPage() { return SmoothCustomScrollView( slivers: _buildPage().toList(), ); } Iterable _buildPage() sync* { for (var part in parts!) { yield* _buildExplorePagePart(part, comicSourceKey); } } @override Object? get tag => widget.title; @override void refresh() { message = null; if (data.loadMultiPart != null) { setState(() { loading = true; }); } else { setState(() { key++; }); } } } class _MixedExplorePage extends StatefulWidget { const _MixedExplorePage(this.data, this.sourceKey, {super.key}); final ExplorePageData data; final String sourceKey; @override State<_MixedExplorePage> createState() => _MixedExplorePageState(); } class _MixedExplorePageState extends MultiPageLoadingState<_MixedExplorePage, Object> { Iterable buildSlivers(BuildContext context, List data) sync* { List cache = []; for (var part in data) { if (part is ExplorePagePart) { if (cache.isNotEmpty) { yield SliverGridComics( comics: (cache), ); yield const SliverToBoxAdapter(child: Divider()); cache.clear(); } yield* _buildExplorePagePart(part, widget.sourceKey); yield const SliverToBoxAdapter(child: Divider()); } else { cache.addAll(part as List); } } if (cache.isNotEmpty) { yield SliverGridComics( comics: (cache), ); } } @override Widget buildContent(BuildContext context, List data) { return SmoothCustomScrollView( slivers: [ ...buildSlivers(context, data), if (haveNextPage) const ListLoadingIndicator().toSliver() ], ); } @override Future>> loadData(int page) async { var res = await widget.data.loadMixed!(page); if (res.error) { return res; } for (var element in res.data) { if (element is! ExplorePagePart && element is! List) { return const Res.error("function loadMixed return invalid data"); } } return res; } } Iterable _buildExplorePagePart( ExplorePagePart part, String sourceKey) sync* { Widget buildTitle(ExplorePagePart part) { return SliverToBoxAdapter( child: SizedBox( height: 60, child: Padding( padding: const EdgeInsets.fromLTRB(16, 10, 5, 10), child: Row( children: [ Text( part.title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), ), const Spacer(), if (part.viewMore != null) TextButton( onPressed: () { // TODO: view more /* var context = App.mainNavigatorKey!.currentContext!; if (part.viewMore!.startsWith("search:")) { context.to( () => SearchResultPage( keyword: part.viewMore!.replaceFirst("search:", ""), 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, ), ); }*/ }, child: Text("查看更多".tl), ) ], ), ), ), ); } Widget buildComics(ExplorePagePart part) { return SliverGridComics(comics: part.comics); } yield buildTitle(part); yield buildComics(part); }