Skip to content

Commit 9d7d3bb

Browse files
committed
fix(notification): basic notice search
1 parent 7a821b0 commit 9d7d3bb

File tree

7 files changed

+263
-107
lines changed

7 files changed

+263
-107
lines changed

lib/features/notification/view/notification_page.dart

Lines changed: 138 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import 'dart:math' as math;
33
import 'package:easy_refresh/easy_refresh.dart';
44
import 'package:flutter/material.dart';
55
import 'package:flutter_bloc/flutter_bloc.dart';
6+
import 'package:go_router/go_router.dart';
67
import 'package:tsdm_client/constants/layout.dart';
78
import 'package:tsdm_client/features/notification/bloc/auto_notification_cubit.dart';
89
import 'package:tsdm_client/features/notification/bloc/notification_bloc.dart';
910
import 'package:tsdm_client/features/notification/bloc/notification_state_cubit.dart';
1011
import 'package:tsdm_client/i18n/strings.g.dart';
12+
import 'package:tsdm_client/routes/screen_paths.dart';
1113
import 'package:tsdm_client/shared/models/notification_type.dart';
1214
import 'package:tsdm_client/utils/logger.dart';
1315
import 'package:tsdm_client/utils/retry_button.dart';
@@ -38,19 +40,29 @@ class _NotificationPageState extends State<NotificationPage> with SingleTickerPr
3840
return Align(
3941
child: LayoutBuilder(
4042
builder:
41-
(context, constraints) => SingleChildScrollView(
43+
(context, constraints) =>
44+
SingleChildScrollView(
4245
physics: const AlwaysScrollableScrollPhysics(),
4346
child: ConstrainedBox(
4447
constraints: BoxConstraints(
45-
minWidth: MediaQuery.sizeOf(context).width,
48+
minWidth: MediaQuery
49+
.sizeOf(context)
50+
.width,
4651
minHeight: constraints.maxHeight,
4752
),
4853
child: Center(
4954
child: Text(
5055
context.t.general.noData,
51-
style: Theme.of(
56+
style: Theme
57+
.of(
5258
context,
53-
).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.outline),
59+
)
60+
.textTheme
61+
.titleMedium
62+
?.copyWith(color: Theme
63+
.of(context)
64+
.colorScheme
65+
.outline),
5466
),
5567
),
5668
),
@@ -85,9 +97,15 @@ class _NotificationPageState extends State<NotificationPage> with SingleTickerPr
8597
if (state.status == NotificationStatus.failure) {
8698
showFailedToLoadSnackBar(context);
8799
} else if (state.status == NotificationStatus.success) {
88-
final n = state.noticeList.where((e) => !e.alreadyRead).length;
89-
final pm = state.personalMessageList.where((e) => !e.alreadyRead).length;
90-
final bm = state.broadcastMessageList.where((e) => !e.alreadyRead).length;
100+
final n = state.noticeList
101+
.where((e) => !e.alreadyRead)
102+
.length;
103+
final pm = state.personalMessageList
104+
.where((e) => !e.alreadyRead)
105+
.length;
106+
final bm = state.broadcastMessageList
107+
.where((e) => !e.alreadyRead)
108+
.length;
91109
context.read<NotificationStateCubit>().setAll(
92110
noticeCount: n,
93111
personalMessageCount: pm,
@@ -98,74 +116,77 @@ class _NotificationPageState extends State<NotificationPage> with SingleTickerPr
98116
child: BlocBuilder<NotificationBloc, NotificationState>(
99117
builder: (context, state) {
100118
final (n, pm, bm) = switch (onlyShowUnread) {
101-
true => (
102-
state.noticeList.where((e) => !e.alreadyRead),
103-
state.personalMessageList.where((e) => !e.alreadyRead),
104-
state.broadcastMessageList.where((e) => !e.alreadyRead),
119+
true =>
120+
(
121+
state.noticeList.where((e) => !e.alreadyRead),
122+
state.personalMessageList.where((e) => !e.alreadyRead),
123+
state.broadcastMessageList.where((e) => !e.alreadyRead),
105124
),
106125
false => (state.noticeList, state.personalMessageList, state.broadcastMessageList),
107126
};
108127

109128
final body = switch (state.status) {
110129
NotificationStatus.initial ||
111130
NotificationStatus.loading => const Center(child: CircularProgressIndicator()),
112-
NotificationStatus.success => TabBarView(
113-
controller: _tabController,
114-
children: [
115-
EasyRefresh.builder(
116-
controller: _noticeRefreshController,
117-
header: const MaterialHeader(),
118-
onRefresh: () => context.read<NotificationBloc>().add(NotificationUpdateAllRequested()),
119-
childBuilder:
120-
(context, physics) =>
121-
n.isEmpty
122-
? _buildEmptyBody(context)
123-
: ListView.separated(
124-
physics: physics,
125-
padding: edgeInsetsL12T4R12B4,
126-
itemCount: n.length,
127-
itemBuilder: (_, idx) => NoticeCardV2(n.elementAt(idx)),
128-
separatorBuilder: (_, __) => sizedBoxW4H4,
129-
),
131+
NotificationStatus.success =>
132+
TabBarView(
133+
controller: _tabController,
134+
children: [
135+
EasyRefresh.builder(
136+
controller: _noticeRefreshController,
137+
header: const MaterialHeader(),
138+
onRefresh: () => context.read<NotificationBloc>().add(NotificationUpdateAllRequested()),
139+
childBuilder:
140+
(context, physics) =>
141+
n.isEmpty
142+
? _buildEmptyBody(context)
143+
: ListView.separated(
144+
physics: physics,
145+
padding: edgeInsetsL12T4R12B4,
146+
itemCount: n.length,
147+
itemBuilder: (_, idx) => NoticeCardV2(n.elementAt(idx)),
148+
separatorBuilder: (_, __) => sizedBoxW4H4,
149+
),
150+
),
151+
EasyRefresh.builder(
152+
controller: _personalMessageRefreshController,
153+
header: const MaterialHeader(),
154+
onRefresh: () => context.read<NotificationBloc>().add(NotificationUpdateAllRequested()),
155+
childBuilder:
156+
(context, physics) =>
157+
pm.isEmpty
158+
? _buildEmptyBody(context)
159+
: ListView.separated(
160+
physics: physics,
161+
padding: edgeInsetsL12T4R12B4,
162+
itemCount: pm.length,
163+
itemBuilder: (_, idx) => PersonalMessageCardV2(pm.elementAt(idx)),
164+
separatorBuilder: (_, __) => sizedBoxW4H4,
165+
),
166+
),
167+
EasyRefresh.builder(
168+
controller: _broadcastMessageRefreshController,
169+
header: const MaterialHeader(),
170+
onRefresh: () => context.read<NotificationBloc>().add(NotificationUpdateAllRequested()),
171+
childBuilder:
172+
(context, physics) =>
173+
bm.isEmpty
174+
? _buildEmptyBody(context)
175+
: ListView.separated(
176+
physics: physics,
177+
padding: edgeInsetsL12T4R12B4,
178+
itemCount: bm.length,
179+
itemBuilder: (_, idx) => BroadcastMessageCardV2(bm.elementAt(idx)),
180+
separatorBuilder: (_, __) => sizedBoxW4H4,
181+
),
182+
),
183+
],
130184
),
131-
EasyRefresh.builder(
132-
controller: _personalMessageRefreshController,
133-
header: const MaterialHeader(),
134-
onRefresh: () => context.read<NotificationBloc>().add(NotificationUpdateAllRequested()),
135-
childBuilder:
136-
(context, physics) =>
137-
pm.isEmpty
138-
? _buildEmptyBody(context)
139-
: ListView.separated(
140-
physics: physics,
141-
padding: edgeInsetsL12T4R12B4,
142-
itemCount: pm.length,
143-
itemBuilder: (_, idx) => PersonalMessageCardV2(pm.elementAt(idx)),
144-
separatorBuilder: (_, __) => sizedBoxW4H4,
145-
),
185+
NotificationStatus.failure =>
186+
buildRetryButton(
187+
context,
188+
() => context.read<NotificationBloc>().add(NotificationUpdateAllRequested()),
146189
),
147-
EasyRefresh.builder(
148-
controller: _broadcastMessageRefreshController,
149-
header: const MaterialHeader(),
150-
onRefresh: () => context.read<NotificationBloc>().add(NotificationUpdateAllRequested()),
151-
childBuilder:
152-
(context, physics) =>
153-
bm.isEmpty
154-
? _buildEmptyBody(context)
155-
: ListView.separated(
156-
physics: physics,
157-
padding: edgeInsetsL12T4R12B4,
158-
itemCount: bm.length,
159-
itemBuilder: (_, idx) => BroadcastMessageCardV2(bm.elementAt(idx)),
160-
separatorBuilder: (_, __) => sizedBoxW4H4,
161-
),
162-
),
163-
],
164-
),
165-
NotificationStatus.failure => buildRetryButton(
166-
context,
167-
() => context.read<NotificationBloc>().add(NotificationUpdateAllRequested()),
168-
),
169190
};
170191
return Scaffold(
171192
appBar: AppBar(
@@ -179,40 +200,45 @@ class _NotificationPageState extends State<NotificationPage> with SingleTickerPr
179200
setState(() => onlyShowUnread = !onlyShowUnread);
180201
},
181202
),
203+
IconButton(
204+
icon: const Icon(Icons.saved_search_outlined),
205+
onPressed: () => context.pushNamed(ScreenPaths.noticeSearch),
206+
),
182207
PopupMenuButton<_Actions>(
183208
itemBuilder:
184-
(_) => [
185-
PopupMenuItem(
186-
value: _Actions.markAllNoticeAsRead,
187-
child: Row(
188-
children: [
189-
const Icon(Icons.notifications_paused_outlined),
190-
sizedBoxPopupMenuItemIconSpacing,
191-
Text(tr.cardMenu.markAllNoticeAsRead),
192-
],
193-
),
194-
),
195-
PopupMenuItem(
196-
value: _Actions.markAllPersonalMessageAsRead,
197-
child: Row(
198-
children: [
199-
const Icon(Icons.notifications_active_outlined),
200-
sizedBoxPopupMenuItemIconSpacing,
201-
Text(tr.cardMenu.markAllPersonalMessageAsRead),
202-
],
203-
),
204-
),
205-
PopupMenuItem(
206-
value: _Actions.markAllBroadcastMessageAsRead,
207-
child: Row(
208-
children: [
209-
const Icon(Icons.notification_important_outlined),
210-
sizedBoxPopupMenuItemIconSpacing,
211-
Text(tr.cardMenu.markAllBroadcastMessageAsRead),
212-
],
213-
),
214-
),
215-
],
209+
(_) =>
210+
[
211+
PopupMenuItem(
212+
value: _Actions.markAllNoticeAsRead,
213+
child: Row(
214+
children: [
215+
const Icon(Icons.notifications_paused_outlined),
216+
sizedBoxPopupMenuItemIconSpacing,
217+
Text(tr.cardMenu.markAllNoticeAsRead),
218+
],
219+
),
220+
),
221+
PopupMenuItem(
222+
value: _Actions.markAllPersonalMessageAsRead,
223+
child: Row(
224+
children: [
225+
const Icon(Icons.notifications_active_outlined),
226+
sizedBoxPopupMenuItemIconSpacing,
227+
Text(tr.cardMenu.markAllPersonalMessageAsRead),
228+
],
229+
),
230+
),
231+
PopupMenuItem(
232+
value: _Actions.markAllBroadcastMessageAsRead,
233+
child: Row(
234+
children: [
235+
const Icon(Icons.notification_important_outlined),
236+
sizedBoxPopupMenuItemIconSpacing,
237+
Text(tr.cardMenu.markAllBroadcastMessageAsRead),
238+
],
239+
),
240+
),
241+
],
216242
onSelected: (value) async {
217243
final noticeType = switch (value) {
218244
_Actions.markAllNoticeAsRead => NotificationType.notice,
@@ -252,16 +278,21 @@ class _PreferredSizeComponentBottom extends StatelessWidget implements Preferred
252278
builder: (context, state) {
253279
return switch (state) {
254280
AutoNoticeStateStopped() => sizedBoxW2H2,
255-
AutoNoticeStateTicking(:final total, :final remain) => LinearProgressIndicator(
256-
value: math.max(1 - remain.inSeconds / total.inSeconds, 0),
257-
minHeight: 2,
258-
),
281+
AutoNoticeStateTicking(:final total, :final remain) =>
282+
LinearProgressIndicator(
283+
value: math.max(1 - remain.inSeconds / total.inSeconds, 0),
284+
minHeight: 2,
285+
),
259286
AutoNoticeStatePending() => const LinearProgressIndicator(minHeight: 2),
260-
AutoNoticeStatePaused(:final total, :final remain) => LinearProgressIndicator(
261-
value: math.max(1 - remain.inSeconds / total.inSeconds, 0),
262-
minHeight: 2,
263-
color: Theme.of(context).colorScheme.outline,
264-
),
287+
AutoNoticeStatePaused(:final total, :final remain) =>
288+
LinearProgressIndicator(
289+
value: math.max(1 - remain.inSeconds / total.inSeconds, 0),
290+
minHeight: 2,
291+
color: Theme
292+
.of(context)
293+
.colorScheme
294+
.outline,
295+
),
265296
};
266297
},
267298
),
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:tsdm_client/constants/layout.dart';
4+
import 'package:tsdm_client/extensions/list.dart';
5+
import 'package:tsdm_client/features/notification/bloc/notification_bloc.dart';
6+
import 'package:tsdm_client/features/notification/models/models.dart';
7+
import 'package:tsdm_client/i18n/strings.g.dart';
8+
import 'package:tsdm_client/widgets/card/notice_card_v2.dart';
9+
10+
/// Gather all kinds of notifications.
11+
final class _SavedNotifications {
12+
/// Constructor.
13+
const _SavedNotifications({
14+
required this.noticeList,
15+
required this.personalMessageList,
16+
required this.broadcastMessageList,
17+
});
18+
19+
/// All saved notice.
20+
final List<NoticeV2> noticeList;
21+
22+
/// All saved personal messages.
23+
final List<PersonalMessageV2> personalMessageList;
24+
25+
/// All saved broadcast messages.
26+
final List<BroadcastMessageV2> broadcastMessageList;
27+
}
28+
29+
/// Search and filter notifications.
30+
class NotificationSearchPage extends StatefulWidget {
31+
/// Constructor.
32+
const NotificationSearchPage({super.key});
33+
34+
@override
35+
State<NotificationSearchPage> createState() => _NotificationSearchPageState();
36+
}
37+
38+
class _NotificationSearchPageState extends State<NotificationSearchPage> {
39+
_SavedNotifications? notice;
40+
41+
var _searchContent = '';
42+
43+
@override
44+
Widget build(BuildContext context) {
45+
if (notice == null) {
46+
final state = context.read<NotificationBloc>().state;
47+
notice = _SavedNotifications(
48+
noticeList: state.noticeList,
49+
personalMessageList: state.personalMessageList,
50+
broadcastMessageList: state.broadcastMessageList,
51+
);
52+
}
53+
54+
final tr = context.t.noticeSearchPage;
55+
return Scaffold(
56+
appBar: AppBar(
57+
title: SearchBar(
58+
autoFocus: true,
59+
hintText: tr.title,
60+
onChanged: (str) {
61+
setState(() {
62+
_searchContent = str;
63+
});
64+
},
65+
),
66+
),
67+
body: ListView(
68+
padding: edgeInsetsL12T4R12B4,
69+
children: <Widget>[
70+
...notice!.noticeList.where((e) => e.data.contains(_searchContent)).map(NoticeCardV2.new),
71+
...notice!.personalMessageList.where((e) => e.data.contains(_searchContent)).map(PersonalMessageCardV2.new),
72+
...notice!.broadcastMessageList.where((e) => e.data.contains(_searchContent)).map(BroadcastMessageCardV2.new),
73+
].insertBetween(sizedBoxW4H4),
74+
),
75+
);
76+
}
77+
}

0 commit comments

Comments
 (0)