[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

profile scrolling

+466 -349
+454 -337
lib/screens/profile_screen.dart
··· 13 13 import '../services/profile_service.dart'; 14 14 import 'auth_prompt_screen.dart'; 15 15 import 'package:cached_network_image/cached_network_image.dart'; 16 + import '../widgets/profile/video_thumbnail.dart'; 16 17 17 18 class ProfileScreen extends StatefulWidget { 18 19 final String? did; // DID of the profile to show, null means current user ··· 237 238 backgroundColor: isDarkMode ? AppColors.deepPurple : AppColors.background, 238 239 ), 239 240 child: SafeArea( 240 - bottom: false, // Don't add padding at the bottom for the tab bar 241 - child: Column( 242 - children: [ 241 + bottom: false, // Don't add padding at the bottom 242 + child: CustomScrollView( 243 + slivers: [ 243 244 // Profile info - horizontal layout 244 - Padding( 245 - padding: const EdgeInsets.all(16.0), 246 - child: Column( 247 - crossAxisAlignment: CrossAxisAlignment.start, 248 - children: [ 249 - // Profile image and stats in a row 250 - Row( 251 - crossAxisAlignment: CrossAxisAlignment.center, 252 - children: [ 253 - // Profile image with + button 254 - Stack( 255 - children: [ 256 - Container( 257 - width: 90, 258 - height: 90, 259 - decoration: BoxDecoration( 260 - color: isDarkMode ? AppColors.darkPurple : AppColors.lightLavender, 261 - shape: BoxShape.circle, 262 - border: Border.all( 245 + SliverToBoxAdapter( 246 + child: Padding( 247 + padding: const EdgeInsets.all(16.0), 248 + child: Column( 249 + crossAxisAlignment: CrossAxisAlignment.start, 250 + children: [ 251 + // Profile image and stats in a row 252 + Row( 253 + crossAxisAlignment: CrossAxisAlignment.center, 254 + children: [ 255 + // Profile image with + button 256 + Stack( 257 + children: [ 258 + Container( 259 + width: 90, 260 + height: 90, 261 + decoration: BoxDecoration( 263 262 color: isDarkMode ? AppColors.darkPurple : AppColors.lightLavender, 264 - width: 2, 263 + shape: BoxShape.circle, 264 + border: Border.all( 265 + color: isDarkMode ? AppColors.darkPurple : AppColors.lightLavender, 266 + width: 2, 267 + ), 265 268 ), 266 - ), 267 - child: Center( 268 - child: avatar != null && avatar.isNotEmpty 269 - ? ClipOval( 270 - child: CachedNetworkImage( 271 - imageUrl: avatar, 272 - width: 90, 273 - height: 90, 274 - fit: BoxFit.cover, 275 - placeholder: (context, url) => const CupertinoActivityIndicator(), 276 - errorWidget: (context, url, error) => Icon( 277 - Ionicons.person_outline, 278 - size: 40, 279 - color: isDarkMode ? AppColors.textLight : AppColors.textSecondary, 269 + child: Center( 270 + child: avatar != null && avatar.isNotEmpty 271 + ? ClipOval( 272 + child: CachedNetworkImage( 273 + imageUrl: avatar, 274 + width: 90, 275 + height: 90, 276 + fit: BoxFit.cover, 277 + placeholder: (context, url) => const CupertinoActivityIndicator(), 278 + errorWidget: (context, url, error) => Icon( 279 + Ionicons.person_outline, 280 + size: 40, 281 + color: isDarkMode ? AppColors.textLight : AppColors.textSecondary, 282 + ), 280 283 ), 284 + ) 285 + : Icon( 286 + Ionicons.person_outline, 287 + size: 40, 288 + color: isDarkMode ? AppColors.textLight : AppColors.textSecondary, 281 289 ), 282 - ) 283 - : Icon( 284 - Ionicons.person_outline, 285 - size: 40, 286 - color: isDarkMode ? AppColors.textLight : AppColors.textSecondary, 287 - ), 290 + ), 288 291 ), 289 - ), 290 - if (isCurrentUser) 291 - Positioned( 292 - right: 0, 293 - bottom: 0, 294 - child: Container( 295 - width: 30, 296 - height: 30, 297 - decoration: BoxDecoration( 298 - shape: BoxShape.circle, 299 - color: AppColors.primary, 300 - border: Border.all( 301 - color: isDarkMode ? AppColors.deepPurple : AppColors.white, 302 - width: 2, 292 + if (isCurrentUser) 293 + Positioned( 294 + right: 0, 295 + bottom: 0, 296 + child: Container( 297 + width: 30, 298 + height: 30, 299 + decoration: BoxDecoration( 300 + shape: BoxShape.circle, 301 + color: AppColors.primary, 302 + border: Border.all( 303 + color: isDarkMode ? AppColors.deepPurple : AppColors.white, 304 + width: 2, 305 + ), 303 306 ), 304 - ), 305 - child: const Center( 306 - child: Icon( 307 - CupertinoIcons.plus, 308 - size: 18, 309 - color: AppColors.white, 307 + child: const Center( 308 + child: Icon( 309 + CupertinoIcons.plus, 310 + size: 18, 311 + color: AppColors.white, 312 + ), 310 313 ), 311 314 ), 312 315 ), 313 - ), 314 - ], 315 - ), 316 + ], 317 + ), 316 318 317 - const SizedBox(width: 20), 319 + const SizedBox(width: 20), 318 320 319 - // Stats row 320 - Expanded( 321 - child: Row( 322 - mainAxisAlignment: MainAxisAlignment.spaceEvenly, 323 - children: const [ 324 - ProfileStatItem(count: '129', label: 'Posts'), 325 - ProfileStatItem(count: '3680', label: 'Followers'), 326 - ProfileStatItem(count: '230', label: 'Following'), 327 - ], 321 + // Stats row 322 + Expanded( 323 + child: Row( 324 + mainAxisAlignment: MainAxisAlignment.spaceEvenly, 325 + children: const [ 326 + ProfileStatItem(count: '129', label: 'Posts'), 327 + ProfileStatItem(count: '3680', label: 'Followers'), 328 + ProfileStatItem(count: '230', label: 'Following'), 329 + ], 330 + ), 328 331 ), 329 - ), 330 - ], 331 - ), 332 + ], 333 + ), 332 334 333 - const SizedBox(height: 16), 335 + const SizedBox(height: 16), 334 336 335 - // Username and verified badge 336 - Row( 337 - children: [ 338 - Text( 339 - displayName.isNotEmpty ? displayName : handle, 340 - style: TextStyle( 341 - fontWeight: FontWeight.bold, 342 - fontSize: 18, 343 - color: AppTheme.getTextColor(context), 337 + // Username and verified badge 338 + Row( 339 + children: [ 340 + Text( 341 + displayName.isNotEmpty ? displayName : handle, 342 + style: TextStyle( 343 + fontWeight: FontWeight.bold, 344 + fontSize: 18, 345 + color: AppTheme.getTextColor(context), 346 + ), 344 347 ), 345 - ), 346 348 347 - // Early Supporter badge 348 - if (_isEarlySupporter) ...[ 349 - const SizedBox(width: 8), 350 - GestureDetector( 351 - onTap: () => _showEarlySupporterInfo(context), 352 - child: SvgPicture.asset( 353 - 'assets/images/match.svg', 354 - height: 20, 355 - width: 20, 356 - colorFilter: const ColorFilter.mode( 357 - AppColors.primary, 358 - BlendMode.srcIn 349 + // Early Supporter badge 350 + if (_isEarlySupporter) ...[ 351 + const SizedBox(width: 8), 352 + GestureDetector( 353 + onTap: () => _showEarlySupporterInfo(context), 354 + child: SvgPicture.asset( 355 + 'assets/images/match.svg', 356 + height: 20, 357 + width: 20, 358 + colorFilter: const ColorFilter.mode( 359 + AppColors.primary, 360 + BlendMode.srcIn 361 + ), 359 362 ), 360 363 ), 361 - ), 364 + ], 362 365 ], 363 - ], 364 - ), 365 - 366 - const SizedBox(height: 4), 367 - 368 - // Username in the format seen in the screenshot 369 - Text( 370 - '@$handle', 371 - style: TextStyle( 372 - color: AppTheme.getSecondaryTextColor(context), 373 - fontSize: 14, 374 366 ), 375 - ), 376 367 377 - if (description.isNotEmpty) ...[ 378 - const SizedBox(height: 8), 368 + const SizedBox(height: 4), 369 + 370 + // Username in the format seen in the screenshot 379 371 Text( 380 - description, 372 + '@$handle', 381 373 style: TextStyle( 382 - color: AppTheme.getTextColor(context), 374 + color: AppTheme.getSecondaryTextColor(context), 383 375 fontSize: 14, 384 376 ), 385 377 ), 386 - ], 378 + 379 + if (description.isNotEmpty) ...[ 380 + const SizedBox(height: 8), 381 + Text( 382 + description, 383 + style: TextStyle( 384 + color: AppTheme.getTextColor(context), 385 + fontSize: 14, 386 + ), 387 + ), 388 + ], 387 389 388 - const SizedBox(height: 16), 390 + const SizedBox(height: 16), 391 + 392 + // Action buttons in a row 393 + Row( 394 + children: [ 395 + // Edit button - only for current user 396 + if (isCurrentUser) ...[ 397 + Expanded( 398 + flex: 1, 399 + child: ProfileActionButton( 400 + label: 'Edit', 401 + onPressed: () => _checkAuthAndProceed(() { 402 + // Edit profile logic here 403 + }), 404 + isPrimary: true, 405 + isOutlined: false, 406 + ), 407 + ), 408 + const SizedBox(width: 8), 409 + ], 389 410 390 - // Action buttons in a row 391 - Row( 392 - children: [ 393 - // Edit button - only for current user 394 - if (isCurrentUser) ...[ 411 + // Share Profile button 395 412 Expanded( 396 413 flex: 1, 397 - child: ProfileActionButton( 398 - label: 'Edit', 399 - onPressed: () => _checkAuthAndProceed(() { 400 - // Edit profile logic here 401 - }), 402 - isPrimary: true, 403 - isOutlined: false, 414 + child: Container( 415 + constraints: const BoxConstraints(minHeight: 36), 416 + child: ProfileActionButton( 417 + label: 'Share Profile', 418 + onPressed: () { 419 + // Share profile doesn't require authentication 420 + }, 421 + ), 404 422 ), 405 423 ), 424 + 406 425 const SizedBox(width: 8), 407 - ], 408 426 409 - // Share Profile button 410 - Expanded( 411 - flex: 1, 412 - child: Container( 413 - constraints: const BoxConstraints(minHeight: 36), 427 + // Follow button for non-current user, Friends+ for current user 428 + Expanded( 429 + flex: 1, 414 430 child: ProfileActionButton( 415 - label: 'Share Profile', 416 - onPressed: () { 417 - // Share profile doesn't require authentication 418 - }, 431 + label: isCurrentUser ? 'Friends +' : 'Follow', 432 + onPressed: () => _checkAuthAndProceed(() { 433 + // Follow or friends management logic here 434 + }), 419 435 ), 420 436 ), 421 - ), 422 - 423 - const SizedBox(width: 8), 424 - 425 - // Follow button for non-current user, Friends+ for current user 426 - Expanded( 427 - flex: 1, 428 - child: ProfileActionButton( 429 - label: isCurrentUser ? 'Friends +' : 'Follow', 430 - onPressed: () => _checkAuthAndProceed(() { 431 - // Follow or friends management logic here 432 - }), 433 - ), 434 - ), 435 - ], 436 - ), 437 - ], 437 + ], 438 + ), 439 + ], 440 + ), 438 441 ), 439 442 ), 440 443 441 - // Tab bar at the bottom of content 442 - Container( 443 - decoration: BoxDecoration( 444 - border: Border( 445 - top: BorderSide( 446 - color: AppColors.border, 447 - width: 0.5, 444 + // Tab bar - Sticky when scrolling 445 + SliverPersistentHeader( 446 + pinned: true, 447 + delegate: _StickyTabBarDelegate( 448 + child: Container( 449 + decoration: BoxDecoration( 450 + color: AppTheme.getBackgroundColor(context, false), 451 + border: Border( 452 + top: BorderSide( 453 + color: AppColors.border, 454 + width: 0.5, 455 + ), 456 + bottom: BorderSide( 457 + color: AppColors.border, 458 + width: 0.5, 459 + ), 460 + ), 448 461 ), 449 - bottom: BorderSide( 450 - color: AppColors.border, 451 - width: 0.5, 462 + child: SingleChildScrollView( 463 + scrollDirection: Axis.horizontal, 464 + child: Row( 465 + mainAxisAlignment: MainAxisAlignment.spaceEvenly, 466 + children: [ 467 + _buildTabItem(context, 0, CupertinoIcons.film), 468 + _buildTabItem(context, 1, CupertinoIcons.heart), 469 + _buildTabItem(context, 2, CupertinoIcons.arrow_2_squarepath), 470 + if (isAuthenticated) _buildTabItem(context, 3, CupertinoIcons.bookmark), 471 + if (isAuthenticated) _buildTabItem(context, 4, CupertinoIcons.lock), 472 + ], 473 + ), 452 474 ), 453 475 ), 454 476 ), 455 - child: SingleChildScrollView( 456 - scrollDirection: Axis.horizontal, 457 - child: Row( 458 - mainAxisAlignment: MainAxisAlignment.spaceEvenly, 459 - children: [ 460 - _buildTabItem(context, 0, CupertinoIcons.film), 461 - _buildTabItem(context, 1, CupertinoIcons.heart), 462 - _buildTabItem(context, 2, CupertinoIcons.arrow_2_squarepath), 463 - if (isAuthenticated) _buildTabItem(context, 3, CupertinoIcons.bookmark), 464 - if (isAuthenticated) _buildTabItem(context, 4, CupertinoIcons.lock), 465 - ], 466 - ), 467 - ), 468 477 ), 469 478 470 - // Tab content - with fixed height to prevent scrolling of the entire screen 471 - Expanded( 472 - child: _buildTabContent(), 473 - ), 479 + // Tab content - now integrated directly as slivers 480 + ..._buildTabContent(), 474 481 ], 475 482 ), 476 483 ), ··· 529 536 ); 530 537 } 531 538 532 - Widget _buildTabContent() { 539 + List<Widget> _buildTabContent() { 533 540 final authService = Provider.of<AuthService>(context); 534 541 535 542 // For tabs that require authentication, show auth prompt if not authenticated 536 543 if ((_selectedTabIndex == 3 || _selectedTabIndex == 4) && !authService.isAuthenticated) { 537 - return Center( 538 - child: Column( 539 - mainAxisAlignment: MainAxisAlignment.center, 540 - children: [ 541 - Icon( 542 - _selectedTabIndex == 3 ? CupertinoIcons.bookmark : CupertinoIcons.lock, 543 - size: 60, 544 - color: AppTheme.getSecondaryTextColor(context), 545 - ), 546 - const SizedBox(height: 20), 547 - Text( 548 - _selectedTabIndex == 3 ? 'Saved videos' : 'Private videos', 549 - style: TextStyle( 550 - fontWeight: FontWeight.bold, 551 - fontSize: 18, 552 - color: AppTheme.getTextColor(context), 553 - ), 544 + return [ 545 + SliverFillRemaining( 546 + hasScrollBody: false, 547 + child: Center( 548 + child: Column( 549 + mainAxisAlignment: MainAxisAlignment.center, 550 + children: [ 551 + Icon( 552 + _selectedTabIndex == 3 ? CupertinoIcons.bookmark : CupertinoIcons.lock, 553 + size: 60, 554 + color: AppTheme.getSecondaryTextColor(context), 555 + ), 556 + const SizedBox(height: 20), 557 + Text( 558 + _selectedTabIndex == 3 ? 'Saved videos' : 'Private videos', 559 + style: TextStyle( 560 + fontWeight: FontWeight.bold, 561 + fontSize: 18, 562 + color: AppTheme.getTextColor(context), 563 + ), 564 + ), 565 + const SizedBox(height: 10), 566 + Text( 567 + 'Login to view your saved content', 568 + style: TextStyle( 569 + color: AppTheme.getSecondaryTextColor(context), 570 + ), 571 + textAlign: TextAlign.center, 572 + ), 573 + const SizedBox(height: 24), 574 + CupertinoButton( 575 + color: CupertinoColors.systemPink, 576 + onPressed: () { 577 + setState(() { 578 + _showAuthPrompt = true; 579 + }); 580 + }, 581 + child: const Text('Login'), 582 + ), 583 + ], 554 584 ), 555 - const SizedBox(height: 10), 556 - Text( 557 - 'Login to view your saved content', 558 - style: TextStyle( 559 - color: AppTheme.getSecondaryTextColor(context), 560 - ), 561 - textAlign: TextAlign.center, 562 - ), 563 - const SizedBox(height: 24), 564 - CupertinoButton( 565 - color: CupertinoColors.systemPink, 566 - onPressed: () { 567 - setState(() { 568 - _showAuthPrompt = true; 569 - }); 570 - }, 571 - child: const Text('Login'), 572 - ), 573 - ], 585 + ), 574 586 ), 575 - ); 587 + ]; 576 588 } 577 589 578 590 switch (_selectedTabIndex) { 579 591 case 0: 580 - return _buildPostsGrid(); 592 + return _buildPostsGridSlivers(); 581 593 case 1: 582 - return const VideosGrid( 583 - itemCount: 15, 584 - iconType: Ionicons.heart_outline, 585 - ); 594 + return [ 595 + SliverGrid( 596 + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 597 + crossAxisCount: 3, 598 + childAspectRatio: 2/3, 599 + crossAxisSpacing: 1, 600 + mainAxisSpacing: 1, 601 + ), 602 + delegate: SliverChildBuilderDelegate( 603 + (context, index) { 604 + // Create different color patterns based on the icon type 605 + Color backgroundColor = index % 3 == 0 606 + ? AppColors.orange.withOpacity(0.7) 607 + : index % 3 == 1 608 + ? AppColors.primary.withOpacity(0.7) 609 + : AppColors.red.withOpacity(0.7); 610 + 611 + return VideoThumbnail( 612 + index: index, 613 + backgroundColor: backgroundColor, 614 + icon: Ionicons.heart_outline, 615 + viewCount: '${(index + 1) * 1000}', 616 + ); 617 + }, 618 + childCount: 30, 619 + ), 620 + ), 621 + ]; 586 622 case 2: 587 - return const VideosGrid( 588 - itemCount: 8, 589 - iconType: CupertinoIcons.arrow_2_squarepath, 590 - ); 623 + return [ 624 + SliverGrid( 625 + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 626 + crossAxisCount: 3, 627 + childAspectRatio: 2/3, 628 + crossAxisSpacing: 1, 629 + mainAxisSpacing: 1, 630 + ), 631 + delegate: SliverChildBuilderDelegate( 632 + (context, index) { 633 + // Create different color patterns based on the icon type 634 + Color backgroundColor = index % 3 == 0 635 + ? AppColors.green.withOpacity(0.7) 636 + : index % 3 == 1 637 + ? AppColors.blue.withOpacity(0.7) 638 + : AppColors.primary.withOpacity(0.7); 639 + 640 + return VideoThumbnail( 641 + index: index, 642 + backgroundColor: backgroundColor, 643 + icon: CupertinoIcons.arrow_2_squarepath, 644 + viewCount: '${(index + 1) * 1000}', 645 + ); 646 + }, 647 + childCount: 25, 648 + ), 649 + ), 650 + ]; 591 651 case 3: 592 - return const VideosGrid( 593 - itemCount: 12, 594 - iconType: Ionicons.bookmark_outline, 595 - ); 652 + return [ 653 + SliverGrid( 654 + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 655 + crossAxisCount: 3, 656 + childAspectRatio: 2/3, 657 + crossAxisSpacing: 1, 658 + mainAxisSpacing: 1, 659 + ), 660 + delegate: SliverChildBuilderDelegate( 661 + (context, index) { 662 + // Create different color patterns based on the icon type 663 + Color backgroundColor = index % 3 == 0 664 + ? AppColors.teal.withOpacity(0.7) 665 + : index % 3 == 1 666 + ? AppColors.blue.withOpacity(0.7) 667 + : AppColors.lightBlue.withOpacity(0.7); 668 + 669 + return VideoThumbnail( 670 + index: index, 671 + backgroundColor: backgroundColor, 672 + icon: Ionicons.bookmark_outline, 673 + viewCount: '${(index + 1) * 1000}', 674 + ); 675 + }, 676 + childCount: 28, 677 + ), 678 + ), 679 + ]; 596 680 case 4: 597 - return _buildPrivateTab(); 681 + return _buildPrivateTabSlivers(); 598 682 default: 599 - return const SizedBox.shrink(); 683 + return [const SliverToBoxAdapter(child: SizedBox.shrink())]; 600 684 } 601 685 } 602 686 603 - Widget _buildPostsGrid() { 604 - return GridView.builder( 605 - physics: const NeverScrollableScrollPhysics(), // Prevents scrolling within the grid 606 - padding: const EdgeInsets.all(1), 607 - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 608 - crossAxisCount: 3, 609 - childAspectRatio: 2/3, 610 - crossAxisSpacing: 1, 611 - mainAxisSpacing: 1, 612 - ), 613 - itemCount: 12, 614 - itemBuilder: (context, index) { 615 - // Alternate between video and image posts 616 - final bool isVideo = index % 2 == 0; 687 + List<Widget> _buildPostsGridSlivers() { 688 + return [ 689 + SliverPadding( 690 + padding: const EdgeInsets.all(1), 691 + sliver: SliverGrid( 692 + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 693 + crossAxisCount: 3, 694 + childAspectRatio: 2/3, 695 + crossAxisSpacing: 1, 696 + mainAxisSpacing: 1, 697 + ), 698 + delegate: SliverChildBuilderDelegate( 699 + (context, index) { 700 + // Alternate between video and image posts 701 + final bool isVideo = index % 2 == 0; 617 702 618 - return GestureDetector( 619 - onTap: () { 620 - debugPrint('Post clicked: ${isVideo ? "Video" : "Image"} at index $index'); 621 - }, 622 - child: Container( 623 - color: isVideo 624 - ? AppColors.richPurple.withOpacity(0.7) 625 - : AppColors.orange.withOpacity(0.7), 626 - child: Stack( 627 - children: [ 628 - Center( 629 - child: Icon( 630 - isVideo ? CupertinoIcons.film : CupertinoIcons.photo, 631 - color: AppColors.white.withOpacity(0.8), 632 - size: 24, 633 - ), 634 - ), 635 - Positioned( 636 - bottom: 5, 637 - left: 5, 638 - child: Row( 703 + return GestureDetector( 704 + onTap: () { 705 + debugPrint('Post clicked: ${isVideo ? "Video" : "Image"} at index $index'); 706 + }, 707 + child: Container( 708 + color: isVideo 709 + ? AppColors.richPurple.withOpacity(0.7) 710 + : AppColors.orange.withOpacity(0.7), 711 + child: Stack( 639 712 children: [ 640 - Icon( 641 - isVideo ? CupertinoIcons.eye : CupertinoIcons.heart, 642 - color: AppColors.white, 643 - size: 12, 713 + Center( 714 + child: Icon( 715 + isVideo ? CupertinoIcons.film : CupertinoIcons.photo, 716 + color: AppColors.white.withOpacity(0.8), 717 + size: 24, 718 + ), 644 719 ), 645 - const SizedBox(width: 4), 646 - Text( 647 - '${(index + 1) * 1000}', 648 - style: const TextStyle( 649 - color: AppColors.white, 650 - fontSize: 12, 720 + Positioned( 721 + bottom: 5, 722 + left: 5, 723 + child: Row( 724 + children: [ 725 + Icon( 726 + isVideo ? CupertinoIcons.eye : CupertinoIcons.heart, 727 + color: AppColors.white, 728 + size: 12, 729 + ), 730 + const SizedBox(width: 4), 731 + Text( 732 + '${(index + 1) * 1000}', 733 + style: const TextStyle( 734 + color: AppColors.white, 735 + fontSize: 12, 736 + ), 737 + ), 738 + ], 651 739 ), 652 740 ), 741 + if (isVideo) 742 + Positioned( 743 + top: 5, 744 + right: 5, 745 + child: Container( 746 + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), 747 + decoration: BoxDecoration( 748 + color: AppColors.black.withOpacity(0.5), 749 + borderRadius: BorderRadius.circular(4), 750 + ), 751 + child: const Text( 752 + '0:30', 753 + style: TextStyle( 754 + color: AppColors.white, 755 + fontSize: 10, 756 + ), 757 + ), 758 + ), 759 + ), 653 760 ], 654 761 ), 655 762 ), 656 - if (isVideo) 657 - Positioned( 658 - top: 5, 659 - right: 5, 660 - child: Container( 661 - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), 662 - decoration: BoxDecoration( 663 - color: AppColors.black.withOpacity(0.5), 664 - borderRadius: BorderRadius.circular(4), 665 - ), 666 - child: const Text( 667 - '0:30', 668 - style: TextStyle( 669 - color: AppColors.white, 670 - fontSize: 10, 671 - ), 672 - ), 673 - ), 674 - ), 675 - ], 676 - ), 763 + ); 764 + }, 765 + childCount: 24, 677 766 ), 678 - ); 679 - }, 680 - ); 767 + ), 768 + ), 769 + ]; 681 770 } 682 771 683 - Widget _buildPrivateTab() { 684 - return Center( 685 - child: Column( 686 - mainAxisAlignment: MainAxisAlignment.center, 687 - children: [ 688 - Icon( 689 - CupertinoIcons.lock, 690 - size: 60, 691 - color: AppTheme.getSecondaryTextColor(context), 692 - ), 693 - const SizedBox(height: 20), 694 - Text( 695 - 'Private videos', 696 - style: TextStyle( 697 - fontWeight: FontWeight.bold, 698 - fontSize: 18, 699 - color: AppTheme.getTextColor(context), 700 - ), 701 - ), 702 - const SizedBox(height: 10), 703 - Text( 704 - 'Videos you\'ve saved to private will appear here', 705 - style: TextStyle( 706 - color: AppTheme.getSecondaryTextColor(context), 707 - ), 708 - textAlign: TextAlign.center, 772 + List<Widget> _buildPrivateTabSlivers() { 773 + return [ 774 + SliverFillRemaining( 775 + hasScrollBody: false, 776 + child: Center( 777 + child: Column( 778 + mainAxisAlignment: MainAxisAlignment.center, 779 + children: [ 780 + Icon( 781 + CupertinoIcons.lock, 782 + size: 60, 783 + color: AppTheme.getSecondaryTextColor(context), 784 + ), 785 + const SizedBox(height: 20), 786 + Text( 787 + 'Private videos', 788 + style: TextStyle( 789 + fontWeight: FontWeight.bold, 790 + fontSize: 18, 791 + color: AppTheme.getTextColor(context), 792 + ), 793 + ), 794 + const SizedBox(height: 10), 795 + Text( 796 + 'Videos you\'ve saved to private will appear here', 797 + style: TextStyle( 798 + color: AppTheme.getSecondaryTextColor(context), 799 + ), 800 + textAlign: TextAlign.center, 801 + ), 802 + ], 709 803 ), 710 - ], 804 + ), 711 805 ), 712 - ); 806 + ]; 807 + } 808 + } 809 + 810 + // Add a StickyTabBarDelegate class for the persistent header 811 + class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate { 812 + final Widget child; 813 + 814 + _StickyTabBarDelegate({required this.child}); 815 + 816 + @override 817 + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { 818 + return child; 819 + } 820 + 821 + @override 822 + double get maxExtent => 50.0; // Adjust this value based on your tab bar height 823 + 824 + @override 825 + double get minExtent => 50.0; // Same as maxExtent to maintain height 826 + 827 + @override 828 + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { 829 + return true; 713 830 } 714 831 }
+12 -12
lib/widgets/profile/videos_grid.dart
··· 6 6 class VideosGrid extends StatelessWidget { 7 7 final int itemCount; 8 8 final IconData iconType; 9 - 9 + 10 10 const VideosGrid({ 11 11 super.key, 12 12 required this.itemCount, ··· 16 16 @override 17 17 Widget build(BuildContext context) { 18 18 return GridView.builder( 19 - physics: const NeverScrollableScrollPhysics(), 19 + physics: const BouncingScrollPhysics(), 20 20 padding: const EdgeInsets.all(1), 21 21 gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 22 22 crossAxisCount: 3, ··· 29 29 // Create different color patterns based on the icon type 30 30 Color backgroundColor; 31 31 if (iconType == Ionicons.heart_outline || iconType == CupertinoIcons.heart) { 32 - backgroundColor = index % 3 == 0 32 + backgroundColor = index % 3 == 0 33 33 ? AppColors.orange.withOpacity(0.7) 34 - : index % 3 == 1 34 + : index % 3 == 1 35 35 ? AppColors.primary.withOpacity(0.7) 36 36 : AppColors.red.withOpacity(0.7); 37 37 } else if (iconType == Ionicons.bookmark_outline || iconType == CupertinoIcons.bookmark) { 38 - backgroundColor = index % 3 == 0 38 + backgroundColor = index % 3 == 0 39 39 ? AppColors.teal.withOpacity(0.7) 40 - : index % 3 == 1 40 + : index % 3 == 1 41 41 ? AppColors.blue.withOpacity(0.7) 42 42 : AppColors.lightBlue.withOpacity(0.7); 43 43 } else if (iconType == CupertinoIcons.arrow_2_squarepath) { 44 - backgroundColor = index % 3 == 0 44 + backgroundColor = index % 3 == 0 45 45 ? AppColors.green.withOpacity(0.7) 46 - : index % 3 == 1 46 + : index % 3 == 1 47 47 ? AppColors.blue.withOpacity(0.7) 48 48 : AppColors.primary.withOpacity(0.7); 49 49 } else { 50 - backgroundColor = index % 3 == 0 50 + backgroundColor = index % 3 == 0 51 51 ? AppColors.richPurple.withOpacity(0.7) 52 - : index % 3 == 1 52 + : index % 3 == 1 53 53 ? AppColors.brightPurple.withOpacity(0.7) 54 54 : AppColors.primary.withOpacity(0.7); 55 55 } 56 - 56 + 57 57 return VideoThumbnail( 58 58 index: index, 59 59 backgroundColor: backgroundColor, ··· 63 63 }, 64 64 ); 65 65 } 66 - } 66 + }