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