Grain flutter app

refactor: refactor UI components to use theme colors and styles

- Updated CircularProgressIndicator colors to use theme's primary color.
- Replaced hardcoded colors in DrawerHeader and other widgets with theme colors.
- Enhanced text styles in various widgets to align with the app's theme.
- Refactored AppButton to utilize theme colors for primary and secondary variants.
- Improved text field styling with animated focus effects based on theme.
- Adjusted background colors and text colors in multiple screens to ensure consistency with the theme.
- Ensured all icons and text elements reflect the current theme's color scheme.

+637 -507
+4 -1
lib/api.dart
··· 129 129 appLogger.i('Fetching gallery for uri: $uri'); 130 130 final response = await http.get( 131 131 Uri.parse('$_apiUrl/xrpc/social.grain.gallery.getGallery?uri=$uri'), 132 - headers: {'Content-Type': 'application/json'}, 132 + headers: { 133 + 'Authorization': "Bearer $_accessToken", 134 + 'Content-Type': 'application/json', 135 + }, 133 136 ); 134 137 if (response.statusCode != 200) { 135 138 appLogger.w(
+73
lib/app_theme.dart
··· 1 + import 'package:flutter/material.dart'; 2 + 3 + class AppTheme { 4 + static const Color primaryColor = Color(0xFF0EA5E9); 5 + static const Color scaffoldBackgroundColor = Color(0xFFF8FAFC); 6 + static const Color favoriteColor = Color(0xFFEC4899); 7 + 8 + static final ThemeData lightTheme = ThemeData( 9 + brightness: Brightness.light, 10 + primaryColor: primaryColor, 11 + scaffoldBackgroundColor: scaffoldBackgroundColor, 12 + appBarTheme: const AppBarTheme( 13 + backgroundColor: scaffoldBackgroundColor, 14 + foregroundColor: Colors.black87, 15 + elevation: 0, 16 + iconTheme: IconThemeData(color: Colors.black87), 17 + titleTextStyle: TextStyle( 18 + color: Colors.black87, 19 + fontSize: 18, 20 + fontWeight: FontWeight.w600, 21 + ), 22 + ), 23 + floatingActionButtonTheme: const FloatingActionButtonThemeData( 24 + backgroundColor: primaryColor, 25 + foregroundColor: Colors.white, 26 + shape: CircleBorder(), 27 + ), 28 + dividerColor: Colors.grey[300], 29 + colorScheme: ColorScheme.light( 30 + primary: primaryColor, 31 + surface: Colors.white, 32 + onSurface: Colors.black87, 33 + onSurfaceVariant: Colors.black54, 34 + onPrimary: Colors.white, 35 + onSecondary: Colors.black87, 36 + onError: Colors.redAccent, 37 + ), 38 + // Add more theme customizations as needed 39 + ); 40 + 41 + static final ThemeData darkTheme = ThemeData( 42 + brightness: Brightness.dark, 43 + primaryColor: primaryColor, 44 + scaffoldBackgroundColor: Colors.black, 45 + appBarTheme: const AppBarTheme( 46 + backgroundColor: Colors.black, 47 + foregroundColor: Colors.white, 48 + elevation: 0, 49 + iconTheme: IconThemeData(color: Colors.white), 50 + titleTextStyle: TextStyle( 51 + color: Colors.white, 52 + fontSize: 18, 53 + fontWeight: FontWeight.w600, 54 + ), 55 + ), 56 + floatingActionButtonTheme: const FloatingActionButtonThemeData( 57 + backgroundColor: primaryColor, 58 + foregroundColor: Colors.white, 59 + shape: CircleBorder(), 60 + ), 61 + dividerColor: Colors.grey[900], 62 + colorScheme: ColorScheme.dark( 63 + primary: primaryColor, 64 + surface: Colors.black, 65 + onSurface: Colors.white, 66 + onSurfaceVariant: Colors.white70, 67 + onPrimary: Colors.white, 68 + onSecondary: Colors.white, 69 + onError: Colors.redAccent, 70 + ), 71 + // Add more theme customizations as needed 72 + ); 73 + }
+4 -6
lib/main.dart
··· 6 6 import 'package:grain/app_logger.dart'; 7 7 import 'package:grain/screens/splash_page.dart'; 8 8 import 'package:grain/screens/home_page.dart'; 9 + import 'package:grain/app_theme.dart'; 9 10 10 11 class AppConfig { 11 12 static late final String apiUrl; ··· 59 60 Widget build(BuildContext context) { 60 61 return MaterialApp( 61 62 title: 'Grain', 62 - theme: ThemeData( 63 - colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlue), 64 - scaffoldBackgroundColor: Colors.white, 65 - dividerColor: const Color(0xFFF4F4F5), 66 - // textTheme: GoogleFonts.interTextTheme(), 67 - ), 63 + theme: AppTheme.lightTheme, 64 + darkTheme: AppTheme.darkTheme, 65 + themeMode: ThemeMode.system, 68 66 home: isSignedIn 69 67 ? MyHomePage(title: 'Grain', onSignOut: handleSignOut) 70 68 : SplashPage(onSignIn: handleSignIn),
+38 -30
lib/screens/comments_page.dart
··· 51 51 52 52 @override 53 53 Widget build(BuildContext context) { 54 + final theme = Theme.of(context); 54 55 return Stack( 55 56 children: [ 56 57 Scaffold( 58 + backgroundColor: theme.scaffoldBackgroundColor, 57 59 appBar: AppBar( 58 - backgroundColor: Colors.white, 59 - surfaceTintColor: Colors.white, 60 + backgroundColor: theme.appBarTheme.backgroundColor, 61 + surfaceTintColor: theme.appBarTheme.backgroundColor, 60 62 bottom: PreferredSize( 61 63 preferredSize: const Size.fromHeight(1), 62 - child: Container( 63 - color: Theme.of(context).dividerColor, 64 - height: 1, 65 - ), 64 + child: Container(color: theme.dividerColor, height: 1), 66 65 ), 67 - title: const Text( 68 - 'Comments', 69 - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), 70 - ), 66 + title: Text('Comments', style: theme.appBarTheme.titleTextStyle), 71 67 ), 72 68 body: _loading 73 - ? const Center( 69 + ? Center( 74 70 child: CircularProgressIndicator( 75 71 strokeWidth: 2, 76 - color: Color(0xFF0EA5E9), 72 + color: theme.colorScheme.primary, 77 73 ), 78 - ) // Tailwind sky-500) 74 + ) 79 75 : _error 80 - ? const Center(child: Text('Failed to load comments.')) 76 + ? Center( 77 + child: Text( 78 + 'Failed to load comments.', 79 + style: theme.textTheme.bodyMedium, 80 + ), 81 + ) 81 82 : ListView( 82 83 padding: const EdgeInsets.all(12), 83 84 children: [ 84 85 if (_gallery != null) 85 - Text( 86 - _gallery!.title, 87 - style: const TextStyle( 88 - fontWeight: FontWeight.bold, 89 - fontSize: 18, 90 - ), 91 - ), 86 + Text(_gallery!.title, style: theme.textTheme.titleMedium), 92 87 const SizedBox(height: 12), 93 88 _CommentsList( 94 89 comments: _comments, ··· 155 150 156 151 @override 157 152 Widget build(BuildContext context) { 153 + final theme = Theme.of(context); 158 154 final repliesByParent = _groupReplies(comments); 159 155 final topLevel = _topLevel(comments); 160 156 if (comments.isEmpty) { 161 - return const Padding( 162 - padding: EdgeInsets.symmetric(vertical: 32), 157 + return Padding( 158 + padding: const EdgeInsets.symmetric(vertical: 32), 163 159 child: Center( 164 - child: Text('No comments yet', style: TextStyle(color: Colors.grey)), 160 + child: Text( 161 + 'No comments yet', 162 + style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor), 163 + ), 165 164 ), 166 165 ); 167 166 } ··· 182 181 183 182 @override 184 183 Widget build(BuildContext context) { 184 + final theme = Theme.of(context); 185 185 final author = comment.author; 186 186 return Padding( 187 187 padding: const EdgeInsets.symmetric(vertical: 8), ··· 198 198 ), 199 199 ) 200 200 else 201 - const CircleAvatar(radius: 16, child: Icon(Icons.person, size: 16)), 201 + CircleAvatar( 202 + radius: 16, 203 + backgroundColor: theme.colorScheme.surfaceContainerHighest, 204 + child: Icon(Icons.person, size: 16, color: theme.iconTheme.color), 205 + ), 202 206 const SizedBox(width: 8), 203 207 Expanded( 204 208 child: Column( ··· 206 210 children: [ 207 211 Text( 208 212 author['displayName'] ?? '@${author['handle'] ?? ''}', 209 - style: const TextStyle(fontWeight: FontWeight.bold), 213 + style: theme.textTheme.bodyLarge?.copyWith( 214 + fontWeight: FontWeight.bold, 215 + ), 210 216 ), 211 - Text(comment.text, style: const TextStyle(fontSize: 15)), 217 + Text(comment.text, style: theme.textTheme.bodyMedium), 212 218 if (comment.focus != null) ...[ 213 219 const SizedBox(height: 8), 214 220 Align( 215 221 alignment: Alignment.centerLeft, 216 222 child: ConstrainedBox( 217 223 constraints: const BoxConstraints( 218 - maxWidth: 180, // Limit max width 219 - maxHeight: 180, // Limit max height 224 + maxWidth: 180, 225 + maxHeight: 180, 220 226 ), 221 227 child: AspectRatio( 222 228 aspectRatio: ··· 255 261 if (comment.createdAt != null) 256 262 Text( 257 263 formatRelativeTime(comment.createdAt!), 258 - style: const TextStyle(fontSize: 11, color: Colors.grey), 264 + style: theme.textTheme.bodySmall?.copyWith( 265 + color: theme.hintColor, 266 + ), 259 267 ), 260 268 ], 261 269 ),
+33 -46
lib/screens/explore_page.dart
··· 3 3 import 'package:grain/api.dart'; 4 4 import 'package:grain/models/profile.dart'; 5 5 import 'package:grain/widgets/app_image.dart'; 6 + import 'package:grain/widgets/plain_text_field.dart'; 6 7 import 'profile_page.dart'; 7 8 8 9 class ExplorePage extends StatefulWidget { ··· 67 68 68 69 @override 69 70 Widget build(BuildContext context) { 71 + final theme = Theme.of(context); 70 72 return Column( 71 73 children: [ 72 74 Padding( 73 - padding: const EdgeInsets.all(16.0), 74 - child: TextField( 75 + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), 76 + child: PlainTextField( 77 + label: '', 75 78 controller: _controller, 76 - decoration: InputDecoration( 77 - hintText: 'Search for users', 78 - filled: true, 79 - fillColor: Theme.of(context).colorScheme.surfaceContainerHighest, 80 - border: OutlineInputBorder( 81 - borderRadius: BorderRadius.circular(8), 82 - borderSide: BorderSide.none, 83 - ), 84 - focusedBorder: OutlineInputBorder( 85 - borderRadius: BorderRadius.circular(8), 86 - borderSide: const BorderSide( 87 - color: Color(0xFF0ea5e9), // Tailwind sky-500 88 - width: 2, 89 - ), 90 - ), 91 - suffixIcon: _controller.text.isNotEmpty 92 - ? IconButton( 93 - icon: const Icon(Icons.clear), 94 - onPressed: () { 95 - _controller.clear(); 96 - setState(() { 97 - _results = []; 98 - _searched = false; 99 - }); 100 - }, 101 - ) 102 - : null, 103 - ), 79 + hintText: 'Search for users', 104 80 onChanged: _onSearchChanged, 81 + enabled: true, 105 82 ), 106 83 ), 107 84 if (_controller.text.isNotEmpty) ··· 111 88 alignment: Alignment.centerLeft, 112 89 child: Text( 113 90 'Search for "${_controller.text}"', 114 - style: TextStyle( 115 - color: Colors.black54, 91 + style: theme.textTheme.bodyMedium?.copyWith( 92 + color: theme.hintColor, 116 93 fontWeight: FontWeight.w500, 117 94 fontSize: 16, 118 95 ), 119 96 ), 120 97 ), 121 98 ), 122 - Expanded(child: _buildResultsList(_results)), 99 + Expanded(child: _buildResultsList(_results, theme)), 123 100 ], 124 101 ); 125 102 } 126 103 127 - Widget _buildResultsList(List<Profile> results) { 104 + Widget _buildResultsList(List<Profile> results, ThemeData theme) { 128 105 if (_loading) { 129 - return const ListTile( 130 - title: Text('Searching...'), 106 + return ListTile( 107 + title: Text('Searching...', style: theme.textTheme.bodyMedium), 131 108 leading: SizedBox( 132 109 width: 20, 133 110 height: 20, 134 111 child: CircularProgressIndicator( 135 112 strokeWidth: 2, 136 - color: Color(0xFF0EA5E9), 113 + color: theme.colorScheme.primary, 137 114 ), 138 115 ), 139 116 ); 140 117 } else if (_searched && results.isEmpty) { 141 - return const ListTile(title: Text('No users found')); 118 + return ListTile( 119 + title: Text('No users found', style: theme.textTheme.bodyMedium), 120 + ); 142 121 } 143 122 return ListView.separated( 144 123 itemCount: results.length, 145 124 separatorBuilder: (context, index) => 146 - const Divider(height: 1, thickness: 1), 125 + Divider(height: 1, thickness: 1, color: theme.dividerColor), 147 126 itemBuilder: (context, index) { 148 127 final profile = results[index]; 149 128 return ListTile( ··· 156 135 fit: BoxFit.cover, 157 136 ), 158 137 ) 159 - : const CircleAvatar( 138 + : CircleAvatar( 160 139 radius: 16, 161 - child: Icon(Icons.account_circle, color: Colors.grey), 140 + backgroundColor: theme.colorScheme.surfaceContainerHighest, 141 + child: Icon( 142 + Icons.account_circle, 143 + color: theme.iconTheme.color, 144 + ), 162 145 ), 163 146 title: Text( 164 147 profile.displayName.isNotEmpty 165 148 ? profile.displayName 166 149 : '@${profile.handle}', 150 + style: theme.textTheme.bodyLarge, 167 151 ), 168 152 subtitle: profile.handle.isNotEmpty 169 - ? Text('@${profile.handle}') 153 + ? Text( 154 + '@${profile.handle}', 155 + style: theme.textTheme.bodyMedium?.copyWith( 156 + color: theme.hintColor, 157 + ), 158 + ) 170 159 : null, 171 160 onTap: () async { 172 - FocusScope.of( 173 - context, 174 - ).unfocus(); // Dismiss keyboard and prevent onChanged 175 - _debounce?.cancel(); // Cancel any pending search 161 + FocusScope.of(context).unfocus(); 162 + _debounce?.cancel(); 176 163 setState(() { 177 164 _searched = false; 178 165 _loading = false;
+51 -62
lib/screens/gallery_page.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:grain/app_theme.dart'; 2 3 import 'package:grain/models/gallery.dart'; 3 4 import 'package:grain/api.dart'; 4 5 import 'package:grain/widgets/justified_gallery_view.dart'; ··· 67 68 68 69 @override 69 70 Widget build(BuildContext context) { 71 + final theme = Theme.of(context); 70 72 if (_loading) { 71 - return const Scaffold( 72 - body: Center( 73 + return Scaffold( 74 + backgroundColor: theme.scaffoldBackgroundColor, 75 + body: const Center( 73 76 child: CircularProgressIndicator( 74 77 strokeWidth: 2, 75 78 color: Color(0xFF0EA5E9), ··· 78 81 ); 79 82 } 80 83 if (_error || _gallery == null) { 81 - return const Scaffold( 82 - body: Center(child: Text('Failed to load gallery.')), 84 + return Scaffold( 85 + backgroundColor: theme.scaffoldBackgroundColor, 86 + body: const Center(child: Text('Failed to load gallery.')), 83 87 ); 84 88 } 85 89 final gallery = _gallery!; ··· 87 91 final galleryItems = gallery.items 88 92 .where((item) => item.thumb.isNotEmpty) 89 93 .toList(); 94 + final isFav = gallery.viewer != null && gallery.viewer!['fav'] != null; 90 95 91 - // The Stack is now OUTSIDE the Scaffold! 92 96 return Stack( 93 97 children: [ 94 98 Scaffold( 99 + backgroundColor: theme.scaffoldBackgroundColor, 95 100 appBar: AppBar( 96 - backgroundColor: Colors.white, 97 - surfaceTintColor: Colors.white, 101 + backgroundColor: theme.appBarTheme.backgroundColor, 102 + surfaceTintColor: theme.appBarTheme.backgroundColor, 98 103 bottom: PreferredSize( 99 104 preferredSize: const Size.fromHeight(1), 100 - child: Container( 101 - color: Theme.of(context).dividerColor, 102 - height: 1, 103 - ), 105 + child: Container(color: theme.dividerColor, height: 1), 104 106 ), 105 - title: const Text( 107 + title: Text( 106 108 'Gallery', 107 - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), 108 - ), 109 - iconTheme: const IconThemeData(color: Colors.black87), 110 - titleTextStyle: const TextStyle( 111 - color: Colors.black87, 112 - fontSize: 18, 113 - fontWeight: FontWeight.w600, 109 + style: theme.textTheme.titleMedium?.copyWith( 110 + fontWeight: FontWeight.w600, 111 + ), 114 112 ), 113 + iconTheme: theme.appBarTheme.iconTheme, 114 + titleTextStyle: theme.appBarTheme.titleTextStyle, 115 115 actions: [ 116 116 if (gallery.creator?.did == widget.currentUserDid) 117 117 IconButton( ··· 121 121 await showModalBottomSheet( 122 122 context: context, 123 123 isScrollControlled: true, 124 - builder: (context) => CreateGalleryPage( 125 - // Optionally pass initial data for editing 126 - gallery: gallery, 127 - ), 124 + builder: (context) => CreateGalleryPage(gallery: gallery), 128 125 ); 129 - // Optionally refresh after editing 130 126 _fetchGallery(); 131 127 }, 132 128 ), ··· 142 138 const SizedBox(height: 10), 143 139 Text( 144 140 gallery.title.isNotEmpty ? gallery.title : 'Gallery', 145 - style: const TextStyle( 146 - fontSize: 24, 141 + style: theme.textTheme.headlineSmall?.copyWith( 147 142 fontWeight: FontWeight.w600, 148 143 ), 149 144 ), ··· 153 148 children: [ 154 149 CircleAvatar( 155 150 radius: 18, 151 + backgroundColor: 152 + theme.colorScheme.surfaceContainerHighest, 156 153 backgroundImage: 157 154 gallery.creator?.avatar != null && 158 155 gallery.creator!.avatar.isNotEmpty ··· 161 158 child: 162 159 (gallery.creator == null || 163 160 gallery.creator!.avatar.isEmpty) 164 - ? const Icon( 161 + ? Icon( 165 162 Icons.account_circle, 166 163 size: 24, 167 - color: Colors.grey, 164 + color: theme.colorScheme.onSurface 165 + .withOpacity(0.4), 168 166 ) 169 167 : ClipOval( 170 168 child: AppImage( ··· 182 180 children: [ 183 181 Text( 184 182 gallery.creator?.displayName ?? '', 185 - style: const TextStyle( 183 + style: theme.textTheme.bodyLarge?.copyWith( 186 184 fontWeight: FontWeight.w600, 187 - fontSize: 16, 188 185 ), 189 186 ), 190 187 if ((gallery.creator?.displayName ?? '') ··· 193 190 const SizedBox(width: 8), 194 191 Text( 195 192 '@${gallery.creator?.handle ?? ''}', 196 - style: const TextStyle( 197 - color: Colors.grey, 198 - fontSize: 15, 193 + style: theme.textTheme.bodyMedium?.copyWith( 194 + color: theme.hintColor, 199 195 ), 200 196 ), 201 197 ], ··· 214 210 ).copyWith(bottom: 8), 215 211 child: Text( 216 212 gallery.description, 217 - style: const TextStyle(fontSize: 15, color: Colors.black87), 213 + style: theme.textTheme.bodyMedium?.copyWith( 214 + color: theme.colorScheme.onSurface, 215 + ), 218 216 ), 219 217 ), 220 218 if (isLoggedIn) ··· 230 228 child: Container( 231 229 padding: const EdgeInsets.symmetric(vertical: 8), 232 230 decoration: BoxDecoration( 233 - color: Theme.of( 234 - context, 235 - ).colorScheme.surfaceContainerHighest, 231 + color: theme.colorScheme.surfaceContainerHighest, 236 232 borderRadius: BorderRadius.circular(12), 237 233 ), 238 234 child: Row( 239 235 mainAxisAlignment: MainAxisAlignment.center, 240 236 children: [ 241 237 FaIcon( 242 - gallery.viewer != null && 243 - gallery.viewer!['fav'] != null 238 + isFav 244 239 ? FontAwesomeIcons.solidHeart 245 240 : FontAwesomeIcons.heart, 246 - color: 247 - gallery.viewer != null && 248 - gallery.viewer!['fav'] != null 249 - ? const Color(0xFFEC4899) 250 - : Colors.black54, 241 + color: isFav 242 + ? AppTheme.favoriteColor 243 + : theme.iconTheme.color, 251 244 size: 20, 252 245 ), 253 246 if (gallery.favCount != null) ...[ 254 247 const SizedBox(width: 6), 255 248 Text( 256 249 gallery.favCount.toString(), 257 - style: const TextStyle( 258 - fontSize: 14, 259 - color: Colors.black87, 250 + style: theme.textTheme.bodyMedium?.copyWith( 260 251 fontWeight: FontWeight.w500, 261 252 ), 262 253 ), ··· 282 273 child: Container( 283 274 padding: const EdgeInsets.symmetric(vertical: 8), 284 275 decoration: BoxDecoration( 285 - color: Theme.of( 286 - context, 287 - ).colorScheme.surfaceContainerHighest, 276 + color: theme.colorScheme.surfaceContainerHighest, 288 277 borderRadius: BorderRadius.circular(12), 289 278 ), 290 279 child: Row( 291 280 mainAxisAlignment: MainAxisAlignment.center, 292 281 children: [ 293 - const FaIcon( 282 + FaIcon( 294 283 FontAwesomeIcons.comment, 295 - color: Colors.black54, 284 + color: theme.iconTheme.color, 296 285 size: 20, 297 286 ), 298 287 if (gallery.commentCount != null) ...[ 299 288 const SizedBox(width: 6), 300 289 Text( 301 290 gallery.commentCount.toString(), 302 - style: const TextStyle( 303 - fontSize: 14, 304 - color: Colors.black87, 291 + style: theme.textTheme.bodyMedium?.copyWith( 305 292 fontWeight: FontWeight.w500, 306 293 ), 307 294 ), ··· 317 304 child: InkWell( 318 305 borderRadius: BorderRadius.circular(12), 319 306 onTap: () { 320 - // Parse the gallery URI to get handle and rkey 321 307 final atUri = AtUri.parse(gallery.uri); 322 308 final handle = gallery.creator?.handle ?? ''; 323 309 final galleryRkey = atUri.rkey; ··· 332 318 child: Container( 333 319 padding: const EdgeInsets.symmetric(vertical: 8), 334 320 decoration: BoxDecoration( 335 - color: Theme.of( 336 - context, 337 - ).colorScheme.surfaceContainerHighest, 321 + color: theme.colorScheme.surfaceContainerHighest, 338 322 borderRadius: BorderRadius.circular(12), 339 323 ), 340 - child: const Center( 324 + child: Center( 341 325 child: FaIcon( 342 326 FontAwesomeIcons.arrowUpFromBracket, 343 - color: Colors.black54, 327 + color: theme.iconTheme.color, 344 328 size: 20, 345 329 ), 346 330 ), ··· 365 349 }, 366 350 ), 367 351 if (galleryItems.isEmpty) 368 - const Center(child: Text('No photos in this gallery.')), 352 + Center( 353 + child: Text( 354 + 'No photos in this gallery.', 355 + style: theme.textTheme.bodyMedium, 356 + ), 357 + ), 369 358 ], 370 359 ), 371 360 ),
+95 -99
lib/screens/home_page.dart
··· 148 148 child: Center( 149 149 child: CircularProgressIndicator( 150 150 strokeWidth: 2, 151 - color: Color(0xFF0ea5e9), 151 + color: Theme.of(context).colorScheme.primary, 152 152 ), 153 153 ), 154 154 ), ··· 176 176 177 177 @override 178 178 Widget build(BuildContext context) { 179 + final theme = Theme.of(context); 179 180 if (apiService.currentUser == null) { 180 - return const Scaffold( 181 + return Scaffold( 181 182 body: Center( 182 183 child: CircularProgressIndicator( 183 184 strokeWidth: 2, 184 - color: Color(0xFF0EA5E9), 185 + color: theme.colorScheme.primary, 185 186 ), 186 187 ), 187 188 ); ··· 194 195 child: ListView( 195 196 padding: EdgeInsets.zero, 196 197 children: [ 197 - DrawerHeader( 198 - decoration: BoxDecoration(color: Colors.white), 198 + Container( 199 + height: 250, 200 + decoration: BoxDecoration( 201 + color: theme.scaffoldBackgroundColor, 202 + border: Border( 203 + bottom: BorderSide(color: theme.dividerColor, width: 1), 204 + ), 205 + ), 206 + padding: const EdgeInsets.fromLTRB(16, 115, 16, 16), 199 207 child: Column( 200 208 crossAxisAlignment: CrossAxisAlignment.start, 201 209 mainAxisAlignment: MainAxisAlignment.center, 202 210 children: [ 203 211 CircleAvatar( 204 - radius: 22, // Smaller avatar 205 - backgroundImage: 206 - apiService.currentUser?.avatar != null && 207 - apiService.currentUser!.avatar.isNotEmpty 208 - ? null 209 - : null, 210 - backgroundColor: Colors.white, 212 + radius: 22, 213 + backgroundColor: theme.scaffoldBackgroundColor, 211 214 child: ClipOval( 212 215 child: AppImage( 213 216 url: apiService.currentUser!.avatar, ··· 220 223 const SizedBox(height: 6), 221 224 Text( 222 225 apiService.currentUser?.displayName ?? '', 223 - style: const TextStyle( 224 - color: Colors.black, 225 - fontSize: 15, // Smaller text 226 + style: theme.textTheme.bodyLarge?.copyWith( 226 227 fontWeight: FontWeight.bold, 228 + color: theme.colorScheme.onSurface, 227 229 ), 228 230 ), 229 231 if (apiService.currentUser?.handle != null) 230 232 Text( 231 233 '@${apiService.currentUser!.handle}', 232 - style: const TextStyle( 233 - color: Colors.black54, 234 - fontSize: 11, // Smaller text 234 + style: theme.textTheme.bodySmall?.copyWith( 235 + color: theme.hintColor, 235 236 ), 236 237 ), 237 238 const SizedBox(height: 6), ··· 241 242 Text( 242 243 (apiService.currentUser?.followersCount ?? 0) 243 244 .toString(), 244 - style: const TextStyle( 245 - color: Colors.black, 245 + style: theme.textTheme.bodyMedium?.copyWith( 246 246 fontWeight: FontWeight.bold, 247 - fontSize: 13, 247 + color: theme.colorScheme.onSurface, 248 248 ), 249 249 ), 250 250 const SizedBox(width: 4), 251 - const Text( 251 + Text( 252 252 'Followers', 253 - style: TextStyle(color: Colors.black54, fontSize: 10), 253 + style: theme.textTheme.bodySmall?.copyWith( 254 + color: theme.hintColor, 255 + ), 254 256 ), 255 257 const SizedBox(width: 16), 256 258 Text( 257 259 (apiService.currentUser?.followsCount ?? 0) 258 260 .toString(), 259 - style: const TextStyle( 260 - color: Colors.black, 261 + style: theme.textTheme.bodyMedium?.copyWith( 261 262 fontWeight: FontWeight.bold, 262 - fontSize: 13, 263 + color: theme.colorScheme.onSurface, 263 264 ), 264 265 ), 265 266 const SizedBox(width: 4), 266 - const Text( 267 + Text( 267 268 'Following', 268 - style: TextStyle(color: Colors.black54, fontSize: 10), 269 + style: theme.textTheme.bodySmall?.copyWith( 270 + color: theme.hintColor, 271 + ), 269 272 ), 270 273 ], 271 274 ), ··· 327 330 ), 328 331 ), 329 332 body: NestedScrollView( 330 - floatHeaderSlivers: 331 - true, // Ensures SliverAppBar snaps instantly on scroll up 333 + floatHeaderSlivers: true, 332 334 headerSliverBuilder: (context, innerBoxIsScrolled) => [ 333 335 SliverOverlapAbsorber( 334 336 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), 335 337 sliver: SliverAppBar( 336 - backgroundColor: Colors.white, 337 - surfaceTintColor: Colors.white, 338 - floating: true, 339 - snap: true, 338 + backgroundColor: theme.appBarTheme.backgroundColor, 339 + surfaceTintColor: theme.appBarTheme.backgroundColor, 340 + floating: false, 341 + snap: false, 340 342 pinned: true, 341 343 elevation: 0.5, 342 344 title: Text( 343 345 widget.title, 344 - style: const TextStyle( 345 - fontSize: 18, 346 - fontWeight: FontWeight.w600, 347 - ), 346 + style: theme.appBarTheme.titleTextStyle, 348 347 ), 349 348 leading: Builder( 350 349 builder: (context) => IconButton( ··· 360 359 ), 361 360 ], 362 361 bottom: PreferredSize( 363 - preferredSize: const Size.fromHeight(49), 362 + preferredSize: const Size.fromHeight(48), 364 363 child: Container( 365 - color: Colors.white, 364 + color: theme.scaffoldBackgroundColor, 366 365 child: TabBar( 366 + dividerColor: theme.dividerColor, 367 367 controller: _tabController, 368 368 indicator: UnderlineTabIndicator( 369 - borderSide: const BorderSide( 370 - color: Color(0xFF0EA5E9), 369 + borderSide: BorderSide( 370 + color: theme.colorScheme.primary, 371 371 width: 3, 372 372 ), 373 373 insets: EdgeInsets.zero, 374 374 ), 375 375 indicatorSize: TabBarIndicatorSize.tab, 376 - labelColor: const Color(0xFF0EA5E9), 377 - unselectedLabelColor: Theme.of( 378 - context, 379 - ).colorScheme.onSurfaceVariant, 376 + labelColor: theme.colorScheme.onSurface, 377 + unselectedLabelColor: theme.colorScheme.onSurfaceVariant, 380 378 labelStyle: const TextStyle( 381 379 fontWeight: FontWeight.w600, 382 380 fontSize: 16, ··· 440 438 ), 441 439 floatingActionButton: 442 440 (!showProfile && !showNotifications && !showExplore) 443 - ? FloatingActionButton( 444 - shape: const CircleBorder(), 445 - onPressed: () { 446 - showModalBottomSheet( 447 - context: context, 448 - isScrollControlled: true, 449 - builder: (context) => CreateGalleryPage(), 450 - ); 451 - }, 452 - backgroundColor: const Color(0xFF0EA5E9), 453 - foregroundColor: Colors.white, 454 - child: const Icon(Icons.add_a_photo), 455 - tooltip: 'Create Gallery', 456 - ) 457 - : null, 441 + ? FloatingActionButton( 442 + shape: const CircleBorder(), 443 + onPressed: () { 444 + showModalBottomSheet( 445 + context: context, 446 + isScrollControlled: true, 447 + builder: (context) => CreateGalleryPage(), 448 + ); 449 + }, 450 + backgroundColor: theme.colorScheme.primary, 451 + foregroundColor: Colors.white, 452 + tooltip: 'Create Gallery', 453 + child: const Icon(Icons.add_a_photo), 454 + ) 455 + : null, 458 456 ); 459 457 } 460 458 // Explore, Notifications, Profile: no tabs, no TabController ··· 463 461 child: ListView( 464 462 padding: EdgeInsets.zero, 465 463 children: [ 466 - DrawerHeader( 467 - decoration: BoxDecoration(color: Colors.white), 464 + Container( 465 + decoration: BoxDecoration( 466 + color: theme.scaffoldBackgroundColor, 467 + border: Border( 468 + bottom: BorderSide(color: theme.dividerColor, width: 1), 469 + ), 470 + ), 471 + padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), 468 472 child: Column( 469 473 crossAxisAlignment: CrossAxisAlignment.start, 470 474 mainAxisAlignment: MainAxisAlignment.center, 471 475 children: [ 472 476 CircleAvatar( 473 - radius: 22, // Smaller avatar 474 - backgroundImage: 475 - apiService.currentUser?.avatar != null && 476 - apiService.currentUser!.avatar.isNotEmpty 477 - ? null 478 - : null, 479 - backgroundColor: Colors.white, 477 + radius: 22, 478 + backgroundColor: theme.scaffoldBackgroundColor, 480 479 child: ClipOval( 481 480 child: AppImage( 482 481 url: apiService.currentUser!.avatar, ··· 489 488 const SizedBox(height: 6), 490 489 Text( 491 490 apiService.currentUser?.displayName ?? '', 492 - style: const TextStyle( 493 - color: Colors.black, 494 - fontSize: 15, // Smaller text 491 + style: theme.textTheme.bodyLarge?.copyWith( 495 492 fontWeight: FontWeight.bold, 493 + fontSize: 15, 494 + color: theme.colorScheme.onSurface, 496 495 ), 497 496 ), 498 497 if (apiService.currentUser?.handle != null) 499 498 Text( 500 499 '@${apiService.currentUser!.handle}', 501 - style: const TextStyle( 502 - color: Colors.black54, 503 - fontSize: 11, // Smaller text 500 + style: theme.textTheme.bodySmall?.copyWith( 501 + color: theme.hintColor, 502 + fontSize: 11, 504 503 ), 505 504 ), 506 505 const SizedBox(height: 6), ··· 510 509 Text( 511 510 (apiService.currentUser?.followersCount ?? 0) 512 511 .toString(), 513 - style: const TextStyle( 514 - color: Colors.black, 512 + style: theme.textTheme.bodyMedium?.copyWith( 515 513 fontWeight: FontWeight.bold, 516 514 fontSize: 13, 515 + color: theme.colorScheme.onSurface, 517 516 ), 518 517 ), 519 518 const SizedBox(width: 4), 520 - const Text( 519 + Text( 521 520 'Followers', 522 - style: TextStyle(color: Colors.black54, fontSize: 10), 521 + style: theme.textTheme.bodySmall?.copyWith( 522 + color: theme.hintColor, 523 + fontSize: 10, 524 + ), 523 525 ), 524 526 const SizedBox(width: 16), 525 527 Text( 526 528 (apiService.currentUser?.followsCount ?? 0).toString(), 527 - style: const TextStyle( 528 - color: Colors.black, 529 + style: theme.textTheme.bodyMedium?.copyWith( 529 530 fontWeight: FontWeight.bold, 530 531 fontSize: 13, 532 + color: theme.colorScheme.onSurface, 531 533 ), 532 534 ), 533 535 const SizedBox(width: 4), 534 - const Text( 536 + Text( 535 537 'Following', 536 - style: TextStyle(color: Colors.black54, fontSize: 10), 538 + style: theme.textTheme.bodySmall?.copyWith( 539 + color: theme.hintColor, 540 + fontSize: 10, 541 + ), 537 542 ), 538 543 ], 539 544 ), ··· 596 601 ), 597 602 appBar: (showExplore || showNotifications) 598 603 ? AppBar( 599 - backgroundColor: Colors.white, 600 - surfaceTintColor: Colors.white, 604 + backgroundColor: theme.appBarTheme.backgroundColor, 605 + surfaceTintColor: theme.appBarTheme.backgroundColor, 601 606 elevation: 0.5, 602 607 title: Text( 603 608 showExplore ? 'Explore' : 'Notifications', 604 - style: const TextStyle( 605 - fontSize: 18, 606 - fontWeight: FontWeight.w600, 607 - ), 609 + style: theme.appBarTheme.titleTextStyle, 608 610 ), 609 611 leading: Builder( 610 612 builder: (context) => IconButton( ··· 626 628 if (showExplore) 627 629 Positioned.fill( 628 630 child: Material( 629 - color: Theme.of( 630 - context, 631 - ).scaffoldBackgroundColor.withOpacity(0.98), 631 + color: theme.scaffoldBackgroundColor.withOpacity(0.98), 632 632 child: SafeArea(child: Stack(children: [ExplorePage()])), 633 633 ), 634 634 ), 635 635 if (showNotifications) 636 636 Positioned.fill( 637 637 child: Material( 638 - color: Theme.of( 639 - context, 640 - ).scaffoldBackgroundColor.withOpacity(0.98), 638 + color: theme.scaffoldBackgroundColor.withOpacity(0.98), 641 639 child: SafeArea(child: Stack(children: [NotificationsPage()])), 642 640 ), 643 641 ), 644 642 if (showProfile) 645 643 Positioned.fill( 646 644 child: Material( 647 - color: Theme.of( 648 - context, 649 - ).scaffoldBackgroundColor.withOpacity(0.98), 645 + color: theme.scaffoldBackgroundColor.withOpacity(0.98), 650 646 child: SafeArea( 651 647 child: Stack( 652 648 children: [
+7 -6
lib/screens/log_page.dart
··· 6 6 7 7 @override 8 8 Widget build(BuildContext context) { 9 + final theme = Theme.of(context); 9 10 final logs = InMemoryLogOutput.logs.reversed.toList(); 10 11 11 12 return Scaffold( 12 13 appBar: AppBar( 13 - title: const Text('Logs'), 14 - backgroundColor: Colors.white, 15 - surfaceTintColor: Colors.white, 14 + title: Text('Logs', style: theme.appBarTheme.titleTextStyle), 15 + backgroundColor: theme.appBarTheme.backgroundColor, 16 16 bottom: PreferredSize( 17 17 preferredSize: const Size.fromHeight(1), 18 - child: Container(color: Theme.of(context).dividerColor, height: 1), 18 + child: Container(color: theme.dividerColor, height: 1), 19 19 ), 20 20 actions: [ 21 21 IconButton( 22 - icon: const Icon(Icons.delete), 22 + icon: Icon(Icons.delete, color: theme.iconTheme.color), 23 23 onPressed: () { 24 24 InMemoryLogOutput.clear(); 25 25 (context as Element).markNeedsBuild(); ··· 27 27 ), 28 28 ], 29 29 ), 30 + backgroundColor: theme.scaffoldBackgroundColor, 30 31 body: ListView.builder( 31 32 itemCount: logs.length, 32 33 itemBuilder: (_, index) => Padding( 33 34 padding: const EdgeInsets.all(4.0), 34 - child: Text(logs[index]), 35 + child: Text(logs[index], style: theme.textTheme.bodyMedium), 35 36 ), 36 37 ), 37 38 );
+25 -11
lib/screens/notifications_page.dart
··· 43 43 } 44 44 45 45 Widget _buildNotificationTile(grain.Notification notification) { 46 + final theme = Theme.of(context); 46 47 final author = notification.author; 47 48 final record = notification.record; 48 49 String message = ''; ··· 75 76 } 76 77 return ListTile( 77 78 leading: CircleAvatar( 79 + backgroundColor: theme.colorScheme.surfaceVariant, 78 80 backgroundImage: author.avatar.isNotEmpty 79 81 ? NetworkImage(author.avatar) 80 82 : null, 81 - child: author.avatar.isEmpty ? const Icon(Icons.account_circle) : null, 83 + child: author.avatar.isEmpty 84 + ? Icon(Icons.account_circle, color: theme.iconTheme.color) 85 + : null, 82 86 ), 83 87 title: Text( 84 88 author.displayName.isNotEmpty 85 89 ? author.displayName 86 90 : '@${author.handle}', 91 + style: theme.textTheme.bodyLarge, 87 92 ), 88 93 subtitle: Text( 89 94 '$message · ${createdAt != null ? formatRelativeTime(createdAt) : ''}', 95 + style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor), 90 96 ), 91 97 onTap: () { 92 98 // TODO: Navigate to gallery/comment/profile as appropriate ··· 96 102 97 103 @override 98 104 Widget build(BuildContext context) { 105 + final theme = Theme.of(context); 99 106 return Scaffold( 107 + backgroundColor: theme.scaffoldBackgroundColor, 100 108 body: _loading 101 - ? const Center( 109 + ? Center( 102 110 child: CircularProgressIndicator( 103 111 strokeWidth: 2, 104 - color: Color(0xFF0EA5E9), 112 + color: theme.colorScheme.primary, 105 113 ), 106 114 ) 107 115 : _error 108 - ? const Center(child: Text('Failed to load notifications.')) 116 + ? Center( 117 + child: Text( 118 + 'Failed to load notifications.', 119 + style: theme.textTheme.bodyMedium, 120 + ), 121 + ) 109 122 : _notifications.isEmpty 110 - ? const Center(child: Text('No notifications yet.')) 123 + ? Center( 124 + child: Text( 125 + 'No notifications yet.', 126 + style: theme.textTheme.bodyMedium, 127 + ), 128 + ) 111 129 : ListView.separated( 112 130 itemCount: _notifications.length, 113 - separatorBuilder: (context, index) => Divider( 114 - height: 1, 115 - color: Theme.of( 116 - context, 117 - ).dividerColor, // Use theme divider color 118 - ), 131 + separatorBuilder: (context, index) => 132 + Divider(height: 1, color: theme.dividerColor), 119 133 itemBuilder: (context, index) { 120 134 final notification = _notifications[index]; 121 135 return _buildNotificationTile(notification);
+38 -27
lib/screens/profile_page.dart
··· 3 3 import 'package:grain/api.dart'; 4 4 import 'gallery_page.dart'; 5 5 import 'package:grain/widgets/app_image.dart'; 6 + import 'package:grain/app_theme.dart'; 6 7 7 8 class ProfilePage extends StatefulWidget { 8 9 final dynamic profile; ··· 109 110 110 111 @override 111 112 Widget build(BuildContext context) { 113 + final theme = Theme.of(context); 112 114 final profile = _profile ?? widget.profile; 113 115 if (_loading) { 114 - return const Scaffold( 115 - backgroundColor: Colors.white, 116 - body: Center( 116 + return Scaffold( 117 + backgroundColor: Theme.of(context).scaffoldBackgroundColor, 118 + body: const Center( 117 119 child: CircularProgressIndicator( 118 120 strokeWidth: 2, 119 - color: Color(0xFF0EA5E9), 121 + color: AppTheme.primaryColor, 120 122 ), 121 123 ), 122 124 ); ··· 125 127 return const Center(child: Text('No profile data')); 126 128 } 127 129 return Scaffold( 128 - backgroundColor: Colors.white, 130 + backgroundColor: theme.scaffoldBackgroundColor, 129 131 appBar: widget.showAppBar 130 132 ? AppBar( 131 - backgroundColor: Colors.white, 132 - surfaceTintColor: Colors.white, 133 + backgroundColor: theme.appBarTheme.backgroundColor, 134 + surfaceTintColor: theme.appBarTheme.backgroundColor, 133 135 bottom: PreferredSize( 134 136 preferredSize: const Size.fromHeight(1), 135 - child: Container( 136 - color: Theme.of(context).dividerColor, 137 - height: 1, 138 - ), 137 + child: Container(color: theme.dividerColor, height: 1), 139 138 ), 140 139 leading: const BackButton(), 141 140 ) ··· 246 245 ), 247 246 // TabBar with no horizontal padding 248 247 Container( 249 - color: Colors.white, 248 + color: theme.scaffoldBackgroundColor, 250 249 child: TabBar( 250 + dividerColor: theme.disabledColor, 251 251 controller: _tabController, 252 252 indicator: UnderlineTabIndicator( 253 253 borderSide: const BorderSide( 254 - color: Color(0xFF0EA5E9), 254 + color: AppTheme.primaryColor, 255 255 width: 3, 256 256 ), 257 257 insets: EdgeInsets.zero, 258 258 ), 259 259 indicatorSize: TabBarIndicatorSize.tab, 260 - labelColor: const Color(0xFF0EA5E9), 261 - unselectedLabelColor: Theme.of( 262 - context, 263 - ).colorScheme.onSurfaceVariant, 260 + labelColor: theme.colorScheme.onSurface, 261 + unselectedLabelColor: 262 + theme.colorScheme.onSurfaceVariant, 264 263 labelStyle: const TextStyle( 265 264 fontWeight: FontWeight.w600, 266 265 fontSize: 16, ··· 284 283 ? const Center( 285 284 child: CircularProgressIndicator( 286 285 strokeWidth: 2, 287 - color: Color(0xFF0EA5E9), 286 + color: AppTheme.primaryColor, 288 287 ), 289 288 ) 290 289 : _galleries.isEmpty ··· 323 322 }, 324 323 child: Container( 325 324 decoration: BoxDecoration( 326 - color: Colors.grey[200], 325 + color: Theme.of( 326 + context, 327 + ).colorScheme.surfaceContainerHighest, 327 328 ), 328 329 clipBehavior: Clip.antiAlias, 329 330 child: hasPhoto ··· 334 335 : Center( 335 336 child: Text( 336 337 gallery.title, 337 - style: const TextStyle( 338 + style: TextStyle( 338 339 fontSize: 12, 339 - color: Colors.black54, 340 + color: theme 341 + .colorScheme 342 + .onSurfaceVariant, 340 343 ), 341 344 textAlign: TextAlign.center, 342 345 ), ··· 345 348 ); 346 349 } 347 350 // Placeholder for empty slots 348 - return Container(color: Colors.grey[100]); 351 + return Container( 352 + color: 353 + theme.colorScheme.surfaceContainerHighest, 354 + ); 349 355 }, 350 356 ), 351 357 // Favs tab ··· 354 360 ? const Center( 355 361 child: CircularProgressIndicator( 356 362 strokeWidth: 2, 357 - color: Color(0xFF0EA5E9), 363 + color: AppTheme.primaryColor, 358 364 ), 359 365 ) 360 366 : _favs.isEmpty ··· 389 395 }, 390 396 child: Container( 391 397 decoration: BoxDecoration( 392 - color: Colors.grey[200], 398 + color: theme 399 + .colorScheme 400 + .surfaceContainerHighest, 393 401 ), 394 402 clipBehavior: Clip.antiAlias, 395 403 child: hasPhoto ··· 400 408 : Center( 401 409 child: Text( 402 410 gallery.title, 403 - style: const TextStyle( 411 + style: TextStyle( 404 412 fontSize: 12, 405 - color: Colors.black54, 413 + color: theme 414 + .colorScheme 415 + .onSurfaceVariant, 406 416 ), 407 417 textAlign: TextAlign.center, 408 418 ), ··· 434 444 435 445 @override 436 446 Widget build(BuildContext context) { 447 + final theme = Theme.of(context); 437 448 final styleCount = const TextStyle( 438 449 fontWeight: FontWeight.bold, 439 450 fontSize: 14, // Set to 14 440 451 ); 441 452 final styleLabel = TextStyle( 442 - color: Theme.of(context).brightness == Brightness.dark 453 + color: theme.brightness == Brightness.dark 443 454 ? Colors.grey[400] 444 455 : Colors.grey[700], 445 456 fontSize: 14, // Set to 14
+17 -38
lib/screens/splash_page.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:grain/auth.dart'; 3 + import 'package:grain/widgets/app_button.dart'; 3 4 import 'package:grain/widgets/app_image.dart'; 5 + import 'package:grain/widgets/plain_text_field.dart'; 4 6 5 7 class SplashPage extends StatefulWidget { 6 8 final void Function()? onSignIn; ··· 39 41 40 42 @override 41 43 Widget build(BuildContext context) { 44 + final theme = Theme.of(context); 42 45 return Scaffold( 46 + backgroundColor: theme.scaffoldBackgroundColor, 43 47 body: Stack( 44 48 fit: StackFit.expand, 45 49 children: [ ··· 55 59 children: [ 56 60 const SizedBox(height: 24), 57 61 Padding( 58 - padding: const EdgeInsets.symmetric(horizontal: 32), 59 - child: TextField( 62 + padding: const EdgeInsets.symmetric(horizontal: 16), 63 + child: PlainTextField( 64 + label: '', 60 65 controller: _handleController, 61 - decoration: const InputDecoration( 62 - labelText: 'Enter your handle', 63 - border: OutlineInputBorder(), 64 - fillColor: Colors.white, 65 - filled: true, 66 - floatingLabelBehavior: FloatingLabelBehavior.never, 67 - contentPadding: EdgeInsets.symmetric( 68 - vertical: 10, 69 - horizontal: 12, 70 - ), 71 - ), 66 + hintText: 'Enter your handle', 72 67 enabled: !_signingIn, 73 - onSubmitted: (_) => _signInWithBluesky(context), 68 + onChanged: (_) {}, 74 69 ), 75 70 ), 76 71 const SizedBox(height: 12), 77 72 Padding( 78 - padding: const EdgeInsets.symmetric(horizontal: 32), 73 + padding: const EdgeInsets.symmetric(horizontal: 16), 79 74 child: SizedBox( 80 75 width: double.infinity, 81 - child: ElevatedButton( 82 - style: ElevatedButton.styleFrom( 83 - backgroundColor: const Color(0xFF0EA5E9), 84 - foregroundColor: Colors.white, 85 - textStyle: const TextStyle( 86 - fontWeight: FontWeight.bold, 87 - fontSize: 15, 88 - ), 89 - padding: const EdgeInsets.symmetric(vertical: 12), 90 - shape: RoundedRectangleBorder( 91 - borderRadius: BorderRadius.circular(6), 92 - ), 93 - ), 76 + child: AppButton( 77 + label: 'Login', 94 78 onPressed: _signingIn 95 79 ? null 96 80 : () => _signInWithBluesky(context), 97 - child: _signingIn 98 - ? const SizedBox( 99 - width: 20, 100 - height: 20, 101 - child: CircularProgressIndicator( 102 - strokeWidth: 2, 103 - color: Colors.white, 104 - ), 105 - ) 106 - : const Text('Login'), 81 + loading: _signingIn, 82 + variant: AppButtonVariant.primary, 83 + height: 44, 84 + fontSize: 15, 85 + borderRadius: 6, 107 86 ), 108 87 ), 109 88 ),
+19 -12
lib/widgets/app_button.dart
··· 28 28 29 29 @override 30 30 Widget build(BuildContext context) { 31 - final Color primaryColor = const Color(0xFF0EA5E9); // Tailwind sky-500 32 - final Color secondaryColor = Theme.of( 33 - context, 34 - ).colorScheme.surfaceContainerHighest; 35 - final Color secondaryBorder = Colors.grey[300]!; 36 - final Color secondaryText = Colors.black87; 37 - final Color primaryText = Colors.white; 38 - 31 + final theme = Theme.of(context); 32 + final Color primaryColor = theme.colorScheme.primary; 33 + final Color secondaryColor = theme.colorScheme.surfaceContainerHighest; 34 + final Color secondaryBorder = theme.dividerColor; 35 + final Color secondaryText = theme.colorScheme.onSurface; 36 + final Color primaryText = theme.colorScheme.onPrimary; 39 37 final bool isPrimary = variant == AppButtonVariant.primary; 40 38 41 39 return SizedBox( ··· 53 51 : BorderSide(color: secondaryBorder, width: 1), 54 52 ), 55 53 padding: padding ?? const EdgeInsets.symmetric(horizontal: 16), 56 - textStyle: TextStyle(fontWeight: FontWeight.w600, fontSize: fontSize), 54 + textStyle: theme.textTheme.labelLarge?.copyWith( 55 + fontWeight: FontWeight.w600, 56 + fontSize: fontSize, 57 + ), 57 58 ), 58 59 child: loading 59 60 ? SizedBox( 60 61 width: 22, 61 62 height: 22, 62 63 child: CircularProgressIndicator( 63 - color: isPrimary ? Colors.white : primaryColor, 64 + color: isPrimary ? primaryColor : secondaryColor, 64 65 strokeWidth: 2, 65 66 ), 66 67 ) ··· 72 73 Icon( 73 74 icon, 74 75 size: 20, 75 - color: isPrimary ? Colors.white : primaryColor, 76 + color: isPrimary ? primaryText : secondaryText, 76 77 ), 77 78 const SizedBox(width: 8), 78 79 ], 79 - Text(label), 80 + Text( 81 + label, 82 + style: theme.textTheme.labelLarge?.copyWith( 83 + fontWeight: FontWeight.w600, 84 + fontSize: fontSize, 85 + ), 86 + ), 80 87 ], 81 88 ), 82 89 ),
+9 -3
lib/widgets/app_image.dart
··· 23 23 24 24 @override 25 25 Widget build(BuildContext context) { 26 + final theme = Theme.of(context); 27 + final Color bgColor = theme.brightness == Brightness.dark 28 + ? Colors.grey[900]! 29 + : Colors.grey[100]!; 26 30 if (url == null || url!.isEmpty) { 27 31 return errorWidget ?? 28 32 Container( 29 33 width: width, 30 34 height: height, 31 - color: Colors.grey[200], 35 + color: bgColor, 32 36 child: const Icon(Icons.broken_image, color: Colors.grey), 33 37 ); 34 38 } ··· 37 41 width: width, 38 42 height: height, 39 43 fit: fit, 44 + fadeInDuration: Duration.zero, 45 + fadeOutDuration: Duration.zero, 40 46 placeholder: (context, _) => 41 47 placeholder ?? 42 48 Container( 43 49 width: width, 44 50 height: height, 45 - color: Colors.grey[200], 51 + color: bgColor, 46 52 // child: const Center( 47 53 // child: CircularProgressIndicator( 48 54 // strokeWidth: 2, ··· 55 61 Container( 56 62 width: width, 57 63 height: height, 58 - color: Colors.grey[200], 64 + color: bgColor, 59 65 child: const Icon(Icons.broken_image, color: Colors.grey), 60 66 ), 61 67 );
+7 -6
lib/widgets/bottom_nav_bar.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 3 + import 'package:grain/app_theme.dart'; 3 4 import 'package:grain/widgets/app_image.dart'; 4 5 5 6 class BottomNavBar extends StatelessWidget { ··· 24 25 Widget build(BuildContext context) { 25 26 return Container( 26 27 decoration: BoxDecoration( 27 - color: Colors.white, 28 + color: Theme.of(context).scaffoldBackgroundColor, 28 29 border: Border( 29 30 top: BorderSide(color: Theme.of(context).dividerColor, width: 1), 30 31 ), ··· 46 47 FontAwesomeIcons.house, 47 48 size: 20, 48 49 color: navIndex == 0 49 - ? const Color(0xFF0EA5E9) 50 + ? AppTheme.primaryColor 50 51 : Theme.of(context).colorScheme.onSurfaceVariant, 51 52 ), 52 53 ), ··· 67 68 FontAwesomeIcons.magnifyingGlass, 68 69 size: 20, 69 70 color: navIndex == 1 70 - ? const Color(0xFF0EA5E9) 71 + ? AppTheme.primaryColor 71 72 : Theme.of(context).colorScheme.onSurfaceVariant, 72 73 ), 73 74 ), ··· 88 89 FontAwesomeIcons.solidBell, 89 90 size: 20, 90 91 color: navIndex == 2 91 - ? const Color(0xFF0EA5E9) 92 + ? AppTheme.primaryColor 92 93 : Theme.of(context).colorScheme.onSurfaceVariant, 93 94 ), 94 95 ), ··· 114 115 ? BoxDecoration( 115 116 shape: BoxShape.circle, 116 117 border: Border.all( 117 - color: const Color(0xFF0EA5E9), 118 + color: AppTheme.primaryColor, 118 119 width: 2.2, 119 120 ), 120 121 ) ··· 134 135 : FontAwesomeIcons.user, 135 136 size: 16, 136 137 color: navIndex == 3 137 - ? const Color(0xFF0EA5E9) 138 + ? AppTheme.primaryColor 138 139 : Theme.of( 139 140 context, 140 141 ).colorScheme.onSurfaceVariant,
+7 -3
lib/widgets/gallery_preview.dart
··· 9 9 10 10 @override 11 11 Widget build(BuildContext context) { 12 + final theme = Theme.of(context); 13 + final Color bgColor = theme.brightness == Brightness.dark 14 + ? Colors.grey[900]! 15 + : Colors.grey[100]!; 12 16 final photos = gallery.items 13 17 .where((item) => item.thumb.isNotEmpty) 14 18 .toList(); ··· 25 29 width: double.infinity, 26 30 height: double.infinity, 27 31 ) 28 - : Container(color: Colors.grey[300]), 32 + : Container(color: theme.colorScheme.surfaceContainerHighest), 29 33 ), 30 34 const SizedBox(width: 2), 31 35 Expanded( ··· 40 44 width: double.infinity, 41 45 height: double.infinity, 42 46 ) 43 - : Container(color: Colors.grey[200]), 47 + : Container(color: bgColor), 44 48 ), 45 49 const SizedBox(height: 2), 46 50 Expanded( ··· 51 55 width: double.infinity, 52 56 height: double.infinity, 53 57 ) 54 - : Container(color: Colors.grey[200]), 58 + : Container(color: bgColor), 55 59 ), 56 60 ], 57 61 ),
+41 -20
lib/widgets/plain_text_field.dart
··· 22 22 23 23 @override 24 24 Widget build(BuildContext context) { 25 + final theme = Theme.of(context); 25 26 return Column( 26 27 crossAxisAlignment: CrossAxisAlignment.start, 27 28 children: [ 28 29 Text( 29 30 label, 30 - style: const TextStyle( 31 + style: theme.textTheme.bodyMedium?.copyWith( 31 32 fontWeight: FontWeight.w500, 32 - fontSize: 15, 33 - color: Colors.black87, 33 + color: theme.colorScheme.onSurface, 34 34 ), 35 35 ), 36 36 const SizedBox(height: 6), 37 37 Container( 38 38 decoration: BoxDecoration( 39 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 39 + color: theme.colorScheme.surfaceContainerHighest, 40 40 borderRadius: BorderRadius.circular(8), 41 - border: Border.all(color: Colors.grey[300]!), 42 41 ), 43 - child: TextField( 44 - controller: controller, 45 - maxLines: maxLines, 46 - enabled: enabled, 47 - keyboardType: keyboardType, 48 - onChanged: onChanged, 49 - style: const TextStyle(fontSize: 15), 50 - decoration: InputDecoration( 51 - hintText: hintText, 52 - border: InputBorder.none, 53 - contentPadding: const EdgeInsets.symmetric( 54 - horizontal: 12, 55 - vertical: 12, 56 - ), 57 - isDense: true, 42 + child: Focus( 43 + child: Builder( 44 + builder: (context) { 45 + final isFocused = Focus.of(context).hasFocus; 46 + return AnimatedContainer( 47 + duration: const Duration(milliseconds: 150), 48 + decoration: BoxDecoration( 49 + border: Border.all( 50 + color: isFocused 51 + ? theme.colorScheme.primary 52 + : theme.dividerColor, 53 + width: isFocused ? 2 : 1, 54 + ), 55 + borderRadius: BorderRadius.circular(8), 56 + ), 57 + child: TextField( 58 + controller: controller, 59 + maxLines: maxLines, 60 + enabled: enabled, 61 + keyboardType: keyboardType, 62 + onChanged: onChanged, 63 + style: theme.textTheme.bodyMedium?.copyWith(fontSize: 15), 64 + decoration: InputDecoration( 65 + hintText: hintText, 66 + hintStyle: theme.textTheme.bodyMedium?.copyWith( 67 + color: theme.hintColor, 68 + ), 69 + border: InputBorder.none, 70 + contentPadding: const EdgeInsets.symmetric( 71 + horizontal: 12, 72 + vertical: 12, 73 + ), 74 + isDense: true, 75 + ), 76 + ), 77 + ); 78 + }, 58 79 ), 59 80 ), 60 81 ),
+30 -14
lib/widgets/timeline_item.dart
··· 8 8 import 'package:grain/api.dart'; 9 9 import 'package:grain/utils.dart'; 10 10 import 'package:grain/widgets/app_image.dart'; 11 + import 'package:grain/app_theme.dart'; 11 12 12 13 class TimelineItemWidget extends StatelessWidget { 13 14 final Gallery gallery; ··· 22 23 Widget build(BuildContext context) { 23 24 final actor = gallery.creator; 24 25 final createdAt = gallery.createdAt; 26 + final theme = Theme.of(context); 25 27 return Column( 26 28 crossAxisAlignment: CrossAxisAlignment.start, 27 29 children: [ ··· 44 46 }, 45 47 child: CircleAvatar( 46 48 radius: 18, 47 - backgroundColor: Colors.transparent, 49 + backgroundColor: theme.scaffoldBackgroundColor, 48 50 child: (actor != null && actor.avatar.isNotEmpty) 49 51 ? ClipOval( 50 52 child: AppImage( ··· 54 56 fit: BoxFit.cover, 55 57 ), 56 58 ) 57 - : const Icon( 59 + : Icon( 58 60 Icons.account_circle, 59 61 size: 24, 60 - color: Colors.grey, 62 + color: theme.colorScheme.onSurfaceVariant, 61 63 ), 62 64 ), 63 65 ), ··· 70 72 actor != null && actor.displayName.isNotEmpty 71 73 ? actor.displayName 72 74 : (actor != null ? '@${actor.handle}' : ''), 73 - style: const TextStyle( 75 + style: theme.textTheme.titleMedium?.copyWith( 74 76 fontWeight: FontWeight.w600, 75 77 fontSize: 16, 76 78 ), ··· 82 84 padding: const EdgeInsets.only(left: 6), 83 85 child: Text( 84 86 '@${actor.handle}', 85 - style: TextStyle( 87 + style: theme.textTheme.bodySmall?.copyWith( 86 88 fontSize: 14, 87 - color: Colors.grey[800], 89 + color: theme.colorScheme.onSurfaceVariant, 88 90 fontWeight: FontWeight.normal, 89 91 ), 90 92 overflow: TextOverflow.ellipsis, ··· 96 98 ), 97 99 Text( 98 100 formatRelativeTime(createdAt ?? ''), 99 - style: const TextStyle(fontSize: 12, color: Colors.grey), 101 + style: theme.textTheme.bodySmall?.copyWith( 102 + fontSize: 12, 103 + color: theme.colorScheme.onSurfaceVariant, 104 + ), 100 105 ), 101 106 ], 102 107 ), ··· 124 129 padding: const EdgeInsets.only(top: 8, left: 8, right: 8), 125 130 child: Text( 126 131 gallery.title, 127 - style: const TextStyle(fontWeight: FontWeight.w600), 132 + style: theme.textTheme.titleMedium?.copyWith( 133 + fontWeight: FontWeight.w600, 134 + ), 128 135 ), 129 136 ), 130 137 if (gallery.description.isNotEmpty) ··· 132 139 padding: const EdgeInsets.only(top: 4, left: 8, right: 8), 133 140 child: Text( 134 141 gallery.description, 135 - style: const TextStyle(fontSize: 13, color: Colors.black54), 142 + style: theme.textTheme.bodySmall?.copyWith( 143 + fontSize: 13, 144 + color: theme.colorScheme.onSurfaceVariant, 145 + ), 136 146 ), 137 147 ), 138 148 const SizedBox(height: 8), ··· 155 165 : FontAwesomeIcons.heart, 156 166 color: 157 167 gallery.viewer != null && gallery.viewer!['fav'] != null 158 - ? Color(0xFFEC4899) 159 - : Colors.black54, 168 + ? AppTheme.favoriteColor 169 + : theme.colorScheme.onSurfaceVariant, 160 170 ), 161 171 ), 162 172 onTap: () {}, ··· 166 176 padding: const EdgeInsets.only(right: 12), 167 177 child: Text( 168 178 gallery.favCount.toString(), 169 - style: const TextStyle(fontSize: 14, color: Colors.black54), 179 + style: theme.textTheme.bodySmall?.copyWith( 180 + fontSize: 14, 181 + color: theme.colorScheme.onSurfaceVariant, 182 + ), 170 183 ), 171 184 ), 172 185 GestureDetector( ··· 183 196 child: Icon( 184 197 FontAwesomeIcons.comment, 185 198 size: 18, 186 - color: Colors.black54, 199 + color: theme.colorScheme.onSurfaceVariant, 187 200 ), 188 201 ), 189 202 ), 190 203 if (gallery.commentCount != null) 191 204 Text( 192 205 gallery.commentCount.toString(), 193 - style: const TextStyle(fontSize: 14, color: Colors.black54), 206 + style: theme.textTheme.bodySmall?.copyWith( 207 + fontSize: 14, 208 + color: theme.colorScheme.onSurfaceVariant, 209 + ), 194 210 ), 195 211 ], 196 212 ),