this repo has no description

refactor: Enhance CommentsPage with photo selection and improve ProfilePage layout

+659 -299
+61 -49
lib/screens/comments_page.dart
··· 18 18 bool _error = false; 19 19 Gallery? _gallery; 20 20 List<Comment> _comments = []; 21 + GalleryPhoto? _selectedPhoto; 21 22 22 23 @override 23 24 void initState() { ··· 49 50 50 51 @override 51 52 Widget build(BuildContext context) { 52 - return Scaffold( 53 - appBar: AppBar( 54 - backgroundColor: Colors.white, 55 - surfaceTintColor: Colors.white, 56 - bottom: PreferredSize( 57 - preferredSize: const Size.fromHeight(1), 58 - child: Container(color: Theme.of(context).dividerColor, height: 1), 59 - ), 60 - title: const Text( 61 - 'Comments', 62 - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), 63 - ), 64 - ), 65 - body: _loading 66 - ? const Center(child: CircularProgressIndicator()) 67 - : _error 68 - ? const Center(child: Text('Failed to load comments.')) 69 - : ListView( 70 - padding: const EdgeInsets.all(12), 71 - children: [ 72 - if (_gallery != null) 73 - Text( 74 - _gallery!.title, 75 - style: const TextStyle( 76 - fontWeight: FontWeight.bold, 77 - fontSize: 18, 53 + return Stack( 54 + children: [ 55 + Scaffold( 56 + appBar: AppBar( 57 + backgroundColor: Colors.white, 58 + surfaceTintColor: Colors.white, 59 + bottom: PreferredSize( 60 + preferredSize: const Size.fromHeight(1), 61 + child: Container(color: Theme.of(context).dividerColor, height: 1), 62 + ), 63 + title: const Text( 64 + 'Comments', 65 + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), 66 + ), 67 + ), 68 + body: _loading 69 + ? const Center(child: CircularProgressIndicator()) 70 + : _error 71 + ? const Center(child: Text('Failed to load comments.')) 72 + : ListView( 73 + padding: const EdgeInsets.all(12), 74 + children: [ 75 + if (_gallery != null) 76 + Text( 77 + _gallery!.title, 78 + style: const TextStyle( 79 + fontWeight: FontWeight.bold, 80 + fontSize: 18, 81 + ), 82 + ), 83 + const SizedBox(height: 12), 84 + _CommentsList( 85 + comments: _comments, 86 + onPhotoTap: (photo) { 87 + setState(() { 88 + _selectedPhoto = photo; 89 + }); 90 + }, 91 + ), 92 + ], 78 93 ), 79 - ), 80 - const SizedBox(height: 12), 81 - _CommentsList(comments: _comments), 82 - ], 94 + ), 95 + if (_selectedPhoto != null) 96 + Positioned.fill( 97 + child: GalleryPhotoView( 98 + photos: [_selectedPhoto!], 99 + initialIndex: 0, 100 + onClose: () => setState(() => _selectedPhoto = null), 83 101 ), 102 + ), 103 + ], 84 104 ); 85 105 } 86 106 } 87 107 88 108 class _CommentsList extends StatelessWidget { 89 109 final List<Comment> comments; 90 - const _CommentsList({required this.comments}); 110 + final void Function(GalleryPhoto photo) onPhotoTap; 111 + const _CommentsList({required this.comments, required this.onPhotoTap}); 91 112 92 113 Map<String, List<Comment>> _groupReplies(List<Comment> comments) { 93 114 final repliesByParent = <String, List<Comment>>{}; ··· 113 134 child: Column( 114 135 crossAxisAlignment: CrossAxisAlignment.start, 115 136 children: [ 116 - _CommentTile(comment: comment), 137 + _CommentTile(comment: comment, onPhotoTap: onPhotoTap), 117 138 if (repliesByParent[comment.uri] != null) 118 139 ...repliesByParent[comment.uri]!.map( 119 140 (reply) => _buildCommentTree(reply, repliesByParent, depth + 1), ··· 139 160 140 161 class _CommentTile extends StatelessWidget { 141 162 final Comment comment; 142 - const _CommentTile({required this.comment}); 163 + final void Function(GalleryPhoto photo)? onPhotoTap; 164 + const _CommentTile({required this.comment, this.onPhotoTap}); 143 165 144 166 @override 145 167 Widget build(BuildContext context) { ··· 179 201 aspectRatio: 180 202 (comment.focus!.width > 0 && 181 203 comment.focus!.height > 0) 182 - ? comment.focus!.width / comment.focus!.height 183 - : 1.0, 204 + ? comment.focus!.width / comment.focus!.height 205 + : 1.0, 184 206 child: ClipRRect( 185 207 borderRadius: BorderRadius.circular(8), 186 208 child: GestureDetector( 187 - onTap: () { 188 - showDialog( 189 - context: context, 190 - barrierColor: Colors.black.withOpacity(0.85), 191 - builder: (context) => GalleryPhotoView( 192 - photos: [ 193 - GalleryPhoto( 209 + onTap: onPhotoTap != null 210 + ? () => onPhotoTap!(GalleryPhoto( 194 211 uri: comment.focus!.uri, 195 212 cid: comment.focus!.cid, 196 213 thumb: comment.focus!.thumb, ··· 198 215 alt: comment.focus!.alt, 199 216 width: comment.focus!.width, 200 217 height: comment.focus!.height, 201 - ), 202 - ], 203 - initialIndex: 0, 204 - onClose: () => Navigator.of(context).pop(), 205 - ), 206 - ); 207 - }, 218 + )) 219 + : null, 208 220 child: Image.network( 209 221 comment.focus!.thumb.isNotEmpty 210 222 ? comment.focus!.thumb
+390 -39
lib/screens/home_page.dart
··· 29 29 State<MyHomePage> createState() => _MyHomePageState(); 30 30 } 31 31 32 - class _MyHomePageState extends State<MyHomePage> { 32 + class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin { 33 33 bool showProfile = false; 34 34 bool showNotifications = false; 35 35 bool showExplore = false; 36 36 List<TimelineItem> _timeline = []; 37 37 bool _timelineLoading = true; 38 + int _tabIndex = 0; // 0 = Timeline, 1 = Following 39 + TabController? _tabController; 38 40 39 41 @override 40 42 void initState() { 41 43 super.initState(); 42 44 _fetchTimeline(); 45 + _initTabController(); 46 + } 47 + 48 + void _initTabController() { 49 + _tabController?.dispose(); 50 + _tabController = TabController(length: 2, vsync: this, initialIndex: _tabIndex); 51 + _tabController!.addListener(() { 52 + if (_tabController!.index != _tabIndex) { 53 + setState(() { 54 + _tabIndex = _tabController!.index; 55 + }); 56 + } 57 + }); 58 + } 59 + 60 + @override 61 + void didUpdateWidget(covariant MyHomePage oldWidget) { 62 + super.didUpdateWidget(oldWidget); 63 + if (_tabController == null || _tabController!.length != 2) { 64 + _initTabController(); 65 + } 66 + } 67 + 68 + @override 69 + void dispose() { 70 + _tabController?.dispose(); 71 + super.dispose(); 43 72 } 44 73 45 74 Future<void> _fetchTimeline() async { ··· 273 302 274 303 @override 275 304 Widget build(BuildContext context) { 305 + // Home page: show tabs 306 + if (!showProfile && !showNotifications && !showExplore) { 307 + if (_tabController == null) _initTabController(); 308 + return Scaffold( 309 + drawer: Drawer( 310 + child: ListView( 311 + padding: EdgeInsets.zero, 312 + children: [ 313 + DrawerHeader( 314 + decoration: BoxDecoration(color: Colors.white), 315 + child: Column( 316 + crossAxisAlignment: CrossAxisAlignment.start, 317 + mainAxisAlignment: MainAxisAlignment.center, 318 + children: [ 319 + CircleAvatar( 320 + radius: 22, // Smaller avatar 321 + backgroundImage: 322 + apiService.currentUser?.avatar != null && 323 + apiService.currentUser!.avatar.isNotEmpty 324 + ? NetworkImage(apiService.currentUser!.avatar) 325 + : null, 326 + backgroundColor: Colors.white, 327 + child: (apiService.currentUser == null || 328 + apiService.currentUser!.avatar.isEmpty) 329 + ? const Icon( 330 + Icons.account_circle, 331 + size: 32, 332 + color: Colors.grey, 333 + ) 334 + : null, 335 + ), 336 + const SizedBox(height: 6), 337 + Text( 338 + apiService.currentUser?.displayName ?? '', 339 + style: const TextStyle( 340 + color: Colors.black, 341 + fontSize: 15, // Smaller text 342 + fontWeight: FontWeight.bold, 343 + ), 344 + ), 345 + if (apiService.currentUser?.handle != null) 346 + Text( 347 + '@${apiService.currentUser!.handle}', 348 + style: const TextStyle( 349 + color: Colors.black54, 350 + fontSize: 11, // Smaller text 351 + ), 352 + ), 353 + const SizedBox(height: 6), 354 + Row( 355 + mainAxisAlignment: MainAxisAlignment.start, 356 + children: [ 357 + Text( 358 + (apiService.currentUser?.followersCount ?? 0) 359 + .toString(), 360 + style: const TextStyle( 361 + color: Colors.black, 362 + fontWeight: FontWeight.bold, 363 + fontSize: 13, 364 + ), 365 + ), 366 + const SizedBox(width: 4), 367 + const Text( 368 + 'Followers', 369 + style: TextStyle(color: Colors.black54, fontSize: 10), 370 + ), 371 + const SizedBox(width: 16), 372 + Text( 373 + (apiService.currentUser?.followsCount ?? 0).toString(), 374 + style: const TextStyle( 375 + color: Colors.black, 376 + fontWeight: FontWeight.bold, 377 + fontSize: 13, 378 + ), 379 + ), 380 + const SizedBox(width: 4), 381 + const Text( 382 + 'Following', 383 + style: TextStyle(color: Colors.black54, fontSize: 10), 384 + ), 385 + ], 386 + ), 387 + ], 388 + ), 389 + ), 390 + ListTile( 391 + leading: const Icon(FontAwesomeIcons.house), 392 + title: const Text('Home'), 393 + onTap: () { 394 + Navigator.pop(context); 395 + setState(() { 396 + showProfile = false; 397 + showNotifications = false; 398 + showExplore = false; 399 + }); 400 + }, 401 + ), 402 + ListTile( 403 + leading: const Icon(FontAwesomeIcons.magnifyingGlass), 404 + title: const Text('Explore'), 405 + onTap: () { 406 + Navigator.pop(context); 407 + setState(() { 408 + showExplore = true; 409 + showProfile = false; 410 + showNotifications = false; 411 + }); 412 + }, 413 + ), 414 + ListTile( 415 + leading: const Icon(FontAwesomeIcons.user), 416 + title: const Text('Profile'), 417 + onTap: () { 418 + Navigator.pop(context); 419 + setState(() { 420 + showProfile = true; 421 + showNotifications = false; 422 + showExplore = false; 423 + }); 424 + }, 425 + ), 426 + ListTile( 427 + leading: const Icon(FontAwesomeIcons.list), 428 + title: const Text('Logs'), 429 + onTap: () { 430 + Navigator.pop(context); 431 + Navigator.of(context).push( 432 + MaterialPageRoute(builder: (context) => const LogPage()), 433 + ); 434 + }, 435 + ), 436 + const SizedBox(height: 16), 437 + Padding( 438 + padding: const EdgeInsets.only(bottom: 16.0), 439 + child: Center(child: AppVersionText()), 440 + ), 441 + ], 442 + ), 443 + ), 444 + appBar: AppBar( 445 + backgroundColor: Colors.white, 446 + surfaceTintColor: Colors.white, 447 + bottom: PreferredSize( 448 + preferredSize: const Size.fromHeight(49), 449 + child: Container( 450 + color: Colors.white, 451 + child: TabBar( 452 + controller: _tabController, 453 + indicator: UnderlineTabIndicator( 454 + borderSide: const BorderSide( 455 + color: Color(0xFF0EA5E9), 456 + width: 3, 457 + ), 458 + insets: EdgeInsets.zero, 459 + ), 460 + indicatorSize: TabBarIndicatorSize.tab, 461 + labelColor: const Color(0xFF0EA5E9), 462 + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, 463 + labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), 464 + tabs: const [ 465 + Tab(text: 'Timeline'), 466 + Tab(text: 'Following'), 467 + ], 468 + ), 469 + ), 470 + ), 471 + leading: Builder( 472 + builder: (context) => IconButton( 473 + icon: const Icon(Icons.menu), 474 + onPressed: () => Scaffold.of(context).openDrawer(), 475 + ), 476 + ), 477 + title: Text( 478 + widget.title, 479 + style: const TextStyle( 480 + fontSize: 18, 481 + fontWeight: FontWeight.w600, 482 + ), 483 + ), 484 + actions: [ 485 + IconButton( 486 + icon: const Icon(Icons.logout), 487 + tooltip: 'Sign Out', 488 + onPressed: widget.onSignOut, 489 + ), 490 + ], 491 + ), 492 + body: TabBarView( 493 + controller: _tabController, 494 + physics: const NeverScrollableScrollPhysics(), 495 + children: [ 496 + _buildTimeline(), 497 + const Center(child: Text('Following timeline coming soon!')), 498 + ], 499 + ), 500 + bottomNavigationBar: Container( 501 + decoration: BoxDecoration( 502 + color: Colors.white, 503 + border: Border( 504 + top: BorderSide(color: Theme.of(context).dividerColor, width: 1), 505 + ), 506 + ), 507 + height: 42 + MediaQuery.of(context).padding.bottom, // Ultra-slim 508 + child: Row( 509 + mainAxisAlignment: MainAxisAlignment.spaceAround, 510 + children: [ 511 + Expanded( 512 + child: GestureDetector( 513 + behavior: HitTestBehavior.opaque, 514 + onTap: () { 515 + setState(() { 516 + showProfile = false; 517 + showNotifications = false; 518 + showExplore = false; 519 + }); 520 + }, 521 + child: SizedBox( 522 + height: 42 + MediaQuery.of(context).padding.bottom, 523 + child: Transform.translate( 524 + offset: const Offset(0, -10), 525 + child: Center( 526 + child: FaIcon( 527 + FontAwesomeIcons.house, 528 + size: 20, 529 + color: _navIndex == 0 530 + ? const Color(0xFF0EA5E9) 531 + : Theme.of(context).colorScheme.onSurfaceVariant, 532 + ), 533 + ), 534 + ), 535 + ), 536 + ), 537 + ), 538 + Expanded( 539 + child: GestureDetector( 540 + behavior: HitTestBehavior.opaque, 541 + onTap: () { 542 + setState(() { 543 + showExplore = true; 544 + showProfile = false; 545 + showNotifications = false; 546 + }); 547 + }, 548 + child: SizedBox( 549 + height: 42 + MediaQuery.of(context).padding.bottom, 550 + child: Transform.translate( 551 + offset: const Offset(0, -10), 552 + child: Center( 553 + child: FaIcon( 554 + FontAwesomeIcons.magnifyingGlass, 555 + size: 20, 556 + color: _navIndex == 1 557 + ? const Color(0xFF0EA5E9) 558 + : Theme.of(context).colorScheme.onSurfaceVariant, 559 + ), 560 + ), 561 + ), 562 + ), 563 + ), 564 + ), 565 + Expanded( 566 + child: GestureDetector( 567 + behavior: HitTestBehavior.opaque, 568 + onTap: () { 569 + setState(() { 570 + showNotifications = true; 571 + showProfile = false; 572 + showExplore = false; 573 + }); 574 + }, 575 + child: SizedBox( 576 + height: 42 + MediaQuery.of(context).padding.bottom, 577 + child: Transform.translate( 578 + offset: const Offset(0, -10), 579 + child: Center( 580 + child: FaIcon( 581 + FontAwesomeIcons.solidBell, 582 + size: 20, 583 + color: _navIndex == 2 584 + ? const Color(0xFF0EA5E9) 585 + : Theme.of(context).colorScheme.onSurfaceVariant, 586 + ), 587 + ), 588 + ), 589 + ), 590 + ), 591 + ), 592 + Expanded( 593 + child: GestureDetector( 594 + behavior: HitTestBehavior.opaque, 595 + onTap: () { 596 + setState(() { 597 + showProfile = true; 598 + showNotifications = false; 599 + showExplore = false; 600 + }); 601 + }, 602 + child: SizedBox( 603 + height: 42 + MediaQuery.of(context).padding.bottom, 604 + child: Transform.translate( 605 + offset: const Offset(0, -10), 606 + child: Center( 607 + child: apiService.currentUser?.avatar != null 608 + ? Container( 609 + width: 28, 610 + height: 28, 611 + alignment: Alignment.center, 612 + decoration: _navIndex == 3 613 + ? BoxDecoration( 614 + shape: BoxShape.circle, 615 + border: Border.all( 616 + color: const Color(0xFF0EA5E9), 617 + width: 2.2, 618 + ), 619 + ) 620 + : null, 621 + child: CircleAvatar( 622 + radius: 12, 623 + backgroundImage: NetworkImage( 624 + apiService.currentUser!.avatar, 625 + ), 626 + backgroundColor: Colors.transparent, 627 + ), 628 + ) 629 + : FaIcon( 630 + _navIndex == 3 631 + ? FontAwesomeIcons.solidUser 632 + : FontAwesomeIcons.user, 633 + size: 16, 634 + color: _navIndex == 3 635 + ? const Color(0xFF0EA5E9) 636 + : Theme.of( 637 + context, 638 + ).colorScheme.onSurfaceVariant, 639 + ), 640 + ), 641 + ), 642 + ), 643 + ), 644 + ), 645 + ], 646 + ), 647 + ), // End of bottomNavigationBar 648 + ); // End of Home page Scaffold 649 + } 650 + // Explore, Notifications, Profile: no tabs, no TabController 276 651 return Scaffold( 277 652 drawer: Drawer( 278 653 child: ListView( ··· 402 777 ); 403 778 }, 404 779 ), 405 - // Add more menu items here 406 780 const SizedBox(height: 16), 407 781 Padding( 408 782 padding: const EdgeInsets.only(bottom: 16.0), ··· 411 785 ], 412 786 ), 413 787 ), 414 - appBar: showProfile 415 - ? null 416 - : AppBar( 788 + appBar: (showExplore || showNotifications) 789 + ? AppBar( 417 790 backgroundColor: Colors.white, 418 791 surfaceTintColor: Colors.white, 419 - bottom: PreferredSize( 420 - preferredSize: const Size.fromHeight(1), 421 - child: Container( 422 - color: Theme.of(context).dividerColor, 423 - height: 1, 792 + elevation: 0.5, 793 + title: Text( 794 + showExplore ? 'Explore' : 'Notifications', 795 + style: const TextStyle( 796 + fontSize: 18, 797 + fontWeight: FontWeight.w600, 424 798 ), 425 799 ), 426 800 leading: Builder( ··· 429 803 onPressed: () => Scaffold.of(context).openDrawer(), 430 804 ), 431 805 ), 432 - title: Text( 433 - showNotifications 434 - ? 'Notifications' 435 - : showExplore 436 - ? 'Explore' 437 - : widget.title, 438 - style: const TextStyle( 439 - fontSize: 18, 440 - fontWeight: FontWeight.w600, 441 - ), 442 - ), 443 806 actions: [ 444 807 IconButton( 445 808 icon: const Icon(Icons.logout), ··· 447 810 onPressed: widget.onSignOut, 448 811 ), 449 812 ], 450 - ), 813 + ) 814 + : null, 451 815 body: Stack( 452 816 children: [ 453 - if (!showProfile && !showNotifications && !showExplore) 454 - _buildTimeline(), 455 817 if (showExplore) 456 818 Positioned.fill( 457 819 child: Material( 458 - color: Theme.of( 459 - context, 460 - ).scaffoldBackgroundColor.withOpacity(0.98), 820 + color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.98), 461 821 child: SafeArea(child: Stack(children: [ExplorePage()])), 462 822 ), 463 823 ), 464 824 if (showNotifications) 465 825 Positioned.fill( 466 826 child: Material( 467 - color: Theme.of( 468 - context, 469 - ).scaffoldBackgroundColor.withOpacity(0.98), 827 + color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.98), 470 828 child: SafeArea(child: Stack(children: [NotificationsPage()])), 471 829 ), 472 830 ), 473 831 if (showProfile) 474 832 Positioned.fill( 475 833 child: Material( 476 - color: Theme.of( 477 - context, 478 - ).scaffoldBackgroundColor.withOpacity(0.98), 834 + color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.98), 479 835 child: SafeArea( 480 836 child: Stack( 481 837 children: [ ··· 638 994 ], 639 995 ), 640 996 ), 641 - // floatingActionButton: FloatingActionButton( 642 - // onPressed: () {}, 643 - // tooltip: 'Action', 644 - // child: const Icon(Icons.add), 645 - // ), 646 - ); 997 + ); // End of fallback Scaffold 647 998 } 648 999 }
+2
lib/screens/notifications_page.dart
··· 28 28 }); 29 29 try { 30 30 final notifications = await apiService.getNotifications(); 31 + if (!mounted) return; 31 32 setState(() { 32 33 _notifications = notifications; 33 34 _loading = false; 34 35 }); 35 36 } catch (e) { 37 + if (!mounted) return; 36 38 setState(() { 37 39 _error = true; 38 40 _loading = false;
+206 -211
lib/screens/profile_page.dart
··· 65 65 appBar: widget.showAppBar 66 66 ? AppBar( 67 67 backgroundColor: Colors.white, 68 + surfaceTintColor: Colors.white, 68 69 bottom: PreferredSize( 69 70 preferredSize: const Size.fromHeight(1), 70 71 child: Container( ··· 80 81 borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), 81 82 child: SafeArea( 82 83 bottom: false, 83 - child: NestedScrollView( 84 - headerSliverBuilder: (context, innerBoxIsScrolled) => [ 85 - SliverToBoxAdapter( 86 - child: Padding( 87 - padding: const EdgeInsets.symmetric(horizontal: 8), 88 - child: Column( 89 - crossAxisAlignment: CrossAxisAlignment.start, 90 - children: [ 91 - const SizedBox(height: 16), 92 - if (profile.avatar != null) 93 - Align( 94 - alignment: Alignment.centerLeft, 95 - child: CircleAvatar( 96 - radius: 32, 97 - backgroundImage: NetworkImage(profile.avatar), 98 - ), 99 - ) 100 - else 101 - const Align( 102 - alignment: Alignment.centerLeft, 103 - child: Icon( 104 - Icons.account_circle, 105 - size: 64, 106 - color: Colors.grey, 107 - ), 108 - ), 109 - const SizedBox(height: 8), 110 - Text( 111 - profile.displayName ?? '', 112 - style: const TextStyle( 113 - fontSize: 28, 114 - fontWeight: FontWeight.w800, 115 - ), 116 - textAlign: TextAlign.left, 117 - ), 118 - const SizedBox(height: 2), 119 - Text( 120 - '@${profile.handle ?? ''}', 121 - style: TextStyle( 122 - fontSize: 14, 123 - color: Theme.of(context).brightness == Brightness.dark 124 - ? Colors.grey[400] 125 - : Colors.grey[700], 126 - ), 127 - textAlign: TextAlign.left, 128 - ), 129 - const SizedBox(height: 12), 130 - _ProfileStatsRow( 131 - followers: 132 - (profile.followersCount is int 133 - ? profile.followersCount 134 - : int.tryParse( 135 - profile.followersCount 136 - ?.toString() ?? 137 - '0', 138 - ) ?? 139 - 0) 140 - .toString(), 141 - following: 142 - (profile.followsCount is int 143 - ? profile.followsCount 144 - : int.tryParse( 145 - profile.followsCount?.toString() ?? 146 - '0', 147 - ) ?? 148 - 0) 149 - .toString(), 150 - galleries: 151 - (profile.galleryCount is int 152 - ? profile.galleryCount 153 - : int.tryParse( 154 - profile.galleryCount?.toString() ?? 155 - '0', 156 - ) ?? 157 - 0) 158 - .toString(), 159 - ), 160 - if ((profile.description ?? '').isNotEmpty) ...[ 161 - const SizedBox(height: 16), 162 - Text(profile.description, textAlign: TextAlign.left), 163 - ], 164 - const SizedBox(height: 24), 165 - ], 166 - ), 167 - ), 168 - ), 169 - if (_tabController != null) 170 - SliverToBoxAdapter( 171 - child: Padding( 172 - padding: const EdgeInsets.only(bottom: 8), 173 - child: Row( 174 - mainAxisAlignment: MainAxisAlignment.start, 175 - children: List.generate(2, (i) { 176 - final isSelected = _tabController!.index == i; 177 - final colorScheme = Theme.of(context).colorScheme; 178 - final bgColor = isSelected 179 - ? colorScheme.surfaceContainerHighest 180 - : Colors.transparent; 181 - final borderColor = isSelected 182 - ? colorScheme.surfaceContainerHighest 183 - : Colors.transparent; 184 - final textColor = isSelected 185 - ? colorScheme.onSurfaceVariant 186 - : colorScheme.onSurface; 187 - return Expanded( 188 - child: Padding( 189 - padding: const EdgeInsets.symmetric(horizontal: 4), 190 - child: InkWell( 191 - borderRadius: BorderRadius.circular(8), 192 - onTap: () { 193 - _tabController!.animateTo(i); 194 - setState(() {}); 195 - }, 196 - child: Container( 197 - height: 44, 198 - alignment: Alignment.center, 199 - decoration: BoxDecoration( 200 - color: bgColor, 201 - borderRadius: BorderRadius.circular(8), 202 - border: Border.all( 203 - color: borderColor, 204 - width: 1, 205 - ), 84 + child: Column( 85 + children: [ 86 + Expanded( 87 + child: NestedScrollView( 88 + headerSliverBuilder: (context, innerBoxIsScrolled) => [ 89 + SliverToBoxAdapter( 90 + child: Padding( 91 + padding: const EdgeInsets.symmetric(horizontal: 8), 92 + child: Column( 93 + crossAxisAlignment: CrossAxisAlignment.start, 94 + children: [ 95 + const SizedBox(height: 16), 96 + if (profile.avatar != null) 97 + Align( 98 + alignment: Alignment.centerLeft, 99 + child: CircleAvatar( 100 + radius: 32, 101 + backgroundImage: NetworkImage(profile.avatar), 206 102 ), 207 - child: Text( 208 - i == 0 ? 'Galleries' : 'Favs', 209 - style: TextStyle( 210 - fontWeight: isSelected 211 - ? FontWeight.w600 212 - : FontWeight.w500, 213 - fontSize: 15, 214 - color: textColor, 215 - ), 103 + ) 104 + else 105 + const Align( 106 + alignment: Alignment.centerLeft, 107 + child: Icon( 108 + Icons.account_circle, 109 + size: 64, 110 + color: Colors.grey, 216 111 ), 217 112 ), 113 + const SizedBox(height: 8), 114 + Text( 115 + profile.displayName ?? '', 116 + style: const TextStyle( 117 + fontSize: 28, 118 + fontWeight: FontWeight.w800, 119 + ), 120 + textAlign: TextAlign.left, 218 121 ), 219 - ), 220 - ); 221 - }), 122 + const SizedBox(height: 2), 123 + Text( 124 + '@${profile.handle ?? ''}', 125 + style: TextStyle( 126 + fontSize: 14, 127 + color: 128 + Theme.of(context).brightness == 129 + Brightness.dark 130 + ? Colors.grey[400] 131 + : Colors.grey[700], 132 + ), 133 + textAlign: TextAlign.left, 134 + ), 135 + const SizedBox(height: 12), 136 + _ProfileStatsRow( 137 + followers: 138 + (profile.followersCount is int 139 + ? profile.followersCount 140 + : int.tryParse( 141 + profile.followersCount 142 + ?.toString() ?? 143 + '0', 144 + ) ?? 145 + 0) 146 + .toString(), 147 + following: 148 + (profile.followsCount is int 149 + ? profile.followsCount 150 + : int.tryParse( 151 + profile.followsCount 152 + ?.toString() ?? 153 + '0', 154 + ) ?? 155 + 0) 156 + .toString(), 157 + galleries: 158 + (profile.galleryCount is int 159 + ? profile.galleryCount 160 + : int.tryParse( 161 + profile.galleryCount 162 + ?.toString() ?? 163 + '0', 164 + ) ?? 165 + 0) 166 + .toString(), 167 + ), 168 + if ((profile.description ?? '').isNotEmpty) ...[ 169 + const SizedBox(height: 16), 170 + Text( 171 + profile.description, 172 + textAlign: TextAlign.left, 173 + ), 174 + ], 175 + const SizedBox(height: 24), 176 + ], 177 + ), 178 + ), 222 179 ), 223 - ), 224 - ), 225 - ], 226 - body: _tabController == null 227 - ? Container() 228 - : TabBarView( 229 - controller: _tabController, 180 + ], 181 + body: Column( 230 182 children: [ 231 - // Galleries tab 232 - Padding( 233 - padding: EdgeInsets.zero, 234 - child: GridView.builder( 235 - padding: EdgeInsets.zero, 236 - gridDelegate: 237 - const SliverGridDelegateWithFixedCrossAxisCount( 238 - crossAxisCount: 3, 239 - childAspectRatio: 3 / 4, 240 - crossAxisSpacing: 2, 241 - mainAxisSpacing: 2, 242 - ), 243 - itemCount: _galleries.isNotEmpty 244 - ? _galleries.length 245 - : 24, 246 - itemBuilder: (context, index) { 247 - if (_galleries.isNotEmpty && 248 - index < _galleries.length) { 249 - final gallery = _galleries[index]; 250 - final hasPhoto = 251 - gallery.items.isNotEmpty && 252 - gallery.items[0].thumb.isNotEmpty; 253 - return GestureDetector( 254 - onTap: () { 255 - if (gallery.uri.isNotEmpty) { 256 - Navigator.of(context).push( 257 - MaterialPageRoute( 258 - builder: (context) => GalleryPage( 259 - uri: gallery.uri, 260 - currentUserDid: profile.did, 261 - ), 262 - ), 263 - ); 264 - } 265 - }, 266 - child: Container( 267 - decoration: BoxDecoration( 268 - color: Colors.grey[200], 269 - ), 270 - clipBehavior: Clip.antiAlias, 271 - child: hasPhoto 272 - ? Image.network( 273 - gallery.items[0].thumb, 274 - fit: BoxFit.cover, 275 - width: double.infinity, 276 - height: double.infinity, 277 - ) 278 - : Center( 279 - child: Text( 280 - gallery.title, 281 - style: const TextStyle( 282 - fontSize: 12, 283 - color: Colors.black54, 183 + Container( 184 + color: Colors.white, 185 + child: TabBar( 186 + controller: _tabController, 187 + indicator: UnderlineTabIndicator( 188 + borderSide: const BorderSide( 189 + color: Color(0xFF0EA5E9), 190 + width: 3, 191 + ), 192 + insets: EdgeInsets.zero, 193 + ), 194 + indicatorSize: TabBarIndicatorSize.tab, 195 + labelColor: const Color(0xFF0EA5E9), 196 + unselectedLabelColor: Theme.of( 197 + context, 198 + ).colorScheme.onSurfaceVariant, 199 + labelStyle: const TextStyle( 200 + fontWeight: FontWeight.w600, 201 + fontSize: 16, 202 + ), 203 + tabs: const [ 204 + Tab(text: 'Galleries'), 205 + Tab(text: 'Favs'), 206 + ], 207 + ), 208 + ), 209 + Expanded( 210 + child: _tabController == null 211 + ? Container() 212 + : TabBarView( 213 + controller: _tabController, 214 + children: [ 215 + // Galleries tab 216 + Padding( 217 + padding: EdgeInsets.zero, 218 + child: GridView.builder( 219 + padding: EdgeInsets.zero, 220 + gridDelegate: 221 + const SliverGridDelegateWithFixedCrossAxisCount( 222 + crossAxisCount: 3, 223 + childAspectRatio: 3 / 4, 224 + crossAxisSpacing: 2, 225 + mainAxisSpacing: 2, 226 + ), 227 + itemCount: _galleries.isNotEmpty 228 + ? _galleries.length 229 + : 24, 230 + itemBuilder: (context, index) { 231 + if (_galleries.isNotEmpty && 232 + index < _galleries.length) { 233 + final gallery = _galleries[index]; 234 + final hasPhoto = 235 + gallery.items.isNotEmpty && 236 + gallery.items[0].thumb.isNotEmpty; 237 + return GestureDetector( 238 + onTap: () { 239 + if (gallery.uri.isNotEmpty) { 240 + Navigator.of(context).push( 241 + MaterialPageRoute( 242 + builder: (context) => 243 + GalleryPage( 244 + uri: gallery.uri, 245 + currentUserDid: 246 + profile.did, 247 + ), 248 + ), 249 + ); 250 + } 251 + }, 252 + child: Container( 253 + decoration: BoxDecoration( 254 + color: Colors.grey[200], 255 + ), 256 + clipBehavior: Clip.antiAlias, 257 + child: hasPhoto 258 + ? Image.network( 259 + gallery.items[0].thumb, 260 + fit: BoxFit.cover, 261 + width: double.infinity, 262 + height: double.infinity, 263 + ) 264 + : Center( 265 + child: Text( 266 + gallery.title, 267 + style: const TextStyle( 268 + fontSize: 12, 269 + color: Colors.black54, 270 + ), 271 + textAlign: 272 + TextAlign.center, 273 + ), 274 + ), 284 275 ), 285 - textAlign: TextAlign.center, 276 + ); 277 + } 278 + return Container( 279 + decoration: BoxDecoration( 280 + color: Colors.grey[200], 281 + borderRadius: BorderRadius.circular( 282 + 8, 283 + ), 286 284 ), 287 - ), 288 - ), 289 - ); 290 - } 291 - return Container( 292 - decoration: BoxDecoration( 293 - color: Colors.grey[200], 294 - borderRadius: BorderRadius.circular(8), 285 + ); 286 + }, 287 + ), 288 + ), 289 + // Favs tab (placeholder) 290 + const Center( 291 + child: Text('No favorites yet'), 292 + ), // Replace with real content later 293 + ], 295 294 ), 296 - ); 297 - }, 298 - ), 299 295 ), 300 - // Favs tab (placeholder) 301 - const Center( 302 - child: Text('No favorites yet'), 303 - ), // Replace with real content later 304 296 ], 305 297 ), 298 + ), 299 + ), 300 + ], 306 301 ), 307 302 ), 308 303 ),