Skip to content

Commit 6d22529

Browse files
committed
feat: sliver app bar in forum and thread page
Uses sliver app bar in forum page and thread page. All scroll controllers in list in these pages have been removed to make the global scroll behavior controls inner scrollable list. Now the top bar occupies much more spaces then before, leave it alone because more than three actions violates material spec, and in future probably here are new actions on the way. Another bottom bar implemention tried but not worked well, especially in the thread page where we already have the editor toolbar at the bottom of page.
1 parent ed662fe commit 6d22529

File tree

5 files changed

+216
-132
lines changed

5 files changed

+216
-132
lines changed

lib/features/forum/view/forum_page.dart

Lines changed: 119 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:easy_refresh/easy_refresh.dart';
2+
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
23
import 'package:flutter/material.dart';
34
import 'package:flutter/rendering.dart';
45
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -57,9 +58,10 @@ class ForumPage extends StatefulWidget {
5758
}
5859

5960
class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMixin, LoggerMixin {
60-
final _pinnedScrollController = ScrollController();
61+
// final _pinnedScrollController = ScrollController();
6162
final _pinnedRefreshController = EasyRefreshController(controlFinishRefresh: true);
62-
final _subredditScrollController = ScrollController();
63+
64+
// final _subredditScrollController = ScrollController();
6365
final _subredditRefreshController = EasyRefreshController(controlFinishRefresh: true);
6466

6567
/// Controller of thread tab.
@@ -74,7 +76,7 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
7476
/// Visibility of FAB.
7577
bool _fabVisible = true;
7678

77-
void _updateFabVisibilityByTabIndex() {
79+
void _updateFabVisibilityByTabIndex() => WidgetsBinding.instance.addPostFrameCallback((_) {
7880
if (tabController.index == _threadTabIndex) {
7981
setState(() {
8082
_fabVisible = true;
@@ -84,39 +86,56 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
8486
_fabVisible = false;
8587
});
8688
}
87-
}
89+
_threadScrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.ease);
90+
});
8891

8992
PreferredSizeWidget _buildListAppBar(BuildContext context, ForumState state) {
9093
return ListAppBar(
9194
title: widget.title ?? state.title,
9295
bottom: state.permissionDeniedMessage == null
93-
? TabBar(
94-
controller: tabController,
95-
tabs: [
96-
Tab(child: Text(context.t.forumPage.stickThreadTab.title)),
97-
Tab(child: Text(context.t.forumPage.threadTab.title)),
98-
Tab(child: Text(context.t.forumPage.subredditTab.title)),
99-
],
100-
onTap: (index) {
101-
// Here we want to scroll the current tab to the top.
102-
// Only scroll to top when user taps on the current
103-
// tab, which means index is not changing.
104-
if (tabController.indexIsChanging) {
105-
// Do nothing because user tapped another index
106-
// and want to switch to it.
107-
return;
108-
}
109-
const duration = Duration(milliseconds: 300);
110-
const curve = Curves.ease;
111-
switch (tabController.index) {
112-
case _pinnedTabIndex:
113-
_pinnedScrollController.animateTo(0, duration: duration, curve: curve);
114-
case _threadTabIndex:
115-
_threadScrollController.animateTo(0, duration: duration, curve: curve);
116-
case _subredditTabIndex:
117-
_subredditScrollController.animateTo(0, duration: duration, curve: curve);
118-
}
119-
},
96+
? PreferredSize(
97+
// There the preferred size is manually calculated.
98+
// Not following material standard and need update when widgets in the column changed.
99+
preferredSize: const Size.fromHeight(kToolbarHeight + 30),
100+
child: Column(
101+
children: [
102+
TabBar(
103+
controller: tabController,
104+
tabs: [
105+
Tab(child: Text(context.t.forumPage.stickThreadTab.title)),
106+
Tab(child: Text(context.t.forumPage.threadTab.title)),
107+
Tab(child: Text(context.t.forumPage.subredditTab.title)),
108+
],
109+
onTap: (index) {
110+
// Here we want to scroll the current tab to the top.
111+
// Only scroll to top when user taps on the current
112+
// tab, which means index is not changing.
113+
if (tabController.indexIsChanging) {
114+
// Do nothing because user tapped another index
115+
// and want to switch to it.
116+
return;
117+
}
118+
// Now the controller in unique.
119+
_threadScrollController.animateTo(
120+
0,
121+
duration: const Duration(milliseconds: 300),
122+
curve: Curves.ease,
123+
);
124+
// switch (tabController.index) {
125+
// case _pinnedTabIndex:
126+
// _pinnedScrollController.animateTo(0, duration: duration, curve: curve);
127+
// case _threadTabIndex:
128+
// _threadScrollController.animateTo(0, duration: duration, curve: curve);
129+
// case _subredditTabIndex:
130+
// _subredditScrollController.animateTo(0, duration: duration, curve: curve);
131+
// }
132+
},
133+
),
134+
sizedBoxW4H4,
135+
_buildNormalThreadFilterRow(context, state),
136+
sizedBoxW4H4,
137+
],
138+
),
120139
)
121140
: null,
122141
onSearch: () async {
@@ -166,22 +185,18 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
166185
return Row(
167186
children: [
168187
Expanded(
169-
child: Container(
170-
color: Theme.of(context).colorScheme.surface,
171-
height: 40,
172-
child: SingleChildScrollView(
173-
scrollDirection: Axis.horizontal,
174-
child: Row(
175-
mainAxisSize: MainAxisSize.min,
176-
children: <Widget>[
177-
if (state.filterTypeList.isNotEmpty) const ThreadTypeChip(),
178-
if (state.filterSpecialTypeList.isNotEmpty) const ThreadSpecialTypeChip(),
179-
if (state.filterDatelineList.isNotEmpty) const ThreadDatelineChip(),
180-
if (state.filterOrderList.isNotEmpty) const ThreadOrderChip(),
181-
const ThreadDigestChip(),
182-
const ThreadRecommendedChip(),
183-
].prepend(sizedBoxW4H4).insertBetween(sizedBoxW12H12),
184-
),
188+
child: SingleChildScrollView(
189+
scrollDirection: Axis.horizontal,
190+
child: Row(
191+
mainAxisSize: MainAxisSize.min,
192+
children: <Widget>[
193+
if (state.filterTypeList.isNotEmpty) const ThreadTypeChip(),
194+
if (state.filterSpecialTypeList.isNotEmpty) const ThreadSpecialTypeChip(),
195+
if (state.filterDatelineList.isNotEmpty) const ThreadDatelineChip(),
196+
if (state.filterOrderList.isNotEmpty) const ThreadOrderChip(),
197+
const ThreadDigestChip(),
198+
const ThreadRecommendedChip(),
199+
].prepend(sizedBoxW4H4).insertBetween(sizedBoxW12H12),
185200
),
186201
),
187202
),
@@ -196,15 +211,15 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
196211
late final Widget content;
197212
if (state.rulesElement == null) {
198213
content = ListView.separated(
199-
controller: _pinnedScrollController,
214+
// controller: _pinnedScrollController,
200215
padding: edgeInsetsL12T4R12,
201216
itemCount: state.stickThreadList.length,
202217
itemBuilder: (context, index) => NormalThreadCard(state.stickThreadList[index]),
203218
separatorBuilder: (context, index) => sizedBoxW4H4,
204219
);
205220
} else {
206221
content = ListView.separated(
207-
controller: _pinnedScrollController,
222+
// controller: _pinnedScrollController,
208223
padding: edgeInsetsL12T4R12.add(context.safePadding()),
209224
itemCount: state.stickThreadList.length + 1,
210225
itemBuilder: (context, index) {
@@ -227,7 +242,7 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
227242
scrollBehaviorBuilder: (physics) => ERScrollBehavior(physics).copyWith(physics: physics, scrollbars: false),
228243
header: const MaterialHeader(),
229244
controller: _pinnedRefreshController,
230-
scrollController: _pinnedScrollController,
245+
// scrollController: _pinnedScrollController,
231246
onRefresh: () async {
232247
if (!mounted) {
233248
return;
@@ -248,10 +263,7 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
248263
if (state.filterState.isFiltering()) {
249264
return Column(
250265
crossAxisAlignment: CrossAxisAlignment.start,
251-
children: [
252-
_buildNormalThreadFilterRow(context, state),
253-
Expanded(child: emptyContentHint),
254-
],
266+
children: [Expanded(child: emptyContentHint)],
255267
);
256268
}
257269
return emptyContentHint;
@@ -264,7 +276,7 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
264276
header: const MaterialHeader(),
265277
footer: const MaterialFooter(),
266278
controller: _threadRefreshController,
267-
scrollController: _threadScrollController,
279+
// scrollController: _threadScrollController,
268280
onRefresh: () async {
269281
if (!mounted) {
270282
return;
@@ -286,10 +298,9 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
286298
// _refreshController.finishLoad();
287299
},
288300
childBuilder: (context, physics) => CustomScrollView(
289-
controller: _threadScrollController,
301+
// controller: _threadScrollController,
290302
physics: physics,
291303
slivers: [
292-
PinnedHeaderSliver(child: _buildNormalThreadFilterRow(context, state)),
293304
const SliverPadding(padding: edgeInsetsL12T4R12),
294305
SliverList.separated(
295306
itemCount: normalThreadList.length,
@@ -330,22 +341,6 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
330341
}
331342
}
332343

333-
Widget _buildBody(BuildContext context, ForumState state) {
334-
return switch (state.status) {
335-
ForumStatus.initial || ForumStatus.loading => Column(
336-
crossAxisAlignment: CrossAxisAlignment.start,
337-
children: [
338-
if (state.filterState.isFiltering()) _buildNormalThreadFilterRow(context, state),
339-
const Expanded(child: Center(child: CircularProgressIndicator())),
340-
],
341-
),
342-
ForumStatus.failure => buildRetryButton(context, () {
343-
context.read<ForumBloc>().add(ForumLoadMoreRequested(state.currentPage));
344-
}),
345-
ForumStatus.success => _buildSuccessContent(context, state),
346-
};
347-
}
348-
349344
Widget _buildSubredditTab(BuildContext context, List<Forum> subredditList) {
350345
if (subredditList.isEmpty) {
351346
return Center(child: Text(context.t.forumPage.subredditTab.noSubreddit));
@@ -355,15 +350,15 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
355350
scrollBehaviorBuilder: (physics) => ERScrollBehavior(physics).copyWith(physics: physics, scrollbars: false),
356351
header: const MaterialHeader(),
357352
controller: _subredditRefreshController,
358-
scrollController: _subredditScrollController,
353+
// scrollController: _subredditScrollController,
359354
onRefresh: () async {
360355
if (!mounted) {
361356
return;
362357
}
363358
context.read<ForumBloc>().add(ForumRefreshRequested());
364359
},
365360
child: ListView.separated(
366-
controller: _subredditScrollController,
361+
// controller: _subredditScrollController,
367362
padding: edgeInsetsL12T4R12.add(context.safePadding()),
368363
itemCount: subredditList.length,
369364
itemBuilder: (context, index) => ForumCard(subredditList[index]),
@@ -414,11 +409,11 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
414409

415410
@override
416411
void dispose() {
417-
_pinnedScrollController.dispose();
412+
// _pinnedScrollController.dispose();
418413
_pinnedRefreshController.dispose();
419414
_threadScrollController.dispose();
420415
_threadRefreshController.dispose();
421-
_subredditScrollController.dispose();
416+
// _subredditScrollController.dispose();
422417
_subredditRefreshController.dispose();
423418
tabController.dispose();
424419
super.dispose();
@@ -472,12 +467,55 @@ class _ForumPageState extends State<ForumPage> with SingleTickerProviderStateMix
472467
}
473468

474469
return Scaffold(
475-
appBar: _buildListAppBar(context, state),
476-
body: NotificationListener<UserScrollNotification>(
477-
onNotification: _onBodyScrollNotification,
478-
child: SafeArea(bottom: false, child: _buildBody(context, state)),
470+
body: ExtendedNestedScrollView(
471+
controller: _threadScrollController,
472+
onlyOneScrollInBody: true,
473+
headerSliverBuilder: (context, innerBoxIsScroller) => [_buildListAppBar(context, state)],
474+
body: NotificationListener(
475+
onNotification: _onBodyScrollNotification,
476+
child: switch (state.status) {
477+
ForumStatus.initial || ForumStatus.loading => const Center(child: CircularProgressIndicator()),
478+
ForumStatus.failure => buildRetryButton(context, () {
479+
context.read<ForumBloc>().add(ForumLoadMoreRequested(state.currentPage));
480+
}),
481+
ForumStatus.success => _buildSuccessContent(context, state),
482+
},
483+
),
479484
),
485+
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
480486
floatingActionButton: _buildFloatingActionButton(context, state),
487+
// bottomNavigationBar: AnimatedContainer(
488+
// curve: Curves.easeIn,
489+
// duration: duration200,
490+
// height: _fabVisible ? 100 : 0,
491+
// child: BottomAppBar(
492+
// padding: edgeInsetsL12R12,
493+
// child: Align(
494+
// alignment: Alignment.centerLeft,
495+
// child: SingleChildScrollView(
496+
// child: Wrap(
497+
// runAlignment: WrapAlignment.center,
498+
// children: [
499+
// const NoticeButton(),
500+
// const OpenInAppPageButton(),
501+
// IconButton(
502+
// icon: const Icon(Icons.search_outlined),
503+
// tooltip: context.t.searchPage.title,
504+
// onPressed: () async {
505+
// await context.pushNamed(ScreenPaths.search, queryParameters: {'fid': widget.fid});
506+
// },
507+
// ),
508+
// IconButton(
509+
// icon: const Icon(Icons.settings_outlined),
510+
// tooltip: context.t.general.openSettings,
511+
// onPressed: () => context.pushNamed(ScreenPaths.rootSettings),
512+
// ),
513+
// ],
514+
// ),
515+
// ),
516+
// ),
517+
// ),
518+
// ),
481519
);
482520
},
483521
),

lib/features/open_in_app/view/open_in_app_page.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:collection/collection.dart';
22
import 'package:flutter/material.dart';
33
import 'package:go_router/go_router.dart';
4+
import 'package:material_symbols_icons/material_symbols_icons.dart';
45
import 'package:tsdm_client/constants/layout.dart';
56
import 'package:tsdm_client/extensions/build_context.dart';
67
import 'package:tsdm_client/extensions/string.dart';
@@ -17,7 +18,7 @@ class OpenInAppPageButton extends StatelessWidget {
1718
@override
1819
Widget build(BuildContext context) {
1920
return IconButton(
20-
icon: const Icon(Icons.open_in_new),
21+
icon: const Icon(Symbols.top_panel_open),
2122
tooltip: context.t.openInAppPage.entryTooltip,
2223
onPressed: () async => context.pushNamed(ScreenPaths.openInApp),
2324
);

0 commit comments

Comments
 (0)