+172
-130
lib/widgets/bottom_nav_bar.dart
+172
-130
lib/widgets/bottom_nav_bar.dart
···
1
1
+
import 'dart:async';
1
2
import 'package:flutter/material.dart';
2
3
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
4
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
···
9
10
import 'package:grain/providers/profile_provider.dart';
10
11
import 'package:grain/widgets/app_image.dart';
11
12
12
12
-
class BottomNavBar extends ConsumerWidget {
13
13
+
class BottomNavBar extends ConsumerStatefulWidget {
13
14
final int navIndex;
14
15
final VoidCallback onHome;
15
16
final VoidCallback onExplore;
···
26
27
});
27
28
28
29
@override
29
29
-
Widget build(BuildContext context, WidgetRef ref) {
30
30
+
ConsumerState<BottomNavBar> createState() => _BottomNavBarState();
31
31
+
}
32
32
+
33
33
+
class _BottomNavBarState extends ConsumerState<BottomNavBar> {
34
34
+
int _pressedIndex = -1;
35
35
+
36
36
+
void _onHoldTap(int index, VoidCallback callback) {
37
37
+
setState(() => _pressedIndex = index);
38
38
+
callback();
39
39
+
Future.delayed(const Duration(milliseconds: 200), () {
40
40
+
setState(() => _pressedIndex = -1);
41
41
+
});
42
42
+
}
43
43
+
44
44
+
Widget _buildNavItem({
45
45
+
required int index,
46
46
+
required Widget icon,
47
47
+
required VoidCallback onHoldComplete,
48
48
+
}) {
49
49
+
return Expanded(
50
50
+
child: _NavItem(
51
51
+
index: index,
52
52
+
isPressed: _pressedIndex == index,
53
53
+
icon: icon,
54
54
+
onHoldComplete: () => _onHoldTap(index, onHoldComplete),
55
55
+
),
56
56
+
);
57
57
+
}
58
58
+
59
59
+
@override
60
60
+
Widget build(BuildContext context) {
61
61
+
final theme = Theme.of(context);
30
62
final did = apiService.currentUser?.did;
31
63
final asyncProfile = did != null
32
64
? ref.watch(profileNotifierProvider(did))
···
37
69
orElse: () => null,
38
70
);
39
71
40
40
-
final theme = Theme.of(context);
41
41
-
42
42
-
// Get unread notifications count
43
72
final notifications = ref.watch(notificationsProvider);
44
73
final unreadCount = notifications.maybeWhen(
45
45
-
data: (list) => list.where((n) => n.isRead == false).length,
74
74
+
data: (list) => list.where((n) => !n.isRead).length,
46
75
orElse: () => 0,
47
76
);
48
77
49
78
return Container(
50
79
decoration: BoxDecoration(
51
51
-
color: Theme.of(context).scaffoldBackgroundColor,
52
52
-
border: Border(top: BorderSide(color: Theme.of(context).dividerColor, width: 1)),
80
80
+
color: theme.scaffoldBackgroundColor,
81
81
+
border: Border(top: BorderSide(color: theme.dividerColor, width: 1)),
53
82
),
54
54
-
height: 42 + MediaQuery.of(context).padding.bottom,
83
83
+
height: 56 + MediaQuery.of(context).padding.bottom,
84
84
+
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
55
85
child: Row(
56
56
-
mainAxisAlignment: MainAxisAlignment.spaceAround,
57
86
children: [
58
58
-
Expanded(
59
59
-
child: GestureDetector(
60
60
-
behavior: HitTestBehavior.opaque,
61
61
-
onTap: onHome,
62
62
-
child: SizedBox(
63
63
-
height: 42 + MediaQuery.of(context).padding.bottom,
64
64
-
child: Transform.translate(
65
65
-
offset: const Offset(0, -10),
66
66
-
child: Center(
67
67
-
child: FaIcon(
68
68
-
AppIcons.house,
69
69
-
size: 20,
70
70
-
color: navIndex == 0
71
71
-
? AppTheme.primaryColor
72
72
-
: Theme.of(context).colorScheme.onSurfaceVariant,
73
73
-
),
74
74
-
),
75
75
-
),
76
76
-
),
87
87
+
_buildNavItem(
88
88
+
index: 0,
89
89
+
onHoldComplete: widget.onHome,
90
90
+
icon: FaIcon(
91
91
+
AppIcons.house,
92
92
+
size: 20,
93
93
+
color: widget.navIndex == 0
94
94
+
? AppTheme.primaryColor
95
95
+
: theme.colorScheme.onSurfaceVariant,
77
96
),
78
97
),
79
79
-
Expanded(
80
80
-
child: GestureDetector(
81
81
-
behavior: HitTestBehavior.opaque,
82
82
-
onTap: onExplore,
83
83
-
child: SizedBox(
84
84
-
height: 42 + MediaQuery.of(context).padding.bottom,
85
85
-
child: Transform.translate(
86
86
-
offset: const Offset(0, -10),
87
87
-
child: Center(
88
88
-
child: FaIcon(
89
89
-
AppIcons.magnifyingGlass,
90
90
-
size: 20,
91
91
-
color: navIndex == 1
92
92
-
? AppTheme.primaryColor
93
93
-
: Theme.of(context).colorScheme.onSurfaceVariant,
94
94
-
),
95
95
-
),
96
96
-
),
97
97
-
),
98
98
+
_buildNavItem(
99
99
+
index: 1,
100
100
+
onHoldComplete: widget.onExplore,
101
101
+
icon: FaIcon(
102
102
+
AppIcons.magnifyingGlass,
103
103
+
size: 20,
104
104
+
color: widget.navIndex == 1
105
105
+
? AppTheme.primaryColor
106
106
+
: theme.colorScheme.onSurfaceVariant,
98
107
),
99
108
),
100
100
-
Expanded(
101
101
-
child: GestureDetector(
102
102
-
behavior: HitTestBehavior.opaque,
103
103
-
onTap: onNotifications,
104
104
-
child: SizedBox(
105
105
-
height: 42 + MediaQuery.of(context).padding.bottom,
106
106
-
child: Transform.translate(
107
107
-
offset: const Offset(0, -10),
108
108
-
child: Stack(
109
109
-
alignment: Alignment.center,
110
110
-
children: [
111
111
-
Center(
112
112
-
child: FaIcon(
113
113
-
AppIcons.solidBell,
114
114
-
size: 20,
115
115
-
color: navIndex == 2
116
116
-
? AppTheme.primaryColor
117
117
-
: Theme.of(context).colorScheme.onSurfaceVariant,
109
109
+
_buildNavItem(
110
110
+
index: 2,
111
111
+
onHoldComplete: widget.onNotifications,
112
112
+
icon: Stack(
113
113
+
alignment: Alignment.center,
114
114
+
children: [
115
115
+
FaIcon(
116
116
+
AppIcons.solidBell,
117
117
+
size: 20,
118
118
+
color: widget.navIndex == 2
119
119
+
? AppTheme.primaryColor
120
120
+
: theme.colorScheme.onSurfaceVariant,
121
121
+
),
122
122
+
if (unreadCount > 0)
123
123
+
Positioned(
124
124
+
right: 0,
125
125
+
top: 0,
126
126
+
child: Container(
127
127
+
padding:
128
128
+
const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
129
129
+
decoration: BoxDecoration(
130
130
+
color: theme.colorScheme.primary,
131
131
+
borderRadius: BorderRadius.circular(10),
132
132
+
border: Border.all(
133
133
+
color: theme.scaffoldBackgroundColor,
134
134
+
width: 1,
118
135
),
119
136
),
120
120
-
if (unreadCount > 0)
121
121
-
Align(
122
122
-
alignment: Alignment.center,
123
123
-
child: Transform.translate(
124
124
-
offset: const Offset(10, -10),
125
125
-
child: Container(
126
126
-
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
127
127
-
decoration: BoxDecoration(
128
128
-
color: theme.colorScheme.primary,
129
129
-
borderRadius: BorderRadius.circular(10),
130
130
-
border: Border.all(color: theme.scaffoldBackgroundColor, width: 1),
131
131
-
),
132
132
-
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
133
133
-
child: Text(
134
134
-
unreadCount > 99 ? '99+' : unreadCount.toString(),
135
135
-
style: const TextStyle(
136
136
-
color: Colors.white,
137
137
-
fontSize: 10,
138
138
-
fontWeight: FontWeight.bold,
139
139
-
),
140
140
-
textAlign: TextAlign.center,
141
141
-
),
142
142
-
),
143
143
-
),
137
137
+
constraints:
138
138
+
const BoxConstraints(minWidth: 16, minHeight: 16),
139
139
+
child: Text(
140
140
+
unreadCount > 99 ? '99+' : unreadCount.toString(),
141
141
+
style: const TextStyle(
142
142
+
color: Colors.white,
143
143
+
fontSize: 10,
144
144
+
fontWeight: FontWeight.bold,
144
145
),
145
145
-
],
146
146
+
textAlign: TextAlign.center,
147
147
+
),
148
148
+
),
146
149
),
147
147
-
),
148
148
-
),
150
150
+
],
149
151
),
150
152
),
151
151
-
Expanded(
152
152
-
child: GestureDetector(
153
153
-
behavior: HitTestBehavior.opaque,
154
154
-
onTap: onProfile,
155
155
-
child: SizedBox(
156
156
-
height: 42 + MediaQuery.of(context).padding.bottom,
157
157
-
child: Transform.translate(
158
158
-
offset: const Offset(0, -10),
159
159
-
child: Center(
160
160
-
child: avatarUrl != null && avatarUrl.isNotEmpty
161
161
-
? Container(
162
162
-
width: 28,
163
163
-
height: 28,
164
164
-
alignment: Alignment.center,
165
165
-
decoration: navIndex == 3
166
166
-
? BoxDecoration(
167
167
-
shape: BoxShape.circle,
168
168
-
border: Border.all(color: AppTheme.primaryColor, width: 2.2),
169
169
-
)
170
170
-
: null,
171
171
-
child: ClipOval(
172
172
-
child: AppImage(
173
173
-
url: avatarUrl,
174
174
-
width: 24,
175
175
-
height: 24,
176
176
-
fit: BoxFit.cover,
177
177
-
),
178
178
-
),
179
179
-
)
180
180
-
: FaIcon(
181
181
-
navIndex == 3 ? AppIcons.solidUser : AppIcons.user,
182
182
-
size: 16,
183
183
-
color: navIndex == 3
184
184
-
? AppTheme.primaryColor
185
185
-
: Theme.of(context).colorScheme.onSurfaceVariant,
186
186
-
),
187
187
-
),
153
153
+
_buildNavItem(
154
154
+
index: 3,
155
155
+
onHoldComplete: widget.onProfile,
156
156
+
icon: avatarUrl != null && avatarUrl.isNotEmpty
157
157
+
? Container(
158
158
+
width: 28,
159
159
+
height: 28,
160
160
+
alignment: Alignment.center,
161
161
+
decoration: widget.navIndex == 3
162
162
+
? BoxDecoration(
163
163
+
shape: BoxShape.circle,
164
164
+
border: Border.all(
165
165
+
color: AppTheme.primaryColor, width: 2.2),
166
166
+
)
167
167
+
: null,
168
168
+
child: ClipOval(
169
169
+
child: AppImage(
170
170
+
url: avatarUrl,
171
171
+
width: 24,
172
172
+
height: 24,
173
173
+
fit: BoxFit.cover,
188
174
),
189
175
),
176
176
+
)
177
177
+
: FaIcon(
178
178
+
widget.navIndex == 3
179
179
+
? AppIcons.solidUser
180
180
+
: AppIcons.user,
181
181
+
size: 16,
182
182
+
color: widget.navIndex == 3
183
183
+
? AppTheme.primaryColor
184
184
+
: theme.colorScheme.onSurfaceVariant,
190
185
),
191
186
),
192
187
],
···
194
189
);
195
190
}
196
191
}
192
192
+
193
193
+
class _NavItem extends StatefulWidget {
194
194
+
final Widget icon;
195
195
+
final VoidCallback onHoldComplete;
196
196
+
final int index;
197
197
+
final bool isPressed;
198
198
+
199
199
+
const _NavItem({
200
200
+
required this.icon,
201
201
+
required this.onHoldComplete,
202
202
+
required this.index,
203
203
+
required this.isPressed,
204
204
+
});
205
205
+
206
206
+
@override
207
207
+
State<_NavItem> createState() => _NavItemState();
208
208
+
}
209
209
+
210
210
+
class _NavItemState extends State<_NavItem> {
211
211
+
bool _pressed = false;
212
212
+
213
213
+
@override
214
214
+
Widget build(BuildContext context) {
215
215
+
return GestureDetector(
216
216
+
onTapDown: (_) {
217
217
+
setState(() => _pressed = true);
218
218
+
},
219
219
+
onTapUp: (_) {
220
220
+
Future.delayed(const Duration(milliseconds: 200), () {
221
221
+
setState(() => _pressed = false);
222
222
+
});
223
223
+
},
224
224
+
onTapCancel: () {
225
225
+
setState(() => _pressed = false);
226
226
+
},
227
227
+
onTap: widget.onHoldComplete,
228
228
+
behavior: HitTestBehavior.opaque,
229
229
+
child: Center(
230
230
+
child: AnimatedScale(
231
231
+
scale: _pressed ? 0.85 : 1.0,
232
232
+
duration: const Duration(milliseconds: 150),
233
233
+
child: widget.icon,
234
234
+
),
235
235
+
),
236
236
+
);
237
237
+
}
238
238
+
}