feat: Refactor gallery-related UI components for consistency and improve drawer navigation

+1 -1
lib/screens/gallery_action_sheet.dart
··· 40 ), 41 ListTile( 42 leading: const Icon(Icons.sort), 43 - title: const Text('Change sort order'), 44 onTap: () { 45 Navigator.of(context).pop(); 46 if (onChangeSortOrder != null) onChangeSortOrder!();
··· 40 ), 41 ListTile( 42 leading: const Icon(Icons.sort), 43 + title: const Text('Edit sort order'), 44 onTap: () { 45 Navigator.of(context).pop(); 46 if (onChangeSortOrder != null) onChangeSortOrder!();
+7 -4
lib/screens/gallery_edit_photos_sheet.dart
··· 47 backgroundColor: theme.colorScheme.surface, 48 border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)), 49 middle: Text( 50 - 'Edit Gallery Photos', 51 style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), 52 ), 53 - leading: CupertinoButton( 54 padding: EdgeInsets.zero, 55 onPressed: (_loading || _deletingPhotoIndex != null) 56 ? null 57 - : () => Navigator.of(context).maybePop(), 58 child: Text( 59 - 'Cancel', 60 style: TextStyle( 61 color: _deletingPhotoIndex != null ? theme.disabledColor : theme.colorScheme.primary, 62 fontWeight: FontWeight.w600,
··· 47 backgroundColor: theme.colorScheme.surface, 48 border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)), 49 middle: Text( 50 + 'Edit photos', 51 style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), 52 ), 53 + trailing: CupertinoButton( 54 padding: EdgeInsets.zero, 55 onPressed: (_loading || _deletingPhotoIndex != null) 56 ? null 57 + : () { 58 + widget.onSave(_photos); 59 + Navigator.of(context).maybePop(); 60 + }, 61 child: Text( 62 + 'Done', 63 style: TextStyle( 64 color: _deletingPhotoIndex != null ? theme.disabledColor : theme.colorScheme.primary, 65 fontWeight: FontWeight.w600,
+1 -1
lib/screens/gallery_sort_order_sheet.dart
··· 34 backgroundColor: theme.colorScheme.surface, 35 border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)), 36 middle: Text( 37 - 'Change Sort Order', 38 style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), 39 ), 40 leading: CupertinoButton(
··· 34 backgroundColor: theme.colorScheme.surface, 35 border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)), 36 middle: Text( 37 + 'Edit sort order', 38 style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), 39 ), 40 leading: CupertinoButton(
+102 -173
lib/screens/home_page.dart
··· 4 import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 5 import 'package:grain/api.dart'; 6 import 'package:grain/screens/create_gallery_page.dart'; 7 - import 'package:grain/widgets/app_version_text.dart'; 8 import 'package:grain/widgets/bottom_nav_bar.dart'; 9 import 'package:grain/widgets/timeline_item.dart'; 10 11 import '../providers/gallery_cache_provider.dart'; 12 import 'explore_page.dart'; 13 - import 'log_page.dart'; 14 import 'notifications_page.dart'; 15 import 'profile_page.dart'; 16 ··· 26 } 27 28 class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin { 29 bool showProfile = false; 30 bool showNotifications = false; 31 bool showExplore = false; ··· 115 ); 116 } 117 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 - } 244 245 @override 246 Widget build(BuildContext context) { ··· 259 onDrawerChanged: (isOpen) { 260 setState(() {}); 261 }, 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 - ], 281 ), 282 body: _buildTimelineSliver(context), 283 bottomNavigationBar: BottomNavBar( 284 navIndex: _navIndex, ··· 311 }); 312 }, 313 ), 314 - floatingActionButton: (!showProfile && !showNotifications && !showExplore) 315 ? FloatingActionButton( 316 shape: const CircleBorder(), 317 onPressed: () async { ··· 330 ); 331 } 332 // Explore, Notifications, Profile: no tabs, no TabController 333 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, 359 body: Stack( 360 children: [ 361 if (showExplore)
··· 4 import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 5 import 'package:grain/api.dart'; 6 import 'package:grain/screens/create_gallery_page.dart'; 7 + import 'package:grain/widgets/app_drawer.dart'; 8 import 'package:grain/widgets/bottom_nav_bar.dart'; 9 import 'package:grain/widgets/timeline_item.dart'; 10 11 import '../providers/gallery_cache_provider.dart'; 12 import 'explore_page.dart'; 13 + // ...existing code... 14 import 'notifications_page.dart'; 15 import 'profile_page.dart'; 16 ··· 26 } 27 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 + 55 bool showProfile = false; 56 bool showNotifications = false; 57 bool showExplore = false; ··· 141 ); 142 } 143 144 + // ...existing code... 145 146 @override 147 Widget build(BuildContext context) { ··· 160 onDrawerChanged: (isOpen) { 161 setState(() {}); 162 }, 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 + }, 195 ), 196 + appBar: _buildAppBar(theme, title: widget.title), 197 body: _buildTimelineSliver(context), 198 bottomNavigationBar: BottomNavBar( 199 navIndex: _navIndex, ··· 226 }); 227 }, 228 ), 229 + floatingActionButton: (!showNotifications && !showExplore) 230 ? FloatingActionButton( 231 shape: const CircleBorder(), 232 onPressed: () async { ··· 245 ); 246 } 247 // Explore, Notifications, Profile: no tabs, no TabController 248 + String pageTitle = showExplore 249 + ? 'Explore' 250 + : showNotifications 251 + ? 'Notifications' 252 + : ''; 253 return Scaffold( 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, 288 body: Stack( 289 children: [ 290 if (showExplore)
+42 -2
lib/screens/notifications_page.dart
··· 194 ); 195 } 196 197 @override 198 Widget build(BuildContext context) { 199 final theme = Theme.of(context); 200 return Scaffold( 201 backgroundColor: theme.scaffoldBackgroundColor, 202 body: _loading 203 - ? Center( 204 - child: CircularProgressIndicator(strokeWidth: 2, color: theme.colorScheme.primary), 205 ) 206 : _error 207 ? Center(child: Text('Failed to load notifications.', style: theme.textTheme.bodyMedium))
··· 194 ); 195 } 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 + 235 @override 236 Widget build(BuildContext context) { 237 final theme = Theme.of(context); 238 return Scaffold( 239 backgroundColor: theme.scaffoldBackgroundColor, 240 body: _loading 241 + ? ListView.separated( 242 + itemCount: 6, 243 + separatorBuilder: (context, index) => Divider(height: 1, color: theme.dividerColor), 244 + itemBuilder: (context, index) => _buildSkeletonTile(context), 245 ) 246 : _error 247 ? Center(child: Text('Failed to load notifications.', style: theme.textTheme.bodyMedium))
+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 + }