+1
-1
lib/screens/gallery_action_sheet.dart
+1
-1
lib/screens/gallery_action_sheet.dart
+7
-4
lib/screens/gallery_edit_photos_sheet.dart
+7
-4
lib/screens/gallery_edit_photos_sheet.dart
···
47
47
backgroundColor: theme.colorScheme.surface,
48
48
border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)),
49
49
middle: Text(
50
-
'Edit Gallery Photos',
50
+
'Edit photos',
51
51
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
52
52
),
53
-
leading: CupertinoButton(
53
+
trailing: CupertinoButton(
54
54
padding: EdgeInsets.zero,
55
55
onPressed: (_loading || _deletingPhotoIndex != null)
56
56
? null
57
-
: () => Navigator.of(context).maybePop(),
57
+
: () {
58
+
widget.onSave(_photos);
59
+
Navigator.of(context).maybePop();
60
+
},
58
61
child: Text(
59
-
'Cancel',
62
+
'Done',
60
63
style: TextStyle(
61
64
color: _deletingPhotoIndex != null ? theme.disabledColor : theme.colorScheme.primary,
62
65
fontWeight: FontWeight.w600,
+1
-1
lib/screens/gallery_sort_order_sheet.dart
+1
-1
lib/screens/gallery_sort_order_sheet.dart
···
34
34
backgroundColor: theme.colorScheme.surface,
35
35
border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)),
36
36
middle: Text(
37
-
'Change Sort Order',
37
+
'Edit sort order',
38
38
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
39
39
),
40
40
leading: CupertinoButton(
+102
-173
lib/screens/home_page.dart
+102
-173
lib/screens/home_page.dart
···
4
4
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
5
5
import 'package:grain/api.dart';
6
6
import 'package:grain/screens/create_gallery_page.dart';
7
-
import 'package:grain/widgets/app_version_text.dart';
7
+
import 'package:grain/widgets/app_drawer.dart';
8
8
import 'package:grain/widgets/bottom_nav_bar.dart';
9
9
import 'package:grain/widgets/timeline_item.dart';
10
10
11
11
import '../providers/gallery_cache_provider.dart';
12
12
import 'explore_page.dart';
13
-
import 'log_page.dart';
13
+
// ...existing code...
14
14
import 'notifications_page.dart';
15
15
import 'profile_page.dart';
16
16
···
26
26
}
27
27
28
28
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
29
+
PreferredSizeWidget _buildAppBar(ThemeData theme, {required String title}) {
30
+
return AppBar(
31
+
backgroundColor: theme.appBarTheme.backgroundColor,
32
+
surfaceTintColor: theme.appBarTheme.backgroundColor,
33
+
elevation: 0.5,
34
+
title: Text(title, style: theme.appBarTheme.titleTextStyle),
35
+
leading: Builder(
36
+
builder: (context) => IconButton(
37
+
color: theme.colorScheme.onSurfaceVariant,
38
+
iconSize: 20,
39
+
icon: const Icon(FontAwesomeIcons.bars),
40
+
onPressed: () => Scaffold.of(context).openDrawer(),
41
+
),
42
+
),
43
+
actions: [
44
+
IconButton(
45
+
color: theme.colorScheme.onSurfaceVariant,
46
+
iconSize: 20,
47
+
icon: const Icon(FontAwesomeIcons.arrowRightFromBracket),
48
+
tooltip: 'Sign Out',
49
+
onPressed: widget.onSignOut,
50
+
),
51
+
],
52
+
);
53
+
}
54
+
29
55
bool showProfile = false;
30
56
bool showNotifications = false;
31
57
bool showExplore = false;
···
115
141
);
116
142
}
117
143
118
-
Widget _buildAppDrawer(ThemeData theme, String? avatarUrl) {
119
-
return Drawer(
120
-
child: ListView(
121
-
padding: EdgeInsets.zero,
122
-
children: [
123
-
Container(
124
-
height: 250,
125
-
decoration: BoxDecoration(
126
-
color: theme.colorScheme.surface,
127
-
border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)),
128
-
),
129
-
padding: const EdgeInsets.fromLTRB(16, 115, 16, 16),
130
-
child: Column(
131
-
crossAxisAlignment: CrossAxisAlignment.start,
132
-
mainAxisAlignment: MainAxisAlignment.center,
133
-
children: [
134
-
CircleAvatar(
135
-
radius: 22,
136
-
backgroundColor: theme.scaffoldBackgroundColor,
137
-
backgroundImage: (avatarUrl != null && avatarUrl.isNotEmpty)
138
-
? NetworkImage(avatarUrl)
139
-
: null,
140
-
child: (avatarUrl == null || avatarUrl.isEmpty)
141
-
? Icon(Icons.person, size: 44, color: theme.hintColor)
142
-
: null,
143
-
),
144
-
const SizedBox(height: 6),
145
-
Text(
146
-
apiService.currentUser?.displayName ?? '',
147
-
style: theme.textTheme.bodyLarge?.copyWith(
148
-
fontWeight: FontWeight.bold,
149
-
color: theme.colorScheme.onSurface,
150
-
),
151
-
),
152
-
if (apiService.currentUser?.handle != null)
153
-
Text(
154
-
'@${apiService.currentUser!.handle}',
155
-
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
156
-
),
157
-
const SizedBox(height: 6),
158
-
Row(
159
-
mainAxisAlignment: MainAxisAlignment.start,
160
-
children: [
161
-
Text(
162
-
(apiService.currentUser?.followersCount ?? 0).toString(),
163
-
style: theme.textTheme.bodyMedium?.copyWith(
164
-
fontWeight: FontWeight.bold,
165
-
color: theme.colorScheme.onSurface,
166
-
),
167
-
),
168
-
const SizedBox(width: 4),
169
-
Text(
170
-
'Followers',
171
-
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
172
-
),
173
-
const SizedBox(width: 16),
174
-
Text(
175
-
(apiService.currentUser?.followsCount ?? 0).toString(),
176
-
style: theme.textTheme.bodyMedium?.copyWith(
177
-
fontWeight: FontWeight.bold,
178
-
color: theme.colorScheme.onSurface,
179
-
),
180
-
),
181
-
const SizedBox(width: 4),
182
-
Text(
183
-
'Following',
184
-
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
185
-
),
186
-
],
187
-
),
188
-
],
189
-
),
190
-
),
191
-
ListTile(
192
-
leading: const Icon(FontAwesomeIcons.house),
193
-
title: const Text('Home'),
194
-
onTap: () {
195
-
Navigator.pop(context);
196
-
setState(() {
197
-
showProfile = false;
198
-
showNotifications = false;
199
-
showExplore = false;
200
-
});
201
-
},
202
-
),
203
-
ListTile(
204
-
leading: const Icon(FontAwesomeIcons.magnifyingGlass),
205
-
title: const Text('Explore'),
206
-
onTap: () {
207
-
Navigator.pop(context);
208
-
setState(() {
209
-
showExplore = true;
210
-
showProfile = false;
211
-
showNotifications = false;
212
-
});
213
-
},
214
-
),
215
-
ListTile(
216
-
leading: const Icon(FontAwesomeIcons.user),
217
-
title: const Text('Profile'),
218
-
onTap: () {
219
-
Navigator.pop(context);
220
-
setState(() {
221
-
showProfile = true;
222
-
showNotifications = false;
223
-
showExplore = false;
224
-
});
225
-
},
226
-
),
227
-
ListTile(
228
-
leading: const Icon(FontAwesomeIcons.list),
229
-
title: const Text('Logs'),
230
-
onTap: () {
231
-
Navigator.pop(context);
232
-
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const LogPage()));
233
-
},
234
-
),
235
-
const SizedBox(height: 16),
236
-
Padding(
237
-
padding: const EdgeInsets.only(bottom: 16.0),
238
-
child: Center(child: AppVersionText()),
239
-
),
240
-
],
241
-
),
242
-
);
243
-
}
144
+
// ...existing code...
244
145
245
146
@override
246
147
Widget build(BuildContext context) {
···
259
160
onDrawerChanged: (isOpen) {
260
161
setState(() {});
261
162
},
262
-
drawer: _buildAppDrawer(theme, avatarUrl),
263
-
appBar: AppBar(
264
-
backgroundColor: theme.appBarTheme.backgroundColor,
265
-
surfaceTintColor: theme.appBarTheme.backgroundColor,
266
-
elevation: 0.5,
267
-
title: Text(widget.title),
268
-
leading: Builder(
269
-
builder: (context) => IconButton(
270
-
icon: const Icon(Icons.menu),
271
-
onPressed: () => Scaffold.of(context).openDrawer(),
272
-
),
273
-
),
274
-
actions: [
275
-
IconButton(
276
-
icon: const Icon(Icons.logout),
277
-
tooltip: 'Sign Out',
278
-
onPressed: widget.onSignOut,
279
-
),
280
-
],
163
+
drawer: AppDrawer(
164
+
theme: theme,
165
+
avatarUrl: avatarUrl,
166
+
activeIndex: _navIndex,
167
+
onHome: () {
168
+
setState(() {
169
+
showProfile = false;
170
+
showNotifications = false;
171
+
showExplore = false;
172
+
});
173
+
},
174
+
onExplore: () {
175
+
setState(() {
176
+
showProfile = false;
177
+
showNotifications = false;
178
+
showExplore = true;
179
+
});
180
+
},
181
+
onNotifications: () {
182
+
setState(() {
183
+
showProfile = false;
184
+
showNotifications = true;
185
+
showExplore = false;
186
+
});
187
+
},
188
+
onProfile: () {
189
+
setState(() {
190
+
showProfile = true;
191
+
showNotifications = false;
192
+
showExplore = false;
193
+
});
194
+
},
281
195
),
196
+
appBar: _buildAppBar(theme, title: widget.title),
282
197
body: _buildTimelineSliver(context),
283
198
bottomNavigationBar: BottomNavBar(
284
199
navIndex: _navIndex,
···
311
226
});
312
227
},
313
228
),
314
-
floatingActionButton: (!showProfile && !showNotifications && !showExplore)
229
+
floatingActionButton: (!showNotifications && !showExplore)
315
230
? FloatingActionButton(
316
231
shape: const CircleBorder(),
317
232
onPressed: () async {
···
330
245
);
331
246
}
332
247
// Explore, Notifications, Profile: no tabs, no TabController
248
+
String pageTitle = showExplore
249
+
? 'Explore'
250
+
: showNotifications
251
+
? 'Notifications'
252
+
: '';
333
253
return Scaffold(
334
-
drawer: _buildAppDrawer(theme, avatarUrl),
335
-
appBar: (showExplore || showNotifications)
336
-
? AppBar(
337
-
backgroundColor: theme.appBarTheme.backgroundColor,
338
-
surfaceTintColor: theme.appBarTheme.backgroundColor,
339
-
elevation: 0.5,
340
-
title: Text(
341
-
showExplore ? 'Explore' : 'Notifications',
342
-
style: theme.appBarTheme.titleTextStyle,
343
-
),
344
-
leading: Builder(
345
-
builder: (context) => IconButton(
346
-
icon: const Icon(Icons.menu),
347
-
onPressed: () => Scaffold.of(context).openDrawer(),
348
-
),
349
-
),
350
-
actions: [
351
-
IconButton(
352
-
icon: const Icon(Icons.logout),
353
-
tooltip: 'Sign Out',
354
-
onPressed: widget.onSignOut,
355
-
),
356
-
],
357
-
)
358
-
: null,
254
+
drawer: AppDrawer(
255
+
theme: theme,
256
+
avatarUrl: avatarUrl,
257
+
activeIndex: _navIndex,
258
+
onHome: () {
259
+
setState(() {
260
+
showProfile = false;
261
+
showNotifications = false;
262
+
showExplore = false;
263
+
});
264
+
},
265
+
onExplore: () {
266
+
setState(() {
267
+
showProfile = false;
268
+
showNotifications = false;
269
+
showExplore = true;
270
+
});
271
+
},
272
+
onNotifications: () {
273
+
setState(() {
274
+
showProfile = false;
275
+
showNotifications = true;
276
+
showExplore = false;
277
+
});
278
+
},
279
+
onProfile: () {
280
+
setState(() {
281
+
showProfile = true;
282
+
showNotifications = false;
283
+
showExplore = false;
284
+
});
285
+
},
286
+
),
287
+
appBar: (showExplore || showNotifications) ? _buildAppBar(theme, title: pageTitle) : null,
359
288
body: Stack(
360
289
children: [
361
290
if (showExplore)
+42
-2
lib/screens/notifications_page.dart
+42
-2
lib/screens/notifications_page.dart
···
194
194
);
195
195
}
196
196
197
+
Widget _buildSkeletonTile(BuildContext context) {
198
+
final theme = Theme.of(context);
199
+
return ListTile(
200
+
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
201
+
leading: Container(
202
+
width: 40,
203
+
height: 40,
204
+
decoration: BoxDecoration(
205
+
color: theme.colorScheme.surfaceContainerHighest.withAlpha(128),
206
+
shape: BoxShape.circle,
207
+
),
208
+
),
209
+
title: Container(
210
+
width: 120,
211
+
height: 16,
212
+
color: theme.colorScheme.surfaceContainerHighest.withAlpha(128),
213
+
margin: const EdgeInsets.only(bottom: 4),
214
+
),
215
+
subtitle: Column(
216
+
crossAxisAlignment: CrossAxisAlignment.start,
217
+
children: [
218
+
Container(
219
+
width: 180,
220
+
height: 14,
221
+
color: theme.colorScheme.surfaceContainerHighest.withAlpha(128),
222
+
margin: const EdgeInsets.only(bottom: 8),
223
+
),
224
+
Container(
225
+
width: 140,
226
+
height: 12,
227
+
color: theme.colorScheme.surfaceContainerHighest.withAlpha(128),
228
+
),
229
+
],
230
+
),
231
+
isThreeLine: true,
232
+
);
233
+
}
234
+
197
235
@override
198
236
Widget build(BuildContext context) {
199
237
final theme = Theme.of(context);
200
238
return Scaffold(
201
239
backgroundColor: theme.scaffoldBackgroundColor,
202
240
body: _loading
203
-
? Center(
204
-
child: CircularProgressIndicator(strokeWidth: 2, color: theme.colorScheme.primary),
241
+
? ListView.separated(
242
+
itemCount: 6,
243
+
separatorBuilder: (context, index) => Divider(height: 1, color: theme.dividerColor),
244
+
itemBuilder: (context, index) => _buildSkeletonTile(context),
205
245
)
206
246
: _error
207
247
? Center(child: Text('Failed to load notifications.', style: theme.textTheme.bodyMedium))
+190
lib/widgets/app_drawer.dart
+190
lib/widgets/app_drawer.dart
···
1
+
import 'package:flutter/material.dart';
2
+
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
3
+
import 'package:grain/api.dart';
4
+
import 'package:grain/screens/log_page.dart';
5
+
import 'package:grain/widgets/app_version_text.dart';
6
+
7
+
class AppDrawer extends StatelessWidget {
8
+
final ThemeData theme;
9
+
final String? avatarUrl;
10
+
final int activeIndex; // 0: Home, 1: Explore, 2: Notifications, 3: Profile
11
+
final VoidCallback onHome;
12
+
final VoidCallback onExplore;
13
+
final VoidCallback onNotifications;
14
+
final VoidCallback onProfile;
15
+
16
+
const AppDrawer({
17
+
super.key,
18
+
required this.theme,
19
+
required this.avatarUrl,
20
+
required this.activeIndex,
21
+
required this.onHome,
22
+
required this.onExplore,
23
+
required this.onNotifications,
24
+
required this.onProfile,
25
+
});
26
+
27
+
@override
28
+
Widget build(BuildContext context) {
29
+
return Drawer(
30
+
child: ListView(
31
+
padding: EdgeInsets.zero,
32
+
children: [
33
+
Container(
34
+
height: 250,
35
+
decoration: BoxDecoration(
36
+
color: theme.colorScheme.surface,
37
+
border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)),
38
+
),
39
+
padding: const EdgeInsets.fromLTRB(16, 115, 16, 16),
40
+
child: Column(
41
+
crossAxisAlignment: CrossAxisAlignment.start,
42
+
mainAxisAlignment: MainAxisAlignment.center,
43
+
children: [
44
+
CircleAvatar(
45
+
radius: 22,
46
+
backgroundColor: theme.scaffoldBackgroundColor,
47
+
backgroundImage: (avatarUrl?.isNotEmpty ?? false)
48
+
? NetworkImage(avatarUrl!)
49
+
: null,
50
+
child: (avatarUrl?.isEmpty ?? true)
51
+
? Icon(Icons.person, size: 44, color: theme.hintColor)
52
+
: null,
53
+
),
54
+
const SizedBox(height: 6),
55
+
Text(
56
+
apiService.currentUser?.displayName ?? '',
57
+
style: theme.textTheme.bodyLarge?.copyWith(
58
+
fontWeight: FontWeight.bold,
59
+
color: theme.colorScheme.onSurface,
60
+
),
61
+
),
62
+
if (apiService.currentUser?.handle != null)
63
+
Text(
64
+
'@${apiService.currentUser!.handle}',
65
+
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
66
+
),
67
+
const SizedBox(height: 6),
68
+
Row(
69
+
mainAxisAlignment: MainAxisAlignment.start,
70
+
children: [
71
+
Text(
72
+
(apiService.currentUser?.followersCount ?? 0).toString(),
73
+
style: theme.textTheme.bodyMedium?.copyWith(
74
+
fontWeight: FontWeight.bold,
75
+
color: theme.colorScheme.onSurface,
76
+
),
77
+
),
78
+
const SizedBox(width: 4),
79
+
Text(
80
+
'Followers',
81
+
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
82
+
),
83
+
const SizedBox(width: 16),
84
+
Text(
85
+
(apiService.currentUser?.followsCount ?? 0).toString(),
86
+
style: theme.textTheme.bodyMedium?.copyWith(
87
+
fontWeight: FontWeight.bold,
88
+
color: theme.colorScheme.onSurface,
89
+
),
90
+
),
91
+
const SizedBox(width: 4),
92
+
Text(
93
+
'Following',
94
+
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
95
+
),
96
+
],
97
+
),
98
+
],
99
+
),
100
+
),
101
+
ListTile(
102
+
leading: Icon(
103
+
FontAwesomeIcons.house,
104
+
size: 18,
105
+
color: activeIndex == 0 ? theme.colorScheme.primary : theme.iconTheme.color,
106
+
),
107
+
title: Text(
108
+
'Home',
109
+
style: TextStyle(
110
+
color: activeIndex == 0 ? theme.colorScheme.primary : theme.textTheme.bodyLarge?.color,
111
+
fontWeight: activeIndex == 0 ? FontWeight.bold : FontWeight.normal,
112
+
),
113
+
),
114
+
onTap: () {
115
+
Navigator.pop(context);
116
+
onHome();
117
+
},
118
+
),
119
+
ListTile(
120
+
leading: Icon(
121
+
FontAwesomeIcons.magnifyingGlass,
122
+
size: 18,
123
+
color: activeIndex == 1 ? theme.colorScheme.primary : theme.iconTheme.color,
124
+
),
125
+
title: Text(
126
+
'Explore',
127
+
style: TextStyle(
128
+
color: activeIndex == 1 ? theme.colorScheme.primary : theme.textTheme.bodyLarge?.color,
129
+
fontWeight: activeIndex == 1 ? FontWeight.bold : FontWeight.normal,
130
+
),
131
+
),
132
+
onTap: () {
133
+
Navigator.pop(context);
134
+
onExplore();
135
+
},
136
+
),
137
+
ListTile(
138
+
leading: Icon(
139
+
FontAwesomeIcons.solidBell,
140
+
size: 18,
141
+
color: activeIndex == 2 ? theme.colorScheme.primary : theme.iconTheme.color,
142
+
),
143
+
title: Text(
144
+
'Notifications',
145
+
style: TextStyle(
146
+
color: activeIndex == 2 ? theme.colorScheme.primary : theme.textTheme.bodyLarge?.color,
147
+
fontWeight: activeIndex == 2 ? FontWeight.bold : FontWeight.normal,
148
+
),
149
+
),
150
+
onTap: () {
151
+
Navigator.pop(context);
152
+
onNotifications();
153
+
},
154
+
),
155
+
ListTile(
156
+
leading: Icon(
157
+
FontAwesomeIcons.user,
158
+
size: 18,
159
+
color: activeIndex == 3 ? theme.colorScheme.primary : theme.iconTheme.color,
160
+
),
161
+
title: Text(
162
+
'Profile',
163
+
style: TextStyle(
164
+
color: activeIndex == 3 ? theme.colorScheme.primary : theme.textTheme.bodyLarge?.color,
165
+
fontWeight: activeIndex == 3 ? FontWeight.bold : FontWeight.normal,
166
+
),
167
+
),
168
+
onTap: () {
169
+
Navigator.pop(context);
170
+
onProfile();
171
+
},
172
+
),
173
+
ListTile(
174
+
leading: const Icon(FontAwesomeIcons.list, size: 18),
175
+
title: const Text('Logs'),
176
+
onTap: () {
177
+
Navigator.pop(context);
178
+
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const LogPage()));
179
+
},
180
+
),
181
+
const SizedBox(height: 16),
182
+
Padding(
183
+
padding: const EdgeInsets.only(bottom: 16.0),
184
+
child: Center(child: AppVersionText()),
185
+
),
186
+
],
187
+
),
188
+
);
189
+
}
190
+
}