From 1a34272f64c0013a5ae05aa94d3de2f54feb6896 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 24 Sep 2024 10:10:46 +0800 Subject: [PATCH] [school] less sliver lists --- lib/design/animation/progress.dart | 26 ++++ lib/school/class2nd/page/activity.dart | 172 ++++++++++--------------- lib/school/oa_announce/page/list.dart | 144 ++++++++------------- lib/school/ywb/page/application.dart | 137 ++++++++------------ 4 files changed, 196 insertions(+), 283 deletions(-) diff --git a/lib/design/animation/progress.dart b/lib/design/animation/progress.dart index 67bf2838..ff22bc63 100644 --- a/lib/design/animation/progress.dart +++ b/lib/design/animation/progress.dart @@ -98,3 +98,29 @@ class BlockWhenLoading extends StatelessWidget { ].stack(); } } + +class WhenLoading extends StatelessWidget { + final bool loading; + final Widget child; + + const WhenLoading({ + super.key, + required this.child, + this.loading = true, + }); + + @override + Widget build(BuildContext context) { + return [ + AnimatedOpacity( + opacity: loading ? 0.5 : 1, + duration: Durations.short4, + child: child, + ), + if (loading) + Positioned.fill( + child: const CircularProgressIndicator().center(), + ), + ].stack(); + } +} diff --git a/lib/school/class2nd/page/activity.dart b/lib/school/class2nd/page/activity.dart index e1ac2bc1..1385defc 100644 --- a/lib/school/class2nd/page/activity.dart +++ b/lib/school/class2nd/page/activity.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; +import 'package:mimir/design/animation/progress.dart'; import 'package:rettulf/rettulf.dart'; import 'package:mimir/design/adaptive/multiplatform.dart'; import 'package:mimir/design/widget/common.dart'; @@ -23,85 +24,52 @@ class ActivityListPage extends StatefulWidget { } class _ActivityListPageState extends State { - final $loadingStates = ValueNotifier(commonClass2ndCategories.map((cat) => false).toList()); - - @override - void dispose() { - $loadingStates.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return $loadingStates >> - (ctx, states) => Scaffold( - floatingActionButton: - !states.any((state) => state == true) ? null : const CircularProgressIndicator.adaptive(), - body: DefaultTabController( - length: commonClass2ndCategories.length, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - // These are the slivers that show up in the "outer" scroll view. - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.title.text(), - forceElevated: innerBoxIsScrolled, - actions: [ - PlatformIconButton( - icon: Icon(context.icons.search), - onPressed: () => showSearch( - useRootNavigator: true, - context: context, - delegate: ActivitySearchDelegate(), - ), - ), - ], - bottom: TabBar( - isScrollable: true, - tabs: commonClass2ndCategories - .mapIndexed( - (i, e) => Tab( - child: e.l10nName().text(), - ), - ) - .toList(), - ), - ), - ), - ]; - }, - body: TabBarView( - // These are the contents of the tab views, below the tabs. - children: commonClass2ndCategories.mapIndexed((i, cat) { - return ActivityLoadingList( - cat: cat, - onLoadingChanged: (state) { - final newStates = List.of($loadingStates.value); - newStates[i] = state; - $loadingStates.value = newStates; - }, - ); - }).toList(), - ), - ), + return DefaultTabController( + length: commonClass2ndCategories.length, + child: Scaffold( + appBar: AppBar( + title: i18n.title.text(), + actions: [ + PlatformIconButton( + icon: Icon(context.icons.search), + onPressed: () => showSearch( + useRootNavigator: true, + context: context, + delegate: ActivitySearchDelegate(), ), - ); + ), + ], + bottom: TabBar( + isScrollable: true, + tabs: commonClass2ndCategories + .mapIndexed( + (i, e) => Tab( + child: e.l10nName().text(), + ), + ) + .toList(), + ), + ), + body: TabBarView( + // These are the contents of the tab views, below the tabs. + children: commonClass2ndCategories.mapIndexed((i, cat) { + return ActivityLoadingList(cat: cat); + }).toList(), + ), + ), + ); } } /// Thanks to the cache, don't worry about that switching tab will re-fetch the activity list. class ActivityLoadingList extends StatefulWidget { final Class2ndActivityCat cat; - final ValueChanged onLoadingChanged; const ActivityLoadingList({ super.key, required this.cat, - required this.onLoadingChanged, }); @override @@ -110,7 +78,7 @@ class ActivityLoadingList extends StatefulWidget { class _ActivityLoadingListState extends State with AutomaticKeepAliveClientMixin { int lastPage = 1; - bool isFetching = false; + bool fetching = false; late var activities = Class2ndInit.activityStorage.getActivities(widget.cat); @override @@ -135,51 +103,45 @@ class _ActivityLoadingListState extends State with Automati } return true; }, - child: CustomScrollView( - // CAN'T USE ScrollController, and I don't know why - // controller: scrollController, - slivers: [ - SliverOverlapInjector( - // This is the flip side of the SliverOverlapAbsorber above. - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (activities != null) - if (activities.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noActivities, - ), - ) - else - SliverList.builder( - itemCount: activities.length, - itemBuilder: (ctx, index) { - final activity = activities[index]; - return ActivityCard( - activity, - onTap: () async { - await context.push( - "/class2nd/activity-details/${activity.id}?title=${activity.title}&time=${activity.time}&enable-apply=true", - ); - }, + child: activities == null + ? const SizedBox() + : WhenLoading( + loading: fetching, + child: buildList(activities), + ), + ); + } + + Widget buildList(List activities) { + return activities.isEmpty + ? LeavingBlank( + icon: Icons.inbox_outlined, + desc: i18n.noActivities, + ) + : ListView.builder( + itemCount: activities.length, + itemBuilder: (ctx, index) { + final activity = activities[index]; + return ActivityCard( + activity, + onTap: () async { + await context.push( + "/class2nd/activity-details/${activity.id}?title=${activity.title}&time=${activity.time}&enable-apply=true", ); }, - ), - ], - ), - ); + ); + }, + ); } Future loadMore() async { final cat = widget.cat; if (!cat.canFetchData) return; - if (isFetching) return; + if (fetching) return; if (!mounted) return; setState(() { - isFetching = true; + fetching = true; }); - widget.onLoadingChanged(true); try { final lastActivities = await Class2ndInit.activityService.getActivityList(cat, lastPage); final activities = this.activities ?? []; @@ -192,16 +154,14 @@ class _ActivityLoadingListState extends State with Automati setState(() { lastPage++; this.activities = activities; - isFetching = false; + fetching = false; }); - widget.onLoadingChanged(false); } catch (error, stackTrace) { handleRequestError(error, stackTrace); if (!mounted) return; setState(() { - isFetching = false; + fetching = false; }); - widget.onLoadingChanged(false); } } } diff --git a/lib/school/oa_announce/page/list.dart b/lib/school/oa_announce/page/list.dart index ccc68ee8..9bb7497d 100644 --- a/lib/school/oa_announce/page/list.dart +++ b/lib/school/oa_announce/page/list.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mimir/credentials/init.dart'; +import 'package:mimir/design/animation/progress.dart'; import 'package:mimir/design/widget/common.dart'; import 'package:mimir/school/oa_announce/widget/tile.dart'; @@ -44,73 +45,42 @@ class OaAnnounceListPageInternal extends StatefulWidget { } class _OaAnnounceListPageInternalState extends State { - late final $loadingStates = ValueNotifier(widget.cats.map((cat) => false).toList()); - - @override - void dispose() { - $loadingStates.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return $loadingStates >> - (ctx, states) => Scaffold( - floatingActionButton: - !states.any((state) => state == true) ? null : const CircularProgressIndicator.adaptive(), - body: DefaultTabController( - length: widget.cats.length, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - // These are the slivers that show up in the "outer" scroll view. - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.title.text(), - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - isScrollable: true, - tabs: widget.cats - .map((cat) => Tab( - child: cat.l10nName().text(), - )) - .toList(), - ), - ), - ), - ]; - }, - body: TabBarView( - // These are the contents of the tab views, below the tabs. - children: widget.cats.mapIndexed((i, cat) { - return OaAnnounceLoadingList( - key: ValueKey(cat), - cat: cat, - onLoadingChanged: (state) { - final newStates = List.of($loadingStates.value); - newStates[i] = state; - $loadingStates.value = newStates; - }, - ); - }).toList(), - ), - ), - ), + return DefaultTabController( + length: widget.cats.length, + child: Scaffold( + appBar: AppBar( + title: i18n.title.text(), + bottom: TabBar( + isScrollable: true, + tabs: widget.cats + .map((cat) => Tab( + child: cat.l10nName().text(), + )) + .toList(), + ), + ), + body: TabBarView( + // These are the contents of the tab views, below the tabs. + children: widget.cats.mapIndexed((i, cat) { + return OaAnnounceLoadingList( + key: ValueKey(cat), + cat: cat, ); + }).toList(), + ), + ), + ); } } class OaAnnounceLoadingList extends StatefulWidget { final OaAnnounceCat cat; - final ValueChanged onLoadingChanged; const OaAnnounceLoadingList({ super.key, required this.cat, - required this.onLoadingChanged, }); @override @@ -119,7 +89,7 @@ class OaAnnounceLoadingList extends StatefulWidget { class _OaAnnounceLoadingListState extends State with AutomaticKeepAliveClientMixin { int lastPage = 1; - bool isFetching = false; + bool fetching = false; late var announcements = OaAnnounceInit.storage.getAnnouncements(widget.cat); @override @@ -144,44 +114,38 @@ class _OaAnnounceLoadingListState extends State with Auto } return true; }, - child: CustomScrollView( - // CAN'T USE ScrollController, and I don't know why - // controller: scrollController, - slivers: [ - SliverOverlapInjector( - // This is the flip side of the SliverOverlapAbsorber above. - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (announcements != null) - if (announcements.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noOaAnnouncementsTip, - ), - ) - else - SliverList.builder( - itemCount: announcements.length, - itemBuilder: (ctx, index) { - return Card.filled( - clipBehavior: Clip.hardEdge, - child: OaAnnounceTile(announcements[index]), - ); - }, - ), - ], - ), + child: announcements == null + ? const SizedBox() + : WhenLoading( + loading: fetching, + child: buildList(announcements), + ), ); } + Widget buildList(List announcements) { + return announcements.isEmpty + ? LeavingBlank( + icon: Icons.inbox_outlined, + desc: i18n.noOaAnnouncementsTip, + ) + : ListView.builder( + itemCount: announcements.length, + itemBuilder: (ctx, index) { + return Card.filled( + clipBehavior: Clip.hardEdge, + child: OaAnnounceTile(announcements[index]), + ); + }, + ); + } + Future loadMore() async { - if (isFetching) return; + if (fetching) return; if (!mounted) return; setState(() { - isFetching = true; + fetching = true; }); - widget.onLoadingChanged(true); final cat = widget.cat; try { final lastPayload = await OaAnnounceInit.service.getAnnounceList(cat, lastPage); @@ -194,16 +158,14 @@ class _OaAnnounceLoadingListState extends State with Auto setState(() { lastPage = max(lastPage + 1, lastPayload.totalPage); this.announcements = announcements; - isFetching = false; + fetching = false; }); - widget.onLoadingChanged(false); } catch (error, stackTrace) { handleRequestError(error, stackTrace); if (!mounted) return; setState(() { - isFetching = false; + fetching = false; }); - widget.onLoadingChanged(false); } } } diff --git a/lib/school/ywb/page/application.dart b/lib/school/ywb/page/application.dart index f5e359fd..c9fa050c 100644 --- a/lib/school/ywb/page/application.dart +++ b/lib/school/ywb/page/application.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:mimir/design/animation/progress.dart'; import 'package:mimir/design/widget/common.dart'; import 'package:rettulf/rettulf.dart'; import 'package:mimir/utils/error.dart'; @@ -17,72 +18,39 @@ class YwbMyApplicationListPage extends StatefulWidget { } class _YwbMyApplicationListPageState extends State { - late final $loadingStates = ValueNotifier(YwbApplicationType.values.map((type) => false).toList()); - - @override - void dispose() { - $loadingStates.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return $loadingStates >> - (ctx, states) => Scaffold( - floatingActionButton: - !states.any((state) => state == true) ? null : const CircularProgressIndicator.adaptive(), - body: DefaultTabController( - length: YwbApplicationType.values.length, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - // These are the slivers that show up in the "outer" scroll view. - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.mine.title.text(), - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - isScrollable: true, - tabs: YwbApplicationType.values - .map((type) => Tab( - child: type.l10nName().text(), - )) - .toList(), - ), - ), - ), - ]; - }, - body: TabBarView( - // These are the contents of the tab views, below the tabs. - children: YwbApplicationType.values.mapIndexed((i, type) { - return YwbApplicationLoadingList( - type: type, - onLoadingChanged: (bool value) { - final newStates = List.of($loadingStates.value); - newStates[i] = value; - $loadingStates.value = newStates; - }, - ); - }).toList(), - ), - ), - ), - ); + return DefaultTabController( + length: YwbApplicationType.values.length, + child: Scaffold( + appBar: AppBar( + title: i18n.mine.title.text(), + bottom: TabBar( + isScrollable: true, + tabs: YwbApplicationType.values + .map((type) => Tab( + child: type.l10nName().text(), + )) + .toList(), + ), + ), + body: TabBarView( + // These are the contents of the tab views, below the tabs. + children: YwbApplicationType.values.mapIndexed((i, type) { + return YwbApplicationLoadingList(type: type); + }).toList(), + ), + ), + ); } } class YwbApplicationLoadingList extends StatefulWidget { final YwbApplicationType type; - final ValueChanged onLoadingChanged; const YwbApplicationLoadingList({ super.key, required this.type, - required this.onLoadingChanged, }); @override @@ -90,7 +58,7 @@ class YwbApplicationLoadingList extends StatefulWidget { } class _YwbApplicationLoadingListState extends State with AutomaticKeepAliveClientMixin { - bool isFetching = false; + bool fetching = false; late var applications = YwbInit.applicationStorage.getApplicationListOf(widget.type); @override @@ -108,38 +76,37 @@ class _YwbApplicationLoadingListState extends State w Widget build(BuildContext context) { super.build(context); final applications = this.applications; - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - // This is the flip side of the SliverOverlapAbsorber above. - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (applications != null) - if (applications.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.mine.noApplicationsTip, - ), - ) - else - SliverList.builder( - itemCount: applications.length, - itemBuilder: (ctx, index) { - return YwbApplicationTile(applications[index]); - }, - ), - ], - ); + return applications == null + ? const SizedBox() + : WhenLoading( + loading: fetching, + child: buildList(applications), + ); + } + + Widget buildList(List applications) { + return applications.isEmpty + ? LeavingBlank( + icon: Icons.inbox_outlined, + desc: i18n.mine.noApplicationsTip, + ) + : ListView.builder( + itemCount: applications.length, + itemBuilder: (ctx, index) { + return Card.filled( + clipBehavior: Clip.hardEdge, + child: YwbApplicationTile(applications[index]), + ); + }, + ); } Future fetch() async { - if (isFetching) return; + if (fetching) return; if (!mounted) return; setState(() { - isFetching = true; + fetching = true; }); - widget.onLoadingChanged(true); final type = widget.type; try { final applications = await YwbInit.applicationService.getApplicationsOf(type); @@ -147,16 +114,14 @@ class _YwbApplicationLoadingListState extends State w if (!mounted) return; setState(() { this.applications = applications; - isFetching = false; + fetching = false; }); - widget.onLoadingChanged(false); } catch (error, stackTrace) { handleRequestError(error, stackTrace); if (!mounted) return; setState(() { - isFetching = false; + fetching = false; }); - widget.onLoadingChanged(false); } } }