reorganzie lib, update drawer ui, wire up explore page to searchActors api

+28 -3
lib/api.dart
··· 2 import 'package:grain/main.dart'; 3 import 'package:http/http.dart' as http; 4 import 'dart:convert'; 5 - import 'profile.dart'; 6 - import 'gallery.dart'; 7 - import 'notification.dart' as grain; 8 9 class ApiService { 10 String? _accessToken; ··· 175 'Failed to load notifications: status ${response.statusCode}, body: ${response.body}', 176 ); 177 throw Exception('Failed to load notifications: \\${response.statusCode}'); 178 } 179 } 180 }
··· 2 import 'package:grain/main.dart'; 3 import 'package:http/http.dart' as http; 4 import 'dart:convert'; 5 + import 'models/profile.dart'; 6 + import 'models/gallery.dart'; 7 + import 'models/notification.dart' as grain; 8 9 class ApiService { 10 String? _accessToken; ··· 175 'Failed to load notifications: status ${response.statusCode}, body: ${response.body}', 176 ); 177 throw Exception('Failed to load notifications: \\${response.statusCode}'); 178 + } 179 + } 180 + 181 + Future<List<Profile>> searchActors(String query) async { 182 + if (_accessToken == null) return []; 183 + appLogger.i('Searching actors for query: $query with token: $_accessToken'); 184 + 185 + final response = await http.get( 186 + Uri.parse('$_apiUrl/xrpc/social.grain.actor.searchActors?q=$query'), 187 + headers: {'Authorization': 'Bearer $_accessToken'}, 188 + ); 189 + if (response.statusCode == 200) { 190 + final data = json.decode(response.body); 191 + final items = data['actors'] as List<dynamic>?; 192 + if (items != null) { 193 + appLogger.i('Found ${items.length} actors for query: $query'); 194 + return items.map((item) => Profile.fromJson(item)).toList(); 195 + } else { 196 + return []; 197 + } 198 + } else { 199 + appLogger.e( 200 + 'Failed to search actors: status ${response.statusCode}, body: ${response.body}', 201 + ); 202 + throw Exception('Failed to search actors: ${response.statusCode}'); 203 } 204 } 205 }
lib/app_version_text.dart lib/widgets/app_version_text.dart
lib/comment.dart lib/models/comment.dart
+31 -22
lib/comments_page.dart lib/screens/comments_page.dart
··· 1 import 'package:flutter/material.dart'; 2 import 'package:grain/api.dart'; 3 - import 'package:grain/comment.dart'; 4 - import 'package:grain/gallery.dart'; 5 import 'package:grain/utils.dart'; 6 7 class CommentsPage extends StatefulWidget { ··· 49 @override 50 Widget build(BuildContext context) { 51 return Scaffold( 52 - appBar: AppBar(title: const Text('Comments')), 53 body: _loading 54 ? const Center(child: CircularProgressIndicator()) 55 : _error 56 ? const Center(child: Text('Failed to load comments.')) 57 : ListView( 58 - padding: const EdgeInsets.all(12), 59 - children: [ 60 - if (_gallery != null) 61 - Text( 62 - _gallery!.title, 63 - style: const TextStyle( 64 - fontWeight: FontWeight.bold, 65 - fontSize: 18, 66 - ), 67 ), 68 - const SizedBox(height: 12), 69 - _CommentsList(comments: _comments), 70 - ], 71 - ), 72 ); 73 } 74 } ··· 91 return comments.where((c) => c.replyTo == null).toList(); 92 } 93 94 - Widget _buildCommentTree(Comment comment, Map<String, List<Comment>> repliesByParent, int depth) { 95 return Padding( 96 padding: EdgeInsets.only(left: depth * 18.0), 97 child: Column( ··· 99 children: [ 100 _CommentTile(comment: comment), 101 if (repliesByParent[comment.uri] != null) 102 - ...repliesByParent[comment.uri]! 103 - .map((reply) => _buildCommentTree(reply, repliesByParent, depth + 1)) 104 - , 105 ], 106 ), 107 ); ··· 161 ), 162 child: AspectRatio( 163 aspectRatio: 164 - (comment.focus!.width > 0 && 165 - comment.focus!.height > 0) 166 ? comment.focus!.width / comment.focus!.height 167 : 1.0, 168 child: ClipRRect(
··· 1 import 'package:flutter/material.dart'; 2 import 'package:grain/api.dart'; 3 + import 'package:grain/models/comment.dart'; 4 + import 'package:grain/models/gallery.dart'; 5 import 'package:grain/utils.dart'; 6 7 class CommentsPage extends StatefulWidget { ··· 49 @override 50 Widget build(BuildContext context) { 51 return Scaffold( 52 + appBar: AppBar( 53 + title: const Text( 54 + 'Comments', 55 + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), 56 + ), 57 + ), 58 body: _loading 59 ? const Center(child: CircularProgressIndicator()) 60 : _error 61 ? const Center(child: Text('Failed to load comments.')) 62 : ListView( 63 + padding: const EdgeInsets.all(12), 64 + children: [ 65 + if (_gallery != null) 66 + Text( 67 + _gallery!.title, 68 + style: const TextStyle( 69 + fontWeight: FontWeight.bold, 70 + fontSize: 18, 71 + ), 72 + ), 73 + const SizedBox(height: 12), 74 + _CommentsList(comments: _comments), 75 + ], 76 ), 77 ); 78 } 79 } ··· 96 return comments.where((c) => c.replyTo == null).toList(); 97 } 98 99 + Widget _buildCommentTree( 100 + Comment comment, 101 + Map<String, List<Comment>> repliesByParent, 102 + int depth, 103 + ) { 104 return Padding( 105 padding: EdgeInsets.only(left: depth * 18.0), 106 child: Column( ··· 108 children: [ 109 _CommentTile(comment: comment), 110 if (repliesByParent[comment.uri] != null) 111 + ...repliesByParent[comment.uri]!.map( 112 + (reply) => _buildCommentTree(reply, repliesByParent, depth + 1), 113 + ), 114 ], 115 ), 116 ); ··· 170 ), 171 child: AspectRatio( 172 aspectRatio: 173 + (comment.focus!.width > 0 && 174 + comment.focus!.height > 0) 175 ? comment.focus!.width / comment.focus!.height 176 : 1.0, 177 child: ClipRRect(
lib/gallery.dart lib/models/gallery.dart
+8 -5
lib/gallery_page.dart lib/screens/gallery_page.dart
··· 1 import 'package:flutter/material.dart'; 2 - import 'package:grain/gallery.dart'; 3 import 'package:grain/api.dart'; 4 - import 'package:grain/justified_gallery_view.dart'; 5 - import 'package:grain/comments_page.dart'; 6 - import 'utils.dart'; 7 8 class GalleryPage extends StatefulWidget { 9 final String uri; ··· 62 .toList(); 63 return Scaffold( 64 appBar: AppBar( 65 - title: Text(gallery.title.isNotEmpty ? gallery.title : 'Gallery'), 66 ), 67 body: Padding( 68 padding: const EdgeInsets.symmetric(horizontal: 16),
··· 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'; 5 + import 'package:grain/utils.dart'; 6 + import './comments_page.dart'; 7 8 class GalleryPage extends StatefulWidget { 9 final String uri; ··· 62 .toList(); 63 return Scaffold( 64 appBar: AppBar( 65 + title: Text( 66 + gallery.title.isNotEmpty ? gallery.title : 'Gallery', 67 + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), 68 + ), 69 ), 70 body: Padding( 71 padding: const EdgeInsets.symmetric(horizontal: 16),
+171 -42
lib/home_page.dart lib/screens/home_page.dart
··· 1 import 'package:flutter/material.dart'; 2 import 'package:grain/api.dart'; 3 - import 'package:grain/gallery.dart'; 4 - import 'package:grain/gallery_page.dart'; 5 - import 'package:grain/comments_page.dart'; 6 import 'profile_page.dart'; 7 - import 'utils.dart'; 8 import 'log_page.dart'; 9 - import 'app_version_text.dart'; 10 import 'notifications_page.dart'; 11 12 class TimelineItem { 13 final Gallery gallery; ··· 35 class _MyHomePageState extends State<MyHomePage> { 36 bool showProfile = false; 37 bool showNotifications = false; 38 List<TimelineItem> _timeline = []; 39 bool _timelineLoading = true; 40 ··· 63 } 64 65 int get _navIndex { 66 - if (showProfile) return 2; 67 - if (showNotifications) return 1; 68 return 0; 69 } 70 ··· 262 padding: EdgeInsets.zero, 263 children: [ 264 DrawerHeader( 265 - decoration: BoxDecoration( 266 - color: Theme.of(context).colorScheme.inversePrimary, 267 - ), 268 - child: Text( 269 - 'Menu', 270 - style: TextStyle(color: Colors.white, fontSize: 24), 271 ), 272 ), 273 ListTile( 274 - leading: const Icon(Icons.home), 275 title: const Text('Home'), 276 onTap: () { 277 Navigator.pop(context); 278 setState(() { 279 showProfile = false; 280 showNotifications = false; 281 }); 282 }, 283 ), 284 ListTile( 285 - leading: const Icon(Icons.person), 286 title: const Text('Profile'), 287 onTap: () { 288 Navigator.pop(context); 289 setState(() { 290 showProfile = true; 291 showNotifications = false; 292 }); 293 }, 294 ), 295 ListTile( 296 - leading: const Icon(Icons.list_alt), 297 title: const Text('Logs'), 298 onTap: () { 299 Navigator.pop(context); ··· 322 ), 323 ), 324 title: Text( 325 - showNotifications ? 'Notifications' : widget.title, 326 style: const TextStyle( 327 fontSize: 18, 328 fontWeight: FontWeight.w600, ··· 338 ), 339 body: Stack( 340 children: [ 341 - if (!showProfile && !showNotifications) _buildTimeline(), 342 if (showNotifications) 343 Positioned.fill( 344 child: Material( ··· 371 bottomNavigationBar: BottomNavigationBar( 372 items: <BottomNavigationBarItem>[ 373 BottomNavigationBarItem( 374 - icon: Icon(_navIndex == 0 ? Icons.home : Icons.home_outlined), 375 label: '', 376 ), 377 BottomNavigationBarItem( 378 - icon: const Icon(Icons.notifications_none), 379 label: '', 380 ), 381 BottomNavigationBarItem( 382 icon: apiService.currentUser?.avatar != null 383 - ? Container( 384 - width: 24, 385 - height: 24, 386 - decoration: BoxDecoration( 387 - shape: BoxShape.circle, 388 - border: Border.all( 389 - color: _navIndex == 2 390 - ? Colors.lightBlue 391 - : Colors.transparent, 392 - width: 3, 393 ), 394 - ), 395 - child: CircleAvatar( 396 - radius: 12, 397 - backgroundImage: NetworkImage( 398 - apiService.currentUser!.avatar, 399 ), 400 - backgroundColor: Colors.transparent, 401 ), 402 ) 403 - : Icon( 404 - _navIndex == 2 405 - ? Icons.account_circle 406 - : Icons.account_circle_outlined, 407 ), 408 label: '', 409 ), ··· 411 currentIndex: _navIndex, 412 selectedItemColor: Colors.lightBlue, 413 onTap: (index) { 414 - if (index == 1) { 415 setState(() { 416 showNotifications = true; 417 showProfile = false; 418 }); 419 return; 420 } 421 - if (index == 2) { 422 setState(() { 423 showProfile = true; 424 showNotifications = false; 425 }); 426 return; 427 } ··· 429 setState(() { 430 showProfile = false; 431 showNotifications = false; 432 }); 433 return; 434 }
··· 1 import 'package:flutter/material.dart'; 2 import 'package:grain/api.dart'; 3 + import 'package:grain/models/gallery.dart'; 4 + import 'gallery_page.dart'; 5 + import 'comments_page.dart'; 6 import 'profile_page.dart'; 7 + import 'package:grain/utils.dart'; 8 import 'log_page.dart'; 9 + import 'package:grain/widgets/app_version_text.dart'; 10 import 'notifications_page.dart'; 11 + import 'explore_page.dart'; 12 + import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 13 14 class TimelineItem { 15 final Gallery gallery; ··· 37 class _MyHomePageState extends State<MyHomePage> { 38 bool showProfile = false; 39 bool showNotifications = false; 40 + bool showExplore = false; 41 List<TimelineItem> _timeline = []; 42 bool _timelineLoading = true; 43 ··· 66 } 67 68 int get _navIndex { 69 + if (showProfile) return 3; 70 + if (showNotifications) return 2; 71 + if (showExplore) return 1; 72 return 0; 73 } 74 ··· 266 padding: EdgeInsets.zero, 267 children: [ 268 DrawerHeader( 269 + decoration: BoxDecoration(color: Colors.white), 270 + child: Column( 271 + crossAxisAlignment: CrossAxisAlignment.start, 272 + mainAxisAlignment: MainAxisAlignment.center, 273 + children: [ 274 + CircleAvatar( 275 + radius: 22, // Smaller avatar 276 + backgroundImage: 277 + apiService.currentUser?.avatar != null && 278 + apiService.currentUser!.avatar.isNotEmpty 279 + ? NetworkImage(apiService.currentUser!.avatar) 280 + : null, 281 + backgroundColor: Colors.white, 282 + child: 283 + (apiService.currentUser == null || 284 + apiService.currentUser!.avatar.isEmpty) 285 + ? const Icon( 286 + Icons.account_circle, 287 + size: 32, 288 + color: Colors.grey, 289 + ) 290 + : null, 291 + ), 292 + const SizedBox(height: 6), 293 + Text( 294 + apiService.currentUser?.displayName ?? '', 295 + style: const TextStyle( 296 + color: Colors.black, 297 + fontSize: 15, // Smaller text 298 + fontWeight: FontWeight.bold, 299 + ), 300 + ), 301 + if (apiService.currentUser?.handle != null) 302 + Text( 303 + '@${apiService.currentUser!.handle}', 304 + style: const TextStyle( 305 + color: Colors.black54, 306 + fontSize: 11, // Smaller text 307 + ), 308 + ), 309 + const SizedBox(height: 6), 310 + Row( 311 + mainAxisAlignment: MainAxisAlignment.start, 312 + children: [ 313 + Text( 314 + (apiService.currentUser?.followersCount ?? 0) 315 + .toString(), 316 + style: const TextStyle( 317 + color: Colors.black, 318 + fontWeight: FontWeight.bold, 319 + fontSize: 13, 320 + ), 321 + ), 322 + const SizedBox(width: 4), 323 + const Text( 324 + 'Followers', 325 + style: TextStyle(color: Colors.black54, fontSize: 10), 326 + ), 327 + const SizedBox(width: 16), 328 + Text( 329 + (apiService.currentUser?.followsCount ?? 0).toString(), 330 + style: const TextStyle( 331 + color: Colors.black, 332 + fontWeight: FontWeight.bold, 333 + fontSize: 13, 334 + ), 335 + ), 336 + const SizedBox(width: 4), 337 + const Text( 338 + 'Following', 339 + style: TextStyle(color: Colors.black54, fontSize: 10), 340 + ), 341 + ], 342 + ), 343 + ], 344 ), 345 ), 346 ListTile( 347 + leading: const Icon(FontAwesomeIcons.house), 348 title: const Text('Home'), 349 onTap: () { 350 Navigator.pop(context); 351 setState(() { 352 showProfile = false; 353 showNotifications = false; 354 + showExplore = false; 355 }); 356 }, 357 ), 358 ListTile( 359 + leading: const Icon(FontAwesomeIcons.magnifyingGlass), 360 + title: const Text('Explore'), 361 + onTap: () { 362 + Navigator.pop(context); 363 + setState(() { 364 + showExplore = true; 365 + showProfile = false; 366 + showNotifications = false; 367 + }); 368 + }, 369 + ), 370 + ListTile( 371 + leading: const Icon(FontAwesomeIcons.user), 372 title: const Text('Profile'), 373 onTap: () { 374 Navigator.pop(context); 375 setState(() { 376 showProfile = true; 377 showNotifications = false; 378 + showExplore = false; 379 }); 380 }, 381 ), 382 ListTile( 383 + leading: const Icon(FontAwesomeIcons.list), 384 title: const Text('Logs'), 385 onTap: () { 386 Navigator.pop(context); ··· 409 ), 410 ), 411 title: Text( 412 + showNotifications 413 + ? 'Notifications' 414 + : showExplore 415 + ? 'Explore' 416 + : widget.title, 417 style: const TextStyle( 418 fontSize: 18, 419 fontWeight: FontWeight.w600, ··· 429 ), 430 body: Stack( 431 children: [ 432 + if (!showProfile && !showNotifications && !showExplore) 433 + _buildTimeline(), 434 + if (showExplore) 435 + Positioned.fill( 436 + child: Material( 437 + color: Theme.of( 438 + context, 439 + ).scaffoldBackgroundColor.withOpacity(0.98), 440 + child: SafeArea(child: Stack(children: [ExplorePage()])), 441 + ), 442 + ), 443 if (showNotifications) 444 Positioned.fill( 445 child: Material( ··· 472 bottomNavigationBar: BottomNavigationBar( 473 items: <BottomNavigationBarItem>[ 474 BottomNavigationBarItem( 475 + icon: Transform.translate( 476 + offset: const Offset(0, 10), 477 + child: FaIcon(FontAwesomeIcons.house), 478 + ), 479 + label: '', 480 + ), 481 + BottomNavigationBarItem( 482 + icon: Transform.translate( 483 + offset: const Offset(0, 10), 484 + child: const FaIcon(FontAwesomeIcons.magnifyingGlass), 485 + ), 486 label: '', 487 ), 488 BottomNavigationBarItem( 489 + icon: Transform.translate( 490 + offset: const Offset(0, 10), 491 + child: const FaIcon(FontAwesomeIcons.solidBell), 492 + ), 493 label: '', 494 ), 495 BottomNavigationBarItem( 496 icon: apiService.currentUser?.avatar != null 497 + ? Transform.translate( 498 + offset: const Offset(0, 10), 499 + child: Container( 500 + width: 32, 501 + height: 32, 502 + decoration: BoxDecoration( 503 + shape: BoxShape.circle, 504 + border: Border.all( 505 + color: _navIndex == 3 506 + ? Colors.lightBlue 507 + : Colors.transparent, 508 + width: 3, 509 + ), 510 ), 511 + child: ClipOval( 512 + child: Image.network( 513 + apiService.currentUser!.avatar, 514 + fit: BoxFit.cover, 515 + width: 24, 516 + height: 24, 517 + ), 518 ), 519 ), 520 ) 521 + : FaIcon( 522 + _navIndex == 3 523 + ? FontAwesomeIcons.solidUser 524 + : FontAwesomeIcons.user, 525 ), 526 label: '', 527 ), ··· 529 currentIndex: _navIndex, 530 selectedItemColor: Colors.lightBlue, 531 onTap: (index) { 532 + if (index == 2) { 533 setState(() { 534 showNotifications = true; 535 showProfile = false; 536 + showExplore = false; 537 }); 538 return; 539 } 540 + if (index == 3) { 541 setState(() { 542 showProfile = true; 543 showNotifications = false; 544 + showExplore = false; 545 + }); 546 + return; 547 + } 548 + if (index == 1) { 549 + setState(() { 550 + showExplore = true; 551 + showProfile = false; 552 + showNotifications = false; 553 }); 554 return; 555 } ··· 557 setState(() { 558 showProfile = false; 559 showNotifications = false; 560 + showExplore = false; 561 }); 562 return; 563 }
lib/log_page.dart lib/screens/log_page.dart
+2 -2
lib/main.dart
··· 4 import 'package:flutter_dotenv/flutter_dotenv.dart'; 5 import 'package:google_fonts/google_fonts.dart'; 6 import 'package:grain/app_logger.dart'; 7 - import 'package:grain/splash_page.dart'; 8 - import 'package:grain/home_page.dart'; 9 10 class AppConfig { 11 static late final String apiUrl;
··· 4 import 'package:flutter_dotenv/flutter_dotenv.dart'; 5 import 'package:google_fonts/google_fonts.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;
lib/notification.dart lib/models/notification.dart
+3 -3
lib/notifications_page.dart lib/screens/notifications_page.dart
··· 1 import 'package:flutter/material.dart'; 2 - import 'api.dart'; 3 - import 'notification.dart' as grain; 4 - import 'utils.dart'; 5 6 class NotificationsPage extends StatefulWidget { 7 const NotificationsPage({super.key});
··· 1 import 'package:flutter/material.dart'; 2 + import 'package:grain/api.dart'; 3 + import 'package:grain/models/notification.dart' as grain; 4 + import 'package:grain/utils.dart'; 5 6 class NotificationsPage extends StatefulWidget { 7 const NotificationsPage({super.key});
lib/profile.dart lib/models/profile.dart
+2 -2
lib/profile_page.dart lib/screens/profile_page.dart
··· 1 import 'package:flutter/material.dart'; 2 - import 'package:grain/gallery.dart'; 3 import 'package:grain/api.dart'; 4 - import 'package:grain/gallery_page.dart'; 5 6 class ProfilePage extends StatefulWidget { 7 final dynamic profile;
··· 1 import 'package:flutter/material.dart'; 2 + import 'package:grain/models/gallery.dart'; 3 import 'package:grain/api.dart'; 4 + import 'gallery_page.dart'; 5 6 class ProfilePage extends StatefulWidget { 7 final dynamic profile;
+100
lib/screens/explore_page.dart
···
··· 1 + import 'dart:async'; 2 + import 'package:flutter/material.dart'; 3 + import 'package:grain/api.dart'; 4 + import 'package:grain/models/profile.dart'; 5 + import 'profile_page.dart'; 6 + 7 + class ExplorePage extends StatelessWidget { 8 + const ExplorePage({super.key}); 9 + 10 + Future<List<Profile>> _delayedSearch(String query) async { 11 + await Future.delayed(const Duration(milliseconds: 500)); 12 + return apiService.searchActors(query); 13 + } 14 + 15 + @override 16 + Widget build(BuildContext context) { 17 + return Padding( 18 + padding: const EdgeInsets.all(16.0), 19 + child: SearchAnchor.bar( 20 + barHintText: 'Search for users', 21 + barShape: WidgetStateProperty.all( 22 + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), 23 + ), 24 + barElevation: WidgetStateProperty.all(0), 25 + suggestionsBuilder: (context, controller) { 26 + if (controller.text.isEmpty) { 27 + return []; 28 + } 29 + return [ 30 + FutureBuilder<List<Profile>>( 31 + future: _delayedSearch(controller.text), 32 + builder: (context, snapshot) { 33 + if (snapshot.connectionState == ConnectionState.waiting) { 34 + return const ListTile( 35 + title: Text('Searching...'), 36 + leading: SizedBox( 37 + width: 20, 38 + height: 20, 39 + child: CircularProgressIndicator(strokeWidth: 2), 40 + ), 41 + ); 42 + } 43 + if (snapshot.hasError) { 44 + return const ListTile(title: Text('Error searching users')); 45 + } 46 + final results = snapshot.data ?? []; 47 + if (results.isEmpty) { 48 + return const ListTile(title: Text('No users found')); 49 + } 50 + return Column( 51 + mainAxisSize: MainAxisSize.min, 52 + children: results.map((profile) { 53 + return ListTile( 54 + leading: profile.avatar.isNotEmpty 55 + ? CircleAvatar( 56 + backgroundImage: NetworkImage(profile.avatar), 57 + radius: 16, 58 + ) 59 + : const CircleAvatar( 60 + radius: 16, 61 + child: Icon( 62 + Icons.account_circle, 63 + color: Colors.grey, 64 + ), 65 + ), 66 + title: Text( 67 + profile.displayName.isNotEmpty 68 + ? profile.displayName 69 + : '@${profile.handle}', 70 + ), 71 + subtitle: profile.handle.isNotEmpty 72 + ? Text('@${profile.handle}') 73 + : null, 74 + onTap: () async { 75 + // Navigate to the profile page for the selected user 76 + final loadedProfile = await apiService.fetchProfile( 77 + did: profile.did, 78 + ); 79 + if (context.mounted) { 80 + Navigator.of(context).push( 81 + MaterialPageRoute( 82 + builder: (context) => ProfilePage( 83 + profile: loadedProfile, 84 + showAppBar: true, 85 + ), 86 + ), 87 + ); 88 + } 89 + }, 90 + ); 91 + }).toList(), 92 + ); 93 + }, 94 + ), 95 + ]; 96 + }, 97 + ), 98 + ); 99 + } 100 + }
lib/splash_page.dart lib/screens/splash_page.dart
+8
pubspec.lock
··· 211 description: flutter 212 source: sdk 213 version: "0.0.0" 214 freezed_annotation: 215 dependency: transitive 216 description:
··· 211 description: flutter 212 source: sdk 213 version: "0.0.0" 214 + font_awesome_flutter: 215 + dependency: "direct main" 216 + description: 217 + name: font_awesome_flutter 218 + sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a 219 + url: "https://pub.dev" 220 + source: hosted 221 + version: "10.8.0" 222 freezed_annotation: 223 dependency: transitive 224 description:
+1
pubspec.yaml
··· 61 google_fonts: ^6.2.1 62 logger: ^2.6.0 63 package_info_plus: ^8.3.0 64 65 dependency_overrides: 66 atproto_oauth:
··· 61 google_fonts: ^6.2.1 62 logger: ^2.6.0 63 package_info_plus: ^8.3.0 64 + font_awesome_flutter: ^10.8.0 65 66 dependency_overrides: 67 atproto_oauth: