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