this repo has no description

feat: Implement TimelineItemWidget and BottomNavBar for improved UI structure

+417 -513
+73 -513
lib/screens/home_page.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:grain/api.dart'; 3 3 import 'package:grain/models/gallery.dart'; 4 - import 'package:grain/widgets/gallery_preview.dart'; 5 - import 'gallery_page.dart'; 6 - import 'comments_page.dart'; 7 - import 'profile_page.dart'; 8 - import 'package:grain/utils.dart'; 9 - import 'log_page.dart'; 4 + import 'package:grain/widgets/timeline_item.dart'; 10 5 import 'package:grain/widgets/app_version_text.dart'; 6 + import 'package:grain/widgets/bottom_nav_bar.dart'; 7 + import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 8 + import 'explore_page.dart'; 9 + import 'log_page.dart'; 11 10 import 'notifications_page.dart'; 12 - import 'explore_page.dart'; 13 - import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 11 + import 'profile_page.dart'; 14 12 15 13 class TimelineItem { 16 14 final Gallery gallery; ··· 167 165 SliverList( 168 166 delegate: SliverChildBuilderDelegate((context, index) { 169 167 final item = timeline[index]; 170 - final gallery = item.gallery; 171 - final actor = gallery.creator; 172 - final createdAt = gallery.createdAt; 173 - return Column( 174 - crossAxisAlignment: CrossAxisAlignment.start, 175 - children: [ 176 - Padding( 177 - padding: const EdgeInsets.symmetric( 178 - vertical: 8, 179 - horizontal: 8, 180 - ), 181 - child: Row( 182 - children: [ 183 - GestureDetector( 184 - onTap: () { 185 - if (actor != null) { 186 - Navigator.of(context).push( 187 - MaterialPageRoute( 188 - builder: (context) => ProfilePage( 189 - did: actor.did, 190 - showAppBar: true, 191 - ), 192 - ), 193 - ); 194 - } 195 - }, 196 - child: CircleAvatar( 197 - radius: 18, 198 - backgroundImage: 199 - actor?.avatar != null && 200 - actor!.avatar.isNotEmpty 201 - ? NetworkImage(actor.avatar) 202 - : null, 203 - backgroundColor: Colors.transparent, 204 - child: (actor == null || actor.avatar.isEmpty) 205 - ? const Icon( 206 - Icons.account_circle, 207 - size: 24, 208 - color: Colors.grey, 209 - ) 210 - : null, 211 - ), 212 - ), 213 - const SizedBox(width: 10), 214 - Expanded( 215 - child: Row( 216 - children: [ 217 - Flexible( 218 - child: Text( 219 - actor != null && 220 - actor.displayName.isNotEmpty 221 - ? actor.displayName 222 - : (actor != null 223 - ? '@${actor.handle}' 224 - : ''), 225 - style: const TextStyle( 226 - fontWeight: FontWeight.w600, 227 - fontSize: 16, 228 - ), 229 - overflow: TextOverflow.ellipsis, 230 - ), 231 - ), 232 - if (actor != null && actor.handle.isNotEmpty) 233 - Padding( 234 - padding: const EdgeInsets.only(left: 6), 235 - child: Text( 236 - '@${actor.handle}', 237 - style: TextStyle( 238 - fontSize: 14, 239 - color: Colors.grey[800], 240 - fontWeight: FontWeight.normal, 241 - ), 242 - overflow: TextOverflow.ellipsis, 243 - maxLines: 1, 244 - ), 245 - ), 246 - ], 247 - ), 248 - ), 249 - Text( 250 - formatRelativeTime(createdAt ?? ''), 251 - style: const TextStyle( 252 - fontSize: 12, 253 - color: Colors.grey, 254 - ), 255 - ), 256 - ], 257 - ), 258 - ), 259 - if (gallery.items.isNotEmpty) 260 - GestureDetector( 261 - onTap: () { 262 - if (gallery.uri.isNotEmpty) { 263 - Navigator.of(context).push( 264 - MaterialPageRoute( 265 - builder: (context) => GalleryPage( 266 - uri: gallery.uri, 267 - currentUserDid: apiService.currentUser?.did, 268 - ), 269 - ), 270 - ); 271 - } 272 - }, 273 - child: GalleryPreview(gallery: gallery), 274 - ) 275 - else 276 - const SizedBox.shrink(), 277 - if (gallery.title.isNotEmpty) 278 - Padding( 279 - padding: const EdgeInsets.only( 280 - top: 8, 281 - left: 8, 282 - right: 8, 283 - ), 284 - child: Text( 285 - gallery.title, 286 - style: const TextStyle(fontWeight: FontWeight.w600), 287 - ), 288 - ), 289 - if (gallery.description.isNotEmpty) 290 - Padding( 291 - padding: const EdgeInsets.only( 292 - top: 4, 293 - left: 8, 294 - right: 8, 295 - ), 296 - child: Text( 297 - gallery.description, 298 - style: const TextStyle( 299 - fontSize: 13, 300 - color: Colors.black54, 301 - ), 302 - ), 303 - ), 304 - const SizedBox(height: 8), 305 - Padding( 306 - padding: const EdgeInsets.only( 307 - top: 12, 308 - bottom: 12, 309 - left: 12, 310 - right: 12, 311 - ), 312 - child: Row( 313 - children: [ 314 - GestureDetector( 315 - child: Padding( 316 - padding: const EdgeInsets.only(right: 12), 317 - child: Icon( 318 - size: 18, 319 - gallery.viewer != null && 320 - gallery.viewer!['fav'] != null 321 - ? FontAwesomeIcons.solidHeart 322 - : FontAwesomeIcons.heart, 323 - color: 324 - gallery.viewer != null && 325 - gallery.viewer!['fav'] != null 326 - ? Color(0xFFEC4899) 327 - : Colors.black54, 328 - ), 329 - ), 330 - onTap: () {}, 331 - ), 332 - if (gallery.favCount != null) 333 - Padding( 334 - padding: const EdgeInsets.only(right: 12), 335 - child: Text( 336 - gallery.favCount.toString(), 337 - style: const TextStyle( 338 - fontSize: 14, 339 - color: Colors.black54, 340 - ), 341 - ), 342 - ), 343 - GestureDetector( 344 - onTap: () { 345 - Navigator.of(context).push( 346 - MaterialPageRoute( 347 - builder: (context) => 348 - CommentsPage(galleryUri: gallery.uri), 349 - ), 350 - ); 351 - }, 352 - child: Padding( 353 - padding: const EdgeInsets.only( 354 - left: 12, 355 - right: 12, 356 - ), 357 - child: Icon( 358 - FontAwesomeIcons.comment, 359 - size: 18, 360 - color: Colors.black54, 361 - ), 362 - ), 363 - ), 364 - if (gallery.commentCount != null) 365 - Text( 366 - gallery.commentCount.toString(), 367 - style: const TextStyle( 368 - fontSize: 14, 369 - color: Colors.black54, 370 - ), 371 - ), 372 - ], 373 - ), 374 - ), 375 - ], 376 - ); 168 + return TimelineItemWidget(gallery: item.gallery); 377 169 }, childCount: timeline.length), 378 170 ), 379 171 ], ··· 603 395 ], 604 396 ), 605 397 ), 606 - bottomNavigationBar: Container( 607 - decoration: BoxDecoration( 608 - color: Colors.white, 609 - border: Border( 610 - top: BorderSide(color: Theme.of(context).dividerColor, width: 1), 611 - ), 612 - ), 613 - height: 42 + MediaQuery.of(context).padding.bottom, // Ultra-slim 614 - child: Row( 615 - mainAxisAlignment: MainAxisAlignment.spaceAround, 616 - children: [ 617 - Expanded( 618 - child: GestureDetector( 619 - behavior: HitTestBehavior.opaque, 620 - onTap: () { 621 - setState(() { 622 - showProfile = false; 623 - showNotifications = false; 624 - showExplore = false; 625 - }); 626 - }, 627 - child: SizedBox( 628 - height: 42 + MediaQuery.of(context).padding.bottom, 629 - child: Transform.translate( 630 - offset: const Offset(0, -10), 631 - child: Center( 632 - child: FaIcon( 633 - FontAwesomeIcons.house, 634 - size: 20, 635 - color: _navIndex == 0 636 - ? const Color(0xFF0EA5E9) 637 - : Theme.of(context).colorScheme.onSurfaceVariant, 638 - ), 639 - ), 640 - ), 641 - ), 642 - ), 643 - ), 644 - Expanded( 645 - child: GestureDetector( 646 - behavior: HitTestBehavior.opaque, 647 - onTap: () { 648 - setState(() { 649 - showExplore = true; 650 - showProfile = false; 651 - showNotifications = false; 652 - }); 653 - }, 654 - child: SizedBox( 655 - height: 42 + MediaQuery.of(context).padding.bottom, 656 - child: Transform.translate( 657 - offset: const Offset(0, -10), 658 - child: Center( 659 - child: FaIcon( 660 - FontAwesomeIcons.magnifyingGlass, 661 - size: 20, 662 - color: _navIndex == 1 663 - ? const Color(0xFF0EA5E9) 664 - : Theme.of(context).colorScheme.onSurfaceVariant, 665 - ), 666 - ), 667 - ), 668 - ), 669 - ), 670 - ), 671 - Expanded( 672 - child: GestureDetector( 673 - behavior: HitTestBehavior.opaque, 674 - onTap: () { 675 - setState(() { 676 - showNotifications = true; 677 - showProfile = false; 678 - showExplore = false; 679 - }); 680 - }, 681 - child: SizedBox( 682 - height: 42 + MediaQuery.of(context).padding.bottom, 683 - child: Transform.translate( 684 - offset: const Offset(0, -10), 685 - child: Center( 686 - child: FaIcon( 687 - FontAwesomeIcons.solidBell, 688 - size: 20, 689 - color: _navIndex == 2 690 - ? const Color(0xFF0EA5E9) 691 - : Theme.of(context).colorScheme.onSurfaceVariant, 692 - ), 693 - ), 694 - ), 695 - ), 696 - ), 697 - ), 698 - Expanded( 699 - child: GestureDetector( 700 - behavior: HitTestBehavior.opaque, 701 - onTap: () { 702 - setState(() { 703 - showProfile = true; 704 - showNotifications = false; 705 - showExplore = false; 706 - }); 707 - }, 708 - child: SizedBox( 709 - height: 42 + MediaQuery.of(context).padding.bottom, 710 - child: Transform.translate( 711 - offset: const Offset(0, -10), 712 - child: Center( 713 - child: apiService.currentUser?.avatar != null 714 - ? Container( 715 - width: 28, 716 - height: 28, 717 - alignment: Alignment.center, 718 - decoration: _navIndex == 3 719 - ? BoxDecoration( 720 - shape: BoxShape.circle, 721 - border: Border.all( 722 - color: const Color(0xFF0EA5E9), 723 - width: 2.2, 724 - ), 725 - ) 726 - : null, 727 - child: CircleAvatar( 728 - radius: 12, 729 - backgroundImage: NetworkImage( 730 - apiService.currentUser!.avatar, 731 - ), 732 - backgroundColor: Colors.transparent, 733 - ), 734 - ) 735 - : FaIcon( 736 - _navIndex == 3 737 - ? FontAwesomeIcons.solidUser 738 - : FontAwesomeIcons.user, 739 - size: 16, 740 - color: _navIndex == 3 741 - ? const Color(0xFF0EA5E9) 742 - : Theme.of( 743 - context, 744 - ).colorScheme.onSurfaceVariant, 745 - ), 746 - ), 747 - ), 748 - ), 749 - ), 750 - ), 751 - ], 752 - ), 753 - ), // End of bottomNavigationBar 754 - ); // End of Home page Scaffold 398 + bottomNavigationBar: BottomNavBar( 399 + navIndex: _navIndex, 400 + onHome: () { 401 + setState(() { 402 + showProfile = false; 403 + showNotifications = false; 404 + showExplore = false; 405 + }); 406 + }, 407 + onExplore: () { 408 + setState(() { 409 + showProfile = false; 410 + showNotifications = false; 411 + showExplore = true; 412 + }); 413 + }, 414 + onNotifications: () { 415 + setState(() { 416 + showProfile = false; 417 + showNotifications = true; 418 + showExplore = false; 419 + }); 420 + }, 421 + onProfile: () { 422 + setState(() { 423 + showProfile = true; 424 + showNotifications = false; 425 + showExplore = false; 426 + }); 427 + }, 428 + avatarUrl: apiService.currentUser?.avatar, 429 + ), 430 + ); 755 431 } 756 432 // Explore, Notifications, Profile: no tabs, no TabController 757 433 return Scaffold( ··· 958 634 ), 959 635 ], 960 636 ), 961 - bottomNavigationBar: Container( 962 - decoration: BoxDecoration( 963 - color: Colors.white, 964 - border: Border( 965 - top: BorderSide(color: Theme.of(context).dividerColor, width: 1), 966 - ), 967 - ), 968 - height: 42 + MediaQuery.of(context).padding.bottom, // Ultra-slim 969 - child: Row( 970 - mainAxisAlignment: MainAxisAlignment.spaceAround, 971 - children: [ 972 - Expanded( 973 - child: GestureDetector( 974 - behavior: HitTestBehavior.opaque, 975 - onTap: () { 976 - setState(() { 977 - showProfile = false; 978 - showNotifications = false; 979 - showExplore = false; 980 - }); 981 - }, 982 - child: SizedBox( 983 - height: 42 + MediaQuery.of(context).padding.bottom, 984 - child: Transform.translate( 985 - offset: const Offset(0, -10), 986 - child: Center( 987 - child: FaIcon( 988 - FontAwesomeIcons.house, 989 - size: 20, 990 - color: _navIndex == 0 991 - ? const Color(0xFF0EA5E9) 992 - : Theme.of(context).colorScheme.onSurfaceVariant, 993 - ), 994 - ), 995 - ), 996 - ), 997 - ), 998 - ), 999 - Expanded( 1000 - child: GestureDetector( 1001 - behavior: HitTestBehavior.opaque, 1002 - onTap: () { 1003 - setState(() { 1004 - showExplore = true; 1005 - showProfile = false; 1006 - showNotifications = false; 1007 - }); 1008 - }, 1009 - child: SizedBox( 1010 - height: 42 + MediaQuery.of(context).padding.bottom, 1011 - child: Transform.translate( 1012 - offset: const Offset(0, -10), 1013 - child: Center( 1014 - child: FaIcon( 1015 - FontAwesomeIcons.magnifyingGlass, 1016 - size: 20, 1017 - color: _navIndex == 1 1018 - ? const Color(0xFF0EA5E9) 1019 - : Theme.of(context).colorScheme.onSurfaceVariant, 1020 - ), 1021 - ), 1022 - ), 1023 - ), 1024 - ), 1025 - ), 1026 - Expanded( 1027 - child: GestureDetector( 1028 - behavior: HitTestBehavior.opaque, 1029 - onTap: () { 1030 - setState(() { 1031 - showNotifications = true; 1032 - showProfile = false; 1033 - showExplore = false; 1034 - }); 1035 - }, 1036 - child: SizedBox( 1037 - height: 42 + MediaQuery.of(context).padding.bottom, 1038 - child: Transform.translate( 1039 - offset: const Offset(0, -10), 1040 - child: Center( 1041 - child: FaIcon( 1042 - FontAwesomeIcons.solidBell, 1043 - size: 20, 1044 - color: _navIndex == 2 1045 - ? const Color(0xFF0EA5E9) 1046 - : Theme.of(context).colorScheme.onSurfaceVariant, 1047 - ), 1048 - ), 1049 - ), 1050 - ), 1051 - ), 1052 - ), 1053 - Expanded( 1054 - child: GestureDetector( 1055 - behavior: HitTestBehavior.opaque, 1056 - onTap: () { 1057 - setState(() { 1058 - showProfile = true; 1059 - showNotifications = false; 1060 - showExplore = false; 1061 - }); 1062 - }, 1063 - child: SizedBox( 1064 - height: 42 + MediaQuery.of(context).padding.bottom, 1065 - child: Transform.translate( 1066 - offset: const Offset(0, -10), 1067 - child: Center( 1068 - child: apiService.currentUser?.avatar != null 1069 - ? Container( 1070 - width: 28, 1071 - height: 28, 1072 - alignment: Alignment.center, 1073 - decoration: _navIndex == 3 1074 - ? BoxDecoration( 1075 - shape: BoxShape.circle, 1076 - border: Border.all( 1077 - color: const Color(0xFF0EA5E9), 1078 - width: 2.2, 1079 - ), 1080 - ) 1081 - : null, 1082 - child: CircleAvatar( 1083 - radius: 12, 1084 - backgroundImage: NetworkImage( 1085 - apiService.currentUser!.avatar, 1086 - ), 1087 - backgroundColor: Colors.transparent, 1088 - ), 1089 - ) 1090 - : FaIcon( 1091 - _navIndex == 3 1092 - ? FontAwesomeIcons.solidUser 1093 - : FontAwesomeIcons.user, 1094 - size: 16, 1095 - color: _navIndex == 3 1096 - ? const Color(0xFF0EA5E9) 1097 - : Theme.of( 1098 - context, 1099 - ).colorScheme.onSurfaceVariant, 1100 - ), 1101 - ), 1102 - ), 1103 - ), 1104 - ), 1105 - ), 1106 - ], 1107 - ), 1108 - ), // End of bottomNavigationBar 1109 - ); // End of fallback Scaffold 637 + bottomNavigationBar: BottomNavBar( 638 + navIndex: _navIndex, 639 + onHome: () { 640 + setState(() { 641 + showProfile = false; 642 + showNotifications = false; 643 + showExplore = false; 644 + }); 645 + }, 646 + onExplore: () { 647 + setState(() { 648 + showProfile = false; 649 + showNotifications = false; 650 + showExplore = true; 651 + }); 652 + }, 653 + onNotifications: () { 654 + setState(() { 655 + showProfile = false; 656 + showNotifications = true; 657 + showExplore = false; 658 + }); 659 + }, 660 + onProfile: () { 661 + setState(() { 662 + showProfile = true; 663 + showNotifications = false; 664 + showExplore = false; 665 + }); 666 + }, 667 + avatarUrl: apiService.currentUser?.avatar, 668 + ), 669 + ); 1110 670 } 1111 671 } // End of _MyHomePageState and file
+147
lib/widgets/bottom_nav_bar.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 3 + 4 + class BottomNavBar extends StatelessWidget { 5 + final int navIndex; 6 + final VoidCallback onHome; 7 + final VoidCallback onExplore; 8 + final VoidCallback onNotifications; 9 + final VoidCallback onProfile; 10 + final String? avatarUrl; 11 + 12 + const BottomNavBar({ 13 + super.key, 14 + required this.navIndex, 15 + required this.onHome, 16 + required this.onExplore, 17 + required this.onNotifications, 18 + required this.onProfile, 19 + this.avatarUrl, 20 + }); 21 + 22 + @override 23 + Widget build(BuildContext context) { 24 + return Container( 25 + decoration: BoxDecoration( 26 + color: Colors.white, 27 + border: Border( 28 + top: BorderSide(color: Theme.of(context).dividerColor, width: 1), 29 + ), 30 + ), 31 + height: 42 + MediaQuery.of(context).padding.bottom, 32 + child: Row( 33 + mainAxisAlignment: MainAxisAlignment.spaceAround, 34 + children: [ 35 + Expanded( 36 + child: GestureDetector( 37 + behavior: HitTestBehavior.opaque, 38 + onTap: onHome, 39 + child: SizedBox( 40 + height: 42 + MediaQuery.of(context).padding.bottom, 41 + child: Transform.translate( 42 + offset: const Offset(0, -10), 43 + child: Center( 44 + child: FaIcon( 45 + FontAwesomeIcons.house, 46 + size: 20, 47 + color: navIndex == 0 48 + ? const Color(0xFF0EA5E9) 49 + : Theme.of(context).colorScheme.onSurfaceVariant, 50 + ), 51 + ), 52 + ), 53 + ), 54 + ), 55 + ), 56 + Expanded( 57 + child: GestureDetector( 58 + behavior: HitTestBehavior.opaque, 59 + onTap: onExplore, 60 + child: SizedBox( 61 + height: 42 + MediaQuery.of(context).padding.bottom, 62 + child: Transform.translate( 63 + offset: const Offset(0, -10), 64 + child: Center( 65 + child: FaIcon( 66 + FontAwesomeIcons.magnifyingGlass, 67 + size: 20, 68 + color: navIndex == 1 69 + ? const Color(0xFF0EA5E9) 70 + : Theme.of(context).colorScheme.onSurfaceVariant, 71 + ), 72 + ), 73 + ), 74 + ), 75 + ), 76 + ), 77 + Expanded( 78 + child: GestureDetector( 79 + behavior: HitTestBehavior.opaque, 80 + onTap: onNotifications, 81 + child: SizedBox( 82 + height: 42 + MediaQuery.of(context).padding.bottom, 83 + child: Transform.translate( 84 + offset: const Offset(0, -10), 85 + child: Center( 86 + child: FaIcon( 87 + FontAwesomeIcons.solidBell, 88 + size: 20, 89 + color: navIndex == 2 90 + ? const Color(0xFF0EA5E9) 91 + : Theme.of(context).colorScheme.onSurfaceVariant, 92 + ), 93 + ), 94 + ), 95 + ), 96 + ), 97 + ), 98 + Expanded( 99 + child: GestureDetector( 100 + behavior: HitTestBehavior.opaque, 101 + onTap: onProfile, 102 + child: SizedBox( 103 + height: 42 + MediaQuery.of(context).padding.bottom, 104 + child: Transform.translate( 105 + offset: const Offset(0, -10), 106 + child: Center( 107 + child: avatarUrl != null && avatarUrl!.isNotEmpty 108 + ? Container( 109 + width: 28, 110 + height: 28, 111 + alignment: Alignment.center, 112 + decoration: navIndex == 3 113 + ? BoxDecoration( 114 + shape: BoxShape.circle, 115 + border: Border.all( 116 + color: const Color(0xFF0EA5E9), 117 + width: 2.2, 118 + ), 119 + ) 120 + : null, 121 + child: CircleAvatar( 122 + radius: 12, 123 + backgroundImage: NetworkImage(avatarUrl!), 124 + backgroundColor: Colors.transparent, 125 + ), 126 + ) 127 + : FaIcon( 128 + navIndex == 3 129 + ? FontAwesomeIcons.solidUser 130 + : FontAwesomeIcons.user, 131 + size: 16, 132 + color: navIndex == 3 133 + ? const Color(0xFF0EA5E9) 134 + : Theme.of( 135 + context, 136 + ).colorScheme.onSurfaceVariant, 137 + ), 138 + ), 139 + ), 140 + ), 141 + ), 142 + ), 143 + ], 144 + ), 145 + ); 146 + } 147 + }
+197
lib/widgets/timeline_item.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:grain/models/gallery.dart'; 3 + import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 4 + import 'package:grain/widgets/gallery_preview.dart'; 5 + import '../screens/gallery_page.dart'; 6 + import '../screens/comments_page.dart'; 7 + import '../screens/profile_page.dart'; 8 + import 'package:grain/api.dart'; 9 + import 'package:grain/utils.dart'; 10 + 11 + class TimelineItemWidget extends StatelessWidget { 12 + final Gallery gallery; 13 + final VoidCallback? onProfileTap; 14 + const TimelineItemWidget({ 15 + super.key, 16 + required this.gallery, 17 + this.onProfileTap, 18 + }); 19 + 20 + @override 21 + Widget build(BuildContext context) { 22 + final actor = gallery.creator; 23 + final createdAt = gallery.createdAt; 24 + return Column( 25 + crossAxisAlignment: CrossAxisAlignment.start, 26 + children: [ 27 + Padding( 28 + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), 29 + child: Row( 30 + children: [ 31 + GestureDetector( 32 + onTap: 33 + onProfileTap ?? 34 + () { 35 + if (actor != null) { 36 + Navigator.of(context).push( 37 + MaterialPageRoute( 38 + builder: (context) => 39 + ProfilePage(did: actor.did, showAppBar: true), 40 + ), 41 + ); 42 + } 43 + }, 44 + child: CircleAvatar( 45 + radius: 18, 46 + backgroundImage: 47 + actor?.avatar != null && actor!.avatar.isNotEmpty 48 + ? NetworkImage(actor.avatar) 49 + : null, 50 + backgroundColor: Colors.transparent, 51 + child: (actor == null || actor.avatar.isEmpty) 52 + ? const Icon( 53 + Icons.account_circle, 54 + size: 24, 55 + color: Colors.grey, 56 + ) 57 + : null, 58 + ), 59 + ), 60 + const SizedBox(width: 10), 61 + Expanded( 62 + child: Row( 63 + children: [ 64 + Flexible( 65 + child: Text( 66 + actor != null && actor.displayName.isNotEmpty 67 + ? actor.displayName 68 + : (actor != null ? '@${actor.handle}' : ''), 69 + style: const TextStyle( 70 + fontWeight: FontWeight.w600, 71 + fontSize: 16, 72 + ), 73 + overflow: TextOverflow.ellipsis, 74 + ), 75 + ), 76 + if (actor != null && actor.handle.isNotEmpty) 77 + Padding( 78 + padding: const EdgeInsets.only(left: 6), 79 + child: Text( 80 + '@${actor.handle}', 81 + style: TextStyle( 82 + fontSize: 14, 83 + color: Colors.grey[800], 84 + fontWeight: FontWeight.normal, 85 + ), 86 + overflow: TextOverflow.ellipsis, 87 + maxLines: 1, 88 + ), 89 + ), 90 + ], 91 + ), 92 + ), 93 + Text( 94 + formatRelativeTime(createdAt ?? ''), 95 + style: const TextStyle(fontSize: 12, color: Colors.grey), 96 + ), 97 + ], 98 + ), 99 + ), 100 + if (gallery.items.isNotEmpty) 101 + GestureDetector( 102 + onTap: () { 103 + if (gallery.uri.isNotEmpty) { 104 + Navigator.of(context).push( 105 + MaterialPageRoute( 106 + builder: (context) => GalleryPage( 107 + uri: gallery.uri, 108 + currentUserDid: apiService.currentUser?.did, 109 + ), 110 + ), 111 + ); 112 + } 113 + }, 114 + child: GalleryPreview(gallery: gallery), 115 + ) 116 + else 117 + const SizedBox.shrink(), 118 + if (gallery.title.isNotEmpty) 119 + Padding( 120 + padding: const EdgeInsets.only(top: 8, left: 8, right: 8), 121 + child: Text( 122 + gallery.title, 123 + style: const TextStyle(fontWeight: FontWeight.w600), 124 + ), 125 + ), 126 + if (gallery.description.isNotEmpty) 127 + Padding( 128 + padding: const EdgeInsets.only(top: 4, left: 8, right: 8), 129 + child: Text( 130 + gallery.description, 131 + style: const TextStyle(fontSize: 13, color: Colors.black54), 132 + ), 133 + ), 134 + const SizedBox(height: 8), 135 + Padding( 136 + padding: const EdgeInsets.only( 137 + top: 12, 138 + bottom: 12, 139 + left: 12, 140 + right: 12, 141 + ), 142 + child: Row( 143 + children: [ 144 + GestureDetector( 145 + child: Padding( 146 + padding: const EdgeInsets.only(right: 12), 147 + child: Icon( 148 + size: 18, 149 + gallery.viewer != null && gallery.viewer!['fav'] != null 150 + ? FontAwesomeIcons.solidHeart 151 + : FontAwesomeIcons.heart, 152 + color: 153 + gallery.viewer != null && gallery.viewer!['fav'] != null 154 + ? Color(0xFFEC4899) 155 + : Colors.black54, 156 + ), 157 + ), 158 + onTap: () {}, 159 + ), 160 + if (gallery.favCount != null) 161 + Padding( 162 + padding: const EdgeInsets.only(right: 12), 163 + child: Text( 164 + gallery.favCount.toString(), 165 + style: const TextStyle(fontSize: 14, color: Colors.black54), 166 + ), 167 + ), 168 + GestureDetector( 169 + onTap: () { 170 + Navigator.of(context).push( 171 + MaterialPageRoute( 172 + builder: (context) => 173 + CommentsPage(galleryUri: gallery.uri), 174 + ), 175 + ); 176 + }, 177 + child: Padding( 178 + padding: const EdgeInsets.only(left: 12, right: 12), 179 + child: Icon( 180 + FontAwesomeIcons.comment, 181 + size: 18, 182 + color: Colors.black54, 183 + ), 184 + ), 185 + ), 186 + if (gallery.commentCount != null) 187 + Text( 188 + gallery.commentCount.toString(), 189 + style: const TextStyle(fontSize: 14, color: Colors.black54), 190 + ), 191 + ], 192 + ), 193 + ), 194 + ], 195 + ); 196 + } 197 + }