[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.

mike baguncinha

atualizacoes no perfil e tals.

C3B f139ee8e 03a040c0

+793 -224
+5
assets/icons/match.svg
··· 1 + <svg width="34" height="40" viewBox="0 0 34 40" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <path d="M24.4102 16.52C26.0317 15.4056 28.1537 15.4143 29.6782 16.5416C30.4446 17.1083 30.9945 17.9125 31.2453 18.8334C31.7444 20.6656 30.9874 22.653 29.3659 23.7674L29.0063 24.0146C28.5478 24.3297 28.0436 24.5717 27.5133 24.7312L26.6259 24.998C24.8912 25.5197 23.0557 24.8984 22.0692 23.4557C21.0827 22.013 21.1665 20.0726 22.2774 18.6385L22.8456 17.9049C23.1852 17.4665 23.5921 17.0823 24.0506 16.7672L24.4102 16.52Z" fill="#FF2696"/> 3 + <path d="M19.5098 23.8924C19.2078 23.2291 19.0567 22.8974 18.8543 22.8104C18.6518 22.7234 18.4244 22.8618 17.9696 23.1385L1.88053 33.8842C-0.0595459 35.065 -0.697365 35.6545 0.91915 38.1983C2.53566 40.7421 3.2963 40.2399 5.09433 38.8514L21.278 27.9768C21.6995 27.6513 21.9103 27.4886 21.9023 27.2679C21.8944 27.0471 21.6403 26.7863 21.132 26.2647C20.8019 25.9258 20.4955 25.5528 20.219 25.1484C19.9425 24.7442 19.706 24.3232 19.5098 23.8924Z" fill="#FF2696"/> 4 + <path d="M33.5118 16.1279C33.4135 16.4009 33.0816 16.4089 32.9627 16.1431C32.5498 15.2204 31.9053 14.4227 31.0787 13.823C28.889 12.2346 25.7939 12.2874 23.395 13.9541L22.8629 14.3238C22.1845 14.7951 21.5793 15.365 21.0707 16.0115L20.2196 17.0933C20.1119 17.2301 20.0105 17.3699 19.9153 17.5123C19.4835 18.1576 19.2677 18.4803 19.0143 18.4452C18.761 18.4101 18.6574 18.1071 18.4501 17.5011C18.247 16.9074 18.118 16.2633 18.063 15.5688C17.9775 14.4878 18.0574 13.5508 18.3028 12.7578C18.563 11.9471 18.878 11.2147 19.2479 10.5605C19.6165 9.88993 19.9554 9.25456 20.2647 8.6544C20.5726 8.03785 20.7471 7.37377 20.7881 6.66215C20.7952 6.54627 20.8303 6.4777 20.8935 6.45643C20.9715 6.41755 21.0534 6.42781 21.1391 6.48721C21.7435 6.95213 22.2557 7.48172 22.6756 8.07597C22.9671 8.46255 23.2423 8.87258 23.501 9.30607C23.6769 9.60066 23.7648 9.74795 23.8956 9.73983C24.0265 9.73171 24.0892 9.58663 24.2148 9.29647C24.5903 8.42868 24.832 7.31372 24.94 5.95159C25.0767 4.19466 24.9606 2.31686 24.5916 0.318187C24.5697 0.219358 24.5803 0.143075 24.6234 0.0893381C24.632 0.0787477 24.6362 0.0734524 24.6428 0.0666227C24.6493 0.059793 24.655 0.0548434 24.6663 0.0449439C24.7246 -0.00609996 24.8054 -0.0136651 24.9087 0.0222488C26.7493 0.739369 28.3204 1.74057 29.6221 3.02585C30.9237 4.31112 31.9369 5.74188 32.6617 7.3181C33.4025 8.8931 33.8364 10.4831 33.9634 12.0883C34.0917 13.7097 33.869 14.4494 33.5118 16.1279Z" fill="#FF2696"/> 5 + </svg>
+6
assets/images/match.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <svg width="34" height="40" viewBox="0 0 34 40" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 + <path d="M24.4102 16.52C26.0317 15.4056 28.1537 15.4143 29.6782 16.5416C30.4446 17.1083 30.9945 17.9125 31.2453 18.8334C31.7444 20.6656 30.9874 22.653 29.3659 23.7674L29.0063 24.0146C28.5478 24.3297 28.0436 24.5717 27.5133 24.7312L26.6259 24.998C24.8912 25.5197 23.0557 24.8984 22.0692 23.4557C21.0827 22.013 21.1665 20.0726 22.2774 18.6385L22.8456 17.9049C23.1852 17.4665 23.5921 17.0823 24.0506 16.7672L24.4102 16.52Z" fill="#FF2696"/> 4 + <path d="M19.5098 23.8924C19.2078 23.2291 19.0567 22.8974 18.8543 22.8104C18.6518 22.7234 18.4244 22.8618 17.9696 23.1385L1.88053 33.8842C-0.0595459 35.065 -0.697365 35.6545 0.91915 38.1983C2.53566 40.7421 3.2963 40.2399 5.09433 38.8514L21.278 27.9768C21.6995 27.6513 21.9103 27.4886 21.9023 27.2679C21.8944 27.0471 21.6403 26.7863 21.132 26.2647C20.8019 25.9258 20.4955 25.5528 20.219 25.1484C19.9425 24.7442 19.706 24.3232 19.5098 23.8924Z" fill="#FF2696"/> 5 + <path d="M33.5118 16.1279C33.4135 16.4009 33.0816 16.4089 32.9627 16.1431C32.5498 15.2204 31.9053 14.4227 31.0787 13.823C28.889 12.2346 25.7939 12.2874 23.395 13.9541L22.8629 14.3238C22.1845 14.7951 21.5793 15.365 21.0707 16.0115L20.2196 17.0933C20.1119 17.2301 20.0105 17.3699 19.9153 17.5123C19.4835 18.1576 19.2677 18.4803 19.0143 18.4452C18.761 18.4101 18.6574 18.1071 18.4501 17.5011C18.247 16.9074 18.118 16.2633 18.063 15.5688C17.9775 14.4878 18.0574 13.5508 18.3028 12.7578C18.563 11.9471 18.878 11.2147 19.2479 10.5605C19.6165 9.88993 19.9554 9.25456 20.2647 8.6544C20.5726 8.03785 20.7471 7.37377 20.7881 6.66215C20.7952 6.54627 20.8303 6.4777 20.8935 6.45643C20.9715 6.41755 21.0534 6.42781 21.1391 6.48721C21.7435 6.95213 22.2557 7.48172 22.6756 8.07597C22.9671 8.46255 23.2423 8.87258 23.501 9.30607C23.6769 9.60066 23.7648 9.74795 23.8956 9.73983C24.0265 9.73171 24.0892 9.58663 24.2148 9.29647C24.5903 8.42868 24.832 7.31372 24.94 5.95159C25.0767 4.19466 24.9606 2.31686 24.5916 0.318187C24.5697 0.219358 24.5803 0.143075 24.6234 0.0893381C24.632 0.0787477 24.6362 0.0734524 24.6428 0.0666227C24.6493 0.059793 24.655 0.0548434 24.6663 0.0449439C24.7246 -0.00609996 24.8054 -0.0136651 24.9087 0.0222488C26.7493 0.739369 28.3204 1.74057 29.6221 3.02585C30.9237 4.31112 31.9369 5.74188 32.6617 7.3181C33.4025 8.8931 33.8364 10.4831 33.9634 12.0883C34.0917 13.7097 33.869 14.4494 33.5118 16.1279Z" fill="#FF2696"/> 6 + </svg>
+6 -6
lib/main.dart
··· 107 107 context, 108 108 0, 109 109 'Home', 110 - Ionicons.home_outline, 111 - Ionicons.home, 110 + Ionicons.heart_outline, 111 + Ionicons.heart, 112 112 ), 113 113 _buildNavItem( 114 114 context, 115 115 1, 116 116 'Discover', 117 - Ionicons.search_outline, 118 - Ionicons.search, 117 + Ionicons.bookmark_outline, 118 + Ionicons.bookmark, 119 119 ), 120 120 _buildCreateButton(context), 121 121 _buildNavItem( 122 122 context, 123 123 3, 124 124 'Inbox', 125 - Ionicons.chatbubble_outline, 126 - Ionicons.chatbubble, 125 + Ionicons.image_outline, 126 + Ionicons.image, 127 127 ), 128 128 _buildNavItem( 129 129 context,
+316 -218
lib/screens/profile_screen.dart
··· 1 1 import 'package:flutter/cupertino.dart'; 2 2 import 'package:flutter/material.dart' show Colors; 3 3 import 'package:ionicons/ionicons.dart'; 4 + import 'package:flutter_svg/flutter_svg.dart'; 4 5 import '../utils/app_colors.dart'; 5 6 import '../utils/app_theme.dart'; 7 + import '../widgets/profile/profile_stat_item.dart'; 8 + import '../widgets/profile/profile_action_button.dart'; 9 + import '../widgets/profile/videos_grid.dart'; 10 + import '../widgets/profile/early_supporter_sheet.dart'; 6 11 7 12 class ProfileScreen extends StatefulWidget { 8 13 const ProfileScreen({super.key}); ··· 14 19 class _ProfileScreenState extends State<ProfileScreen> { 15 20 int _selectedTabIndex = 0; 16 21 22 + // This flag would normally come from user auth 23 + final bool _isOwnProfile = true; 24 + 25 + // Flags for special badges 26 + final bool _isEarlySupporter = true; 27 + 28 + void _showEarlySupporterInfo(BuildContext context) { 29 + showCupertinoModalPopup( 30 + context: context, 31 + barrierDismissible: true, 32 + builder: (context) => SizedBox( 33 + width: double.infinity, 34 + child: SafeArea( 35 + child: Padding( 36 + padding: const EdgeInsets.only(top: 20), 37 + child: EarlySupporterSheet(), 38 + ), 39 + ), 40 + ), 41 + ); 42 + } 43 + 17 44 @override 18 45 Widget build(BuildContext context) { 19 46 final brightness = MediaQuery.of(context).platformBrightness; 20 47 final isDarkMode = brightness == Brightness.dark; 48 + 49 + // Get screen dimensions to ensure no overflow 50 + final screenHeight = MediaQuery.of(context).size.height; 21 51 22 52 return CupertinoPageScaffold( 23 53 backgroundColor: AppTheme.getBackgroundColor(context, false), 24 54 navigationBar: CupertinoNavigationBar( 25 - middle: Text( 26 - '@username', 27 - style: TextStyle(color: AppTheme.getTextColor(context)), 28 - ), 29 - trailing: Icon( 30 - Ionicons.menu_outline, 31 - color: AppTheme.getTextColor(context), 55 + middle: const Text( 56 + 'Profile', 57 + style: TextStyle( 58 + fontWeight: FontWeight.bold, 59 + fontSize: 18, 60 + ), 32 61 ), 33 62 backgroundColor: isDarkMode ? AppColors.deepPurple : AppColors.background, 34 63 ), 35 64 child: SafeArea( 65 + bottom: false, // Don't add padding at the bottom for the tab bar 36 66 child: Column( 37 67 children: [ 38 - // Profile info 68 + // Profile info - horizontal layout 39 69 Padding( 40 70 padding: const EdgeInsets.all(16.0), 41 71 child: Column( 72 + crossAxisAlignment: CrossAxisAlignment.start, 42 73 children: [ 43 - // Profile image 44 - Container( 45 - width: 100, 46 - height: 100, 47 - decoration: BoxDecoration( 48 - color: isDarkMode ? AppColors.darkPurple : AppColors.lightLavender, 49 - shape: BoxShape.circle, 50 - border: Border.all( 51 - color: isDarkMode ? AppColors.darkPurple : AppColors.lightLavender, 52 - width: 2, 74 + // Profile image and stats in a row 75 + Row( 76 + crossAxisAlignment: CrossAxisAlignment.center, 77 + children: [ 78 + // Profile image with + button 79 + Stack( 80 + children: [ 81 + Container( 82 + width: 90, 83 + height: 90, 84 + decoration: BoxDecoration( 85 + color: isDarkMode ? AppColors.darkPurple : AppColors.lightLavender, 86 + shape: BoxShape.circle, 87 + border: Border.all( 88 + color: isDarkMode ? AppColors.darkPurple : AppColors.lightLavender, 89 + width: 2, 90 + ), 91 + ), 92 + child: Center( 93 + child: Icon( 94 + Ionicons.person_outline, 95 + size: 40, 96 + color: isDarkMode ? AppColors.textLight : AppColors.textSecondary, 97 + ), 98 + ), 99 + ), 100 + Positioned( 101 + right: 0, 102 + bottom: 0, 103 + child: Container( 104 + width: 30, 105 + height: 30, 106 + decoration: BoxDecoration( 107 + shape: BoxShape.circle, 108 + color: AppColors.primary, 109 + border: Border.all( 110 + color: isDarkMode ? AppColors.deepPurple : AppColors.white, 111 + width: 2, 112 + ), 113 + ), 114 + child: const Center( 115 + child: Icon( 116 + CupertinoIcons.plus, 117 + size: 18, 118 + color: AppColors.white, 119 + ), 120 + ), 121 + ), 122 + ), 123 + ], 53 124 ), 54 - ), 55 - child: Center( 56 - child: Icon( 57 - Ionicons.person_outline, 58 - size: 50, 59 - color: isDarkMode ? AppColors.textLight : AppColors.textSecondary, 125 + 126 + const SizedBox(width: 20), 127 + 128 + // Stats row 129 + Expanded( 130 + child: Row( 131 + mainAxisAlignment: MainAxisAlignment.spaceEvenly, 132 + children: const [ 133 + ProfileStatItem(count: '129', label: 'Posts'), 134 + ProfileStatItem(count: '3680', label: 'Followers'), 135 + ProfileStatItem(count: '230', label: 'Following'), 136 + ], 137 + ), 138 + ), 139 + ], 140 + ), 141 + 142 + const SizedBox(height: 16), 143 + 144 + // Username and verified badge 145 + Row( 146 + children: [ 147 + Text( 148 + 'Joe Basser', 149 + style: TextStyle( 150 + fontWeight: FontWeight.bold, 151 + fontSize: 18, 152 + color: AppTheme.getTextColor(context), 153 + ), 60 154 ), 155 + 156 + // Early Supporter badge 157 + if (_isEarlySupporter) ...[ 158 + const SizedBox(width: 8), 159 + GestureDetector( 160 + onTap: () => _showEarlySupporterInfo(context), 161 + child: SvgPicture.asset( 162 + 'assets/images/match.svg', 163 + height: 20, 164 + width: 20, 165 + colorFilter: const ColorFilter.mode( 166 + AppColors.primary, 167 + BlendMode.srcIn 168 + ), 169 + ), 170 + ), 171 + ], 172 + ], 173 + ), 174 + 175 + const SizedBox(height: 4), 176 + 177 + // Username in the format seen in the screenshot 178 + Text( 179 + '@joebasser.sprk.so', 180 + style: TextStyle( 181 + color: AppTheme.getSecondaryTextColor(context), 182 + fontSize: 14, 61 183 ), 62 184 ), 63 185 64 - const SizedBox(height: 12), 186 + const SizedBox(height: 4), 65 187 66 - // Username 188 + // Website 67 189 Text( 68 - '@username', 190 + 'www.website.com', 69 191 style: TextStyle( 70 - fontWeight: FontWeight.bold, 71 - fontSize: 18, 72 - color: AppTheme.getTextColor(context), 192 + color: AppColors.blue, 193 + fontSize: 14, 73 194 ), 74 195 ), 75 196 76 - const SizedBox(height: 20), 197 + const SizedBox(height: 16), 77 198 78 - // Stats 199 + // Action buttons in a row 79 200 Row( 80 - mainAxisAlignment: MainAxisAlignment.spaceEvenly, 81 201 children: [ 82 - _buildStatColumn(context, '150', 'Following'), 83 - _buildStatColumn(context, '1.2M', 'Followers'), 84 - _buildStatColumn(context, '10.5M', 'Likes'), 85 - ], 86 - ), 87 - 88 - const SizedBox(height: 20), 89 - 90 - // Edit profile button 91 - CupertinoButton( 92 - padding: EdgeInsets.zero, 93 - onPressed: () {}, 94 - child: Container( 95 - width: double.infinity, 96 - padding: const EdgeInsets.symmetric(vertical: 12), 97 - decoration: BoxDecoration( 98 - border: Border.all( 99 - color: isDarkMode ? AppColors.lightLavender : AppColors.deepPurple, 202 + // Edit button 203 + Expanded( 204 + flex: 1, 205 + child: ProfileActionButton( 206 + label: 'Edit', 207 + onPressed: () {}, 208 + isPrimary: true, 209 + isOutlined: false, 100 210 ), 101 - borderRadius: BorderRadius.circular(4), 102 211 ), 103 - child: Center( 104 - child: Text( 105 - 'Edit Profile', 106 - style: TextStyle( 107 - fontWeight: FontWeight.bold, 108 - color: AppTheme.getTextColor(context), 212 + 213 + const SizedBox(width: 8), 214 + 215 + // Share Profile button 216 + Expanded( 217 + flex: 1, 218 + child: Container( 219 + constraints: const BoxConstraints(minHeight: 36), 220 + child: ProfileActionButton( 221 + label: 'Share Profile', 222 + onPressed: () {}, 109 223 ), 110 224 ), 111 225 ), 112 - ), 113 - ), 114 - 115 - const SizedBox(height: 8), 116 - 117 - // Bio (if any) 118 - Text( 119 - 'Digital creator | Making cool videos | For business inquiries: email@example.com', 120 - textAlign: TextAlign.center, 121 - style: TextStyle( 122 - color: AppTheme.getSecondaryTextColor(context), 123 - ), 226 + 227 + const SizedBox(width: 8), 228 + 229 + // Friends + button 230 + Expanded( 231 + flex: 1, 232 + child: ProfileActionButton( 233 + label: 'Friends +', 234 + onPressed: () {}, 235 + ), 236 + ), 237 + ], 124 238 ), 125 239 ], 126 240 ), 127 241 ), 128 242 129 - // Tab selector 243 + // Tab bar at the bottom of content 130 244 Container( 131 245 decoration: BoxDecoration( 132 246 border: Border( 133 247 top: BorderSide( 134 - color: isDarkMode ? AppColors.darkPurple : AppColors.divider, 248 + color: AppColors.border, 135 249 width: 0.5, 136 250 ), 137 251 bottom: BorderSide( 138 - color: isDarkMode ? AppColors.darkPurple : AppColors.divider, 252 + color: AppColors.border, 139 253 width: 0.5, 140 254 ), 141 255 ), 142 256 ), 143 - child: Row( 144 - children: [ 145 - _buildTabButton(context, 0, Ionicons.grid_outline), 146 - _buildTabButton(context, 1, Ionicons.heart_outline), 147 - _buildTabButton(context, 2, Ionicons.lock_closed_outline), 148 - ], 257 + child: SingleChildScrollView( 258 + scrollDirection: Axis.horizontal, 259 + child: Row( 260 + mainAxisAlignment: MainAxisAlignment.spaceEvenly, 261 + children: [ 262 + _buildTabItem(context, 0, CupertinoIcons.film), 263 + _buildTabItem(context, 1, CupertinoIcons.heart), 264 + _buildTabItem(context, 2, CupertinoIcons.arrow_2_squarepath), 265 + if (_isOwnProfile) _buildTabItem(context, 3, CupertinoIcons.bookmark), 266 + if (_isOwnProfile) _buildTabItem(context, 4, CupertinoIcons.lock), 267 + ], 268 + ), 149 269 ), 150 270 ), 151 271 152 - // Tab content 272 + // Tab content - with fixed height to prevent scrolling of the entire screen 153 273 Expanded( 154 274 child: _buildTabContent(), 155 275 ), ··· 159 279 ); 160 280 } 161 281 162 - Widget _buildStatColumn(BuildContext context, String count, String label) { 163 - return Column( 164 - children: [ 165 - Text( 166 - count, 167 - style: TextStyle( 168 - fontWeight: FontWeight.bold, 169 - fontSize: 16, 170 - color: AppTheme.getTextColor(context), 171 - ), 172 - ), 173 - const SizedBox(height: 4), 174 - Text( 175 - label, 176 - style: TextStyle( 177 - color: AppTheme.getSecondaryTextColor(context), 178 - ), 179 - ), 180 - ], 181 - ); 182 - } 183 - 184 - Widget _buildTabButton(BuildContext context, int index, IconData icon) { 282 + Widget _buildTabItem(BuildContext context, int index, IconData icon) { 185 283 final brightness = MediaQuery.of(context).platformBrightness; 186 284 final isDarkMode = brightness == Brightness.dark; 187 285 final isSelected = _selectedTabIndex == index; 188 286 189 - return Expanded( 190 - child: CupertinoButton( 191 - padding: EdgeInsets.zero, 192 - onPressed: () { 193 - setState(() { 194 - _selectedTabIndex = index; 195 - }); 196 - }, 197 - child: Container( 198 - padding: const EdgeInsets.symmetric(vertical: 12), 199 - decoration: BoxDecoration( 200 - border: Border( 201 - bottom: BorderSide( 202 - color: isSelected 203 - ? AppColors.primary 204 - : Colors.transparent, 205 - width: 2, 206 - ), 287 + // Get filled icon variants based on the outline icon 288 + IconData getFilledIcon(IconData outlineIcon) { 289 + if (outlineIcon == CupertinoIcons.film) { 290 + return CupertinoIcons.film_fill; 291 + } else if (outlineIcon == CupertinoIcons.heart) { 292 + return CupertinoIcons.heart_fill; 293 + } else if (outlineIcon == CupertinoIcons.arrow_2_squarepath) { 294 + return CupertinoIcons.arrow_2_squarepath; 295 + } else if (outlineIcon == CupertinoIcons.bookmark) { 296 + return CupertinoIcons.bookmark_fill; 297 + } else if (outlineIcon == CupertinoIcons.lock) { 298 + return CupertinoIcons.lock_fill; 299 + } else { 300 + return outlineIcon; 301 + } 302 + } 303 + 304 + return CupertinoButton( 305 + padding: EdgeInsets.zero, 306 + onPressed: () { 307 + setState(() { 308 + _selectedTabIndex = index; 309 + }); 310 + }, 311 + child: Container( 312 + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), 313 + decoration: BoxDecoration( 314 + border: Border( 315 + bottom: BorderSide( 316 + color: isSelected 317 + ? AppColors.primary 318 + : Colors.transparent, 319 + width: 2, 207 320 ), 208 321 ), 209 - child: Icon( 210 - icon, 211 - color: isSelected 212 - ? AppColors.primary 213 - : (isDarkMode ? AppColors.textLight : AppColors.textSecondary), 214 - ), 322 + ), 323 + child: Icon( 324 + isSelected ? getFilledIcon(icon) : icon, 325 + color: isSelected 326 + ? AppColors.primary 327 + : (isDarkMode ? AppColors.textLight : AppColors.textSecondary), 328 + size: 26, 215 329 ), 216 330 ), 217 331 ); 218 332 } 219 333 220 334 Widget _buildTabContent() { 221 - final brightness = MediaQuery.of(context).platformBrightness; 222 - final isDarkMode = brightness == Brightness.dark; 223 - 224 335 switch (_selectedTabIndex) { 225 336 case 0: 226 - return _buildVideosGrid(); 337 + return _buildPostsGrid(); 227 338 case 1: 228 - return _buildLikedGrid(); 339 + return const VideosGrid( 340 + itemCount: 15, 341 + iconType: Ionicons.heart_outline, 342 + ); 229 343 case 2: 344 + return const VideosGrid( 345 + itemCount: 8, 346 + iconType: CupertinoIcons.arrow_2_squarepath, 347 + ); 348 + case 3: 349 + return const VideosGrid( 350 + itemCount: 12, 351 + iconType: Ionicons.bookmark_outline, 352 + ); 353 + case 4: 230 354 return _buildPrivateTab(); 231 355 default: 232 356 return const SizedBox.shrink(); 233 357 } 234 358 } 235 359 236 - Widget _buildVideosGrid() { 360 + Widget _buildPostsGrid() { 237 361 return GridView.builder( 362 + physics: const NeverScrollableScrollPhysics(), // Prevents scrolling within the grid 238 363 padding: const EdgeInsets.all(1), 239 364 gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 240 365 crossAxisCount: 3, ··· 242 367 crossAxisSpacing: 1, 243 368 mainAxisSpacing: 1, 244 369 ), 245 - itemCount: 15, 370 + itemCount: 12, 246 371 itemBuilder: (context, index) { 247 - return Container( 248 - color: index % 3 == 0 249 - ? AppColors.richPurple.withOpacity(0.7) 250 - : index % 3 == 1 251 - ? AppColors.brightPurple.withOpacity(0.7) 252 - : AppColors.primary.withOpacity(0.7), 253 - child: Stack( 254 - children: [ 255 - Center( 256 - child: Icon( 257 - Ionicons.play_outline, 258 - color: AppColors.white.withOpacity(0.8), 372 + // Alternate between video and image posts 373 + final bool isVideo = index % 2 == 0; 374 + 375 + return GestureDetector( 376 + onTap: () { 377 + debugPrint('Post clicked: ${isVideo ? "Video" : "Image"} at index $index'); 378 + }, 379 + child: Container( 380 + color: isVideo 381 + ? AppColors.richPurple.withOpacity(0.7) 382 + : AppColors.orange.withOpacity(0.7), 383 + child: Stack( 384 + children: [ 385 + Center( 386 + child: Icon( 387 + isVideo ? CupertinoIcons.film : CupertinoIcons.photo, 388 + color: AppColors.white.withOpacity(0.8), 389 + size: 24, 390 + ), 259 391 ), 260 - ), 261 - Positioned( 262 - bottom: 5, 263 - left: 5, 264 - child: Row( 265 - children: [ 266 - const Icon( 267 - Ionicons.eye_outline, 268 - color: AppColors.white, 269 - size: 12, 270 - ), 271 - const SizedBox(width: 4), 272 - Text( 273 - '${(index + 1) * 1000}', 274 - style: const TextStyle( 392 + Positioned( 393 + bottom: 5, 394 + left: 5, 395 + child: Row( 396 + children: [ 397 + Icon( 398 + isVideo ? CupertinoIcons.eye : CupertinoIcons.heart, 275 399 color: AppColors.white, 276 - fontSize: 12, 400 + size: 12, 401 + ), 402 + const SizedBox(width: 4), 403 + Text( 404 + '${(index + 1) * 1000}', 405 + style: const TextStyle( 406 + color: AppColors.white, 407 + fontSize: 12, 408 + ), 277 409 ), 278 - ), 279 - ], 410 + ], 411 + ), 280 412 ), 281 - ), 282 - ], 283 - ), 284 - ); 285 - }, 286 - ); 287 - } 288 - 289 - Widget _buildLikedGrid() { 290 - return GridView.builder( 291 - padding: const EdgeInsets.all(1), 292 - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 293 - crossAxisCount: 3, 294 - childAspectRatio: 2/3, 295 - crossAxisSpacing: 1, 296 - mainAxisSpacing: 1, 297 - ), 298 - itemCount: 9, 299 - itemBuilder: (context, index) { 300 - return Container( 301 - color: index % 3 == 0 302 - ? AppColors.orange.withOpacity(0.7) 303 - : index % 3 == 1 304 - ? AppColors.primary.withOpacity(0.7) 305 - : AppColors.red.withOpacity(0.7), 306 - child: Stack( 307 - children: [ 308 - Center( 309 - child: Icon( 310 - Ionicons.play_outline, 311 - color: AppColors.white.withOpacity(0.8), 312 - ), 313 - ), 314 - Positioned( 315 - bottom: 5, 316 - left: 5, 317 - child: Row( 318 - children: [ 319 - const Icon( 320 - Ionicons.heart_outline, 321 - color: AppColors.white, 322 - size: 12, 323 - ), 324 - const SizedBox(width: 4), 325 - Text( 326 - '${(index + 1) * 1000}', 327 - style: const TextStyle( 328 - color: AppColors.white, 329 - fontSize: 12, 413 + if (isVideo) 414 + Positioned( 415 + top: 5, 416 + right: 5, 417 + child: Container( 418 + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), 419 + decoration: BoxDecoration( 420 + color: AppColors.black.withOpacity(0.5), 421 + borderRadius: BorderRadius.circular(4), 422 + ), 423 + child: const Text( 424 + '0:30', 425 + style: TextStyle( 426 + color: AppColors.white, 427 + fontSize: 10, 428 + ), 330 429 ), 331 430 ), 332 - ], 333 - ), 334 - ), 335 - ], 431 + ), 432 + ], 433 + ), 336 434 ), 337 435 ); 338 436 }, ··· 345 443 mainAxisAlignment: MainAxisAlignment.center, 346 444 children: [ 347 445 Icon( 348 - Ionicons.lock_closed_outline, 446 + CupertinoIcons.lock, 349 447 size: 60, 350 448 color: AppTheme.getSecondaryTextColor(context), 351 449 ),
+3
lib/utils/app_colors.dart
··· 10 10 static const Color brightPurple = Color(0xFFB20AFF); 11 11 static const Color pink = Color(0xFFFF2696); // Main app color for buttons and highlights 12 12 static const Color white = Color(0xFFFFFFFF); 13 + static const Color black = Color(0xFF000000); 13 14 14 15 // New colors 15 16 static const Color blue = Color(0xFF0073FF); // For followers 17 + static const Color lightBlue = Color(0xFF40A9FF); // Lighter blue variant 18 + static const Color teal = Color(0xFF00C9B8); // For bookmarks 16 19 static const Color green = Color(0xFF12DB59); // For comments 17 20 static const Color red = Color(0xFFFF3A2C); // For likes 18 21 static const Color orange = Color(0xFFFF7B00); // For alerts and danger
+112
lib/widgets/profile/early_supporter_sheet.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:flutter_svg/flutter_svg.dart'; 3 + import '../../utils/app_colors.dart'; 4 + 5 + class EarlySupporterSheet extends StatelessWidget { 6 + const EarlySupporterSheet({super.key}); 7 + 8 + @override 9 + Widget build(BuildContext context) { 10 + final brightness = MediaQuery.of(context).platformBrightness; 11 + final isDarkMode = brightness == Brightness.dark; 12 + 13 + return Container( 14 + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), 15 + decoration: BoxDecoration( 16 + color: isDarkMode ? AppColors.deepPurple : CupertinoColors.white, 17 + borderRadius: const BorderRadius.only( 18 + topLeft: Radius.circular(16), 19 + topRight: Radius.circular(16), 20 + ), 21 + ), 22 + child: Column( 23 + mainAxisSize: MainAxisSize.min, 24 + children: [ 25 + // Drag handle 26 + Container( 27 + width: 40, 28 + height: 4, 29 + margin: const EdgeInsets.only(bottom: 24), 30 + decoration: BoxDecoration( 31 + color: CupertinoColors.systemGrey4, 32 + borderRadius: BorderRadius.circular(2), 33 + ), 34 + ), 35 + 36 + // Match icon 37 + SvgPicture.asset( 38 + 'assets/images/match.svg', 39 + height: 48, 40 + width: 48, 41 + colorFilter: const ColorFilter.mode(AppColors.primary, BlendMode.srcIn), 42 + ), 43 + 44 + const SizedBox(height: 16), 45 + 46 + // Title 47 + const Text( 48 + 'Early supporter', 49 + style: TextStyle( 50 + fontSize: 24, 51 + fontWeight: FontWeight.bold, 52 + color: AppColors.primary, 53 + ), 54 + ), 55 + 56 + const SizedBox(height: 24), 57 + 58 + // Description 59 + Text.rich( 60 + TextSpan( 61 + style: TextStyle( 62 + fontSize: 16, 63 + color: isDarkMode ? CupertinoColors.systemGrey : CupertinoColors.systemGrey2, 64 + height: 1.5, 65 + ), 66 + children: const [ 67 + TextSpan(text: 'This person was one of the '), 68 + TextSpan( 69 + text: 'first matches to light our Spark', 70 + style: TextStyle( 71 + color: AppColors.primary, 72 + fontWeight: FontWeight.bold, 73 + ), 74 + ), 75 + TextSpan( 76 + text: '. Thanks to them for believing in us and supporting us when we were just an idea.', 77 + ), 78 + ], 79 + ), 80 + textAlign: TextAlign.center, 81 + ), 82 + 83 + const SizedBox(height: 24), 84 + 85 + // Bottom message 86 + Text.rich( 87 + TextSpan( 88 + style: TextStyle( 89 + fontSize: 16, 90 + color: isDarkMode ? CupertinoColors.systemGrey : CupertinoColors.systemGrey2, 91 + ), 92 + children: const [ 93 + TextSpan(text: 'Thanks to them, '), 94 + TextSpan( 95 + text: 'Spark is a reality', 96 + style: TextStyle( 97 + color: AppColors.primary, 98 + fontWeight: FontWeight.bold, 99 + ), 100 + ), 101 + TextSpan(text: '.'), 102 + ], 103 + ), 104 + textAlign: TextAlign.center, 105 + ), 106 + 107 + const SizedBox(height: 40), 108 + ], 109 + ), 110 + ); 111 + } 112 + }
+74
lib/widgets/profile/profile_action_button.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import '../../utils/app_colors.dart'; 3 + import '../../utils/app_theme.dart'; 4 + 5 + class ProfileActionButton extends StatelessWidget { 6 + final String label; 7 + final IconData? icon; 8 + final VoidCallback onPressed; 9 + final bool isPrimary; 10 + final bool isOutlined; 11 + 12 + const ProfileActionButton({ 13 + super.key, 14 + required this.label, 15 + this.icon, 16 + required this.onPressed, 17 + this.isPrimary = false, 18 + this.isOutlined = true, 19 + }); 20 + 21 + @override 22 + Widget build(BuildContext context) { 23 + final brightness = MediaQuery.of(context).platformBrightness; 24 + final isDarkMode = brightness == Brightness.dark; 25 + 26 + return CupertinoButton( 27 + padding: EdgeInsets.zero, 28 + minSize: 0, 29 + onPressed: onPressed, 30 + child: Container( 31 + width: double.infinity, 32 + height: 36, 33 + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4), 34 + decoration: BoxDecoration( 35 + color: isPrimary ? AppColors.primary : CupertinoColors.transparent, 36 + border: isOutlined ? Border.all( 37 + color: isDarkMode ? AppColors.lightLavender : AppColors.deepPurple, 38 + width: 1, 39 + ) : null, 40 + borderRadius: BorderRadius.circular(4), 41 + ), 42 + child: Center( 43 + child: Row( 44 + mainAxisSize: MainAxisSize.min, 45 + mainAxisAlignment: MainAxisAlignment.center, 46 + children: [ 47 + if (icon != null) ...[ 48 + Icon( 49 + icon, 50 + size: 14, 51 + color: isPrimary ? AppColors.white : AppTheme.getTextColor(context), 52 + ), 53 + const SizedBox(width: 2), 54 + ], 55 + Flexible( 56 + child: Text( 57 + label, 58 + style: TextStyle( 59 + fontWeight: FontWeight.bold, 60 + fontSize: 13, 61 + color: isPrimary ? AppColors.white : AppTheme.getTextColor(context), 62 + ), 63 + maxLines: 1, 64 + overflow: TextOverflow.ellipsis, 65 + textAlign: TextAlign.center, 66 + ), 67 + ), 68 + ], 69 + ), 70 + ), 71 + ), 72 + ); 73 + } 74 + }
+40
lib/widgets/profile/profile_stat_item.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import '../../utils/app_colors.dart'; 3 + import '../../utils/app_theme.dart'; 4 + 5 + class ProfileStatItem extends StatelessWidget { 6 + final String count; 7 + final String label; 8 + 9 + const ProfileStatItem({ 10 + super.key, 11 + required this.count, 12 + required this.label, 13 + }); 14 + 15 + @override 16 + Widget build(BuildContext context) { 17 + return Column( 18 + mainAxisSize: MainAxisSize.min, 19 + mainAxisAlignment: MainAxisAlignment.center, 20 + children: [ 21 + Text( 22 + count, 23 + style: TextStyle( 24 + fontWeight: FontWeight.bold, 25 + fontSize: 20, 26 + color: AppTheme.getTextColor(context), 27 + ), 28 + ), 29 + const SizedBox(height: 4), 30 + Text( 31 + label, 32 + style: TextStyle( 33 + color: AppTheme.getSecondaryTextColor(context), 34 + fontSize: 14, 35 + ), 36 + ), 37 + ], 38 + ); 39 + } 40 + }
+61
lib/widgets/profile/profile_tab_bar.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:flutter/material.dart' show Colors; 3 + import 'package:ionicons/ionicons.dart'; 4 + import '../../utils/app_colors.dart'; 5 + import '../../utils/app_theme.dart'; 6 + 7 + class ProfileTabBar extends StatelessWidget { 8 + final int selectedIndex; 9 + final Function(int) onTabSelected; 10 + 11 + const ProfileTabBar({ 12 + super.key, 13 + required this.selectedIndex, 14 + required this.onTabSelected, 15 + }); 16 + 17 + @override 18 + Widget build(BuildContext context) { 19 + return Container( 20 + decoration: BoxDecoration( 21 + border: Border( 22 + bottom: BorderSide( 23 + color: AppColors.border, 24 + width: 0.5, 25 + ), 26 + ), 27 + ), 28 + child: Row( 29 + mainAxisAlignment: MainAxisAlignment.spaceEvenly, 30 + children: [ 31 + _buildTabItem(context, 0, CupertinoIcons.heart), 32 + _buildTabItem(context, 1, CupertinoIcons.bookmark), 33 + _buildTabItem(context, 2, CupertinoIcons.refresh), 34 + _buildTabItem(context, 3, CupertinoIcons.photo), 35 + _buildTabItem(context, 4, CupertinoIcons.person_2), 36 + ], 37 + ), 38 + ); 39 + } 40 + 41 + Widget _buildTabItem(BuildContext context, int index, IconData icon) { 42 + final brightness = MediaQuery.of(context).platformBrightness; 43 + final isDarkMode = brightness == Brightness.dark; 44 + final isSelected = selectedIndex == index; 45 + 46 + return CupertinoButton( 47 + padding: EdgeInsets.zero, 48 + onPressed: () => onTabSelected(index), 49 + child: Container( 50 + padding: const EdgeInsets.symmetric(vertical: 12), 51 + child: Icon( 52 + icon, 53 + color: isSelected 54 + ? AppColors.primary 55 + : (isDarkMode ? AppColors.textLight : AppColors.textSecondary), 56 + size: 26, 57 + ), 58 + ), 59 + ); 60 + } 61 + }
+63
lib/widgets/profile/video_thumbnail.dart
··· 1 + import 'dart:developer'; 2 + import 'package:flutter/cupertino.dart'; 3 + import 'package:ionicons/ionicons.dart'; 4 + import '../../utils/app_colors.dart'; 5 + 6 + class VideoThumbnail extends StatelessWidget { 7 + final int index; 8 + final Color backgroundColor; 9 + final IconData icon; 10 + final String viewCount; 11 + 12 + const VideoThumbnail({ 13 + super.key, 14 + required this.index, 15 + required this.backgroundColor, 16 + this.icon = Ionicons.play_outline, 17 + required this.viewCount, 18 + }); 19 + 20 + @override 21 + Widget build(BuildContext context) { 22 + return GestureDetector( 23 + onTap: () { 24 + log('Video thumbnail clicked: index $index'); 25 + }, 26 + child: Container( 27 + color: backgroundColor, 28 + child: Stack( 29 + children: [ 30 + Center( 31 + child: Icon( 32 + icon, 33 + color: AppColors.white.withOpacity(0.8), 34 + size: 24, 35 + ), 36 + ), 37 + Positioned( 38 + bottom: 5, 39 + left: 5, 40 + child: Row( 41 + children: [ 42 + const Icon( 43 + Ionicons.eye_outline, 44 + color: AppColors.white, 45 + size: 12, 46 + ), 47 + const SizedBox(width: 4), 48 + Text( 49 + viewCount, 50 + style: const TextStyle( 51 + color: AppColors.white, 52 + fontSize: 12, 53 + ), 54 + ), 55 + ], 56 + ), 57 + ), 58 + ], 59 + ), 60 + ), 61 + ); 62 + } 63 + }
+66
lib/widgets/profile/videos_grid.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:ionicons/ionicons.dart'; 3 + import '../../utils/app_colors.dart'; 4 + import 'video_thumbnail.dart'; 5 + 6 + class VideosGrid extends StatelessWidget { 7 + final int itemCount; 8 + final IconData iconType; 9 + 10 + const VideosGrid({ 11 + super.key, 12 + required this.itemCount, 13 + this.iconType = Ionicons.play_outline, 14 + }); 15 + 16 + @override 17 + Widget build(BuildContext context) { 18 + return GridView.builder( 19 + physics: const NeverScrollableScrollPhysics(), 20 + padding: const EdgeInsets.all(1), 21 + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 22 + crossAxisCount: 3, 23 + childAspectRatio: 2/3, 24 + crossAxisSpacing: 1, 25 + mainAxisSpacing: 1, 26 + ), 27 + itemCount: itemCount, 28 + itemBuilder: (context, index) { 29 + // Create different color patterns based on the icon type 30 + Color backgroundColor; 31 + if (iconType == Ionicons.heart_outline || iconType == CupertinoIcons.heart) { 32 + backgroundColor = index % 3 == 0 33 + ? AppColors.orange.withOpacity(0.7) 34 + : index % 3 == 1 35 + ? AppColors.primary.withOpacity(0.7) 36 + : AppColors.red.withOpacity(0.7); 37 + } else if (iconType == Ionicons.bookmark_outline || iconType == CupertinoIcons.bookmark) { 38 + backgroundColor = index % 3 == 0 39 + ? AppColors.teal.withOpacity(0.7) 40 + : index % 3 == 1 41 + ? AppColors.blue.withOpacity(0.7) 42 + : AppColors.lightBlue.withOpacity(0.7); 43 + } else if (iconType == CupertinoIcons.arrow_2_squarepath) { 44 + backgroundColor = index % 3 == 0 45 + ? AppColors.green.withOpacity(0.7) 46 + : index % 3 == 1 47 + ? AppColors.blue.withOpacity(0.7) 48 + : AppColors.primary.withOpacity(0.7); 49 + } else { 50 + backgroundColor = index % 3 == 0 51 + ? AppColors.richPurple.withOpacity(0.7) 52 + : index % 3 == 1 53 + ? AppColors.brightPurple.withOpacity(0.7) 54 + : AppColors.primary.withOpacity(0.7); 55 + } 56 + 57 + return VideoThumbnail( 58 + index: index, 59 + backgroundColor: backgroundColor, 60 + icon: iconType, 61 + viewCount: '${(index + 1) * 1000}', 62 + ); 63 + }, 64 + ); 65 + } 66 + }
+40
pubspec.lock
··· 238 238 url: "https://pub.dev" 239 239 source: hosted 240 240 version: "2.0.27" 241 + flutter_svg: 242 + dependency: "direct main" 243 + description: 244 + name: flutter_svg 245 + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b 246 + url: "https://pub.dev" 247 + source: hosted 248 + version: "2.0.17" 241 249 flutter_test: 242 250 dependency: "direct dev" 243 251 description: flutter ··· 376 384 url: "https://pub.dev" 377 385 source: hosted 378 386 version: "1.9.1" 387 + path_parsing: 388 + dependency: transitive 389 + description: 390 + name: path_parsing 391 + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" 392 + url: "https://pub.dev" 393 + source: hosted 394 + version: "1.1.0" 379 395 path_provider: 380 396 dependency: "direct main" 381 397 description: ··· 605 621 url: "https://pub.dev" 606 622 source: hosted 607 623 version: "4.5.1" 624 + vector_graphics: 625 + dependency: transitive 626 + description: 627 + name: vector_graphics 628 + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" 629 + url: "https://pub.dev" 630 + source: hosted 631 + version: "1.1.18" 632 + vector_graphics_codec: 633 + dependency: transitive 634 + description: 635 + name: vector_graphics_codec 636 + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" 637 + url: "https://pub.dev" 638 + source: hosted 639 + version: "1.1.13" 640 + vector_graphics_compiler: 641 + dependency: transitive 642 + description: 643 + name: vector_graphics_compiler 644 + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" 645 + url: "https://pub.dev" 646 + source: hosted 647 + version: "1.1.16" 608 648 vector_math: 609 649 dependency: transitive 610 650 description:
+1
pubspec.yaml
··· 17 17 path_provider: ^2.1.2 18 18 provider: ^6.1.1 19 19 flutter_launcher_icons: ^0.14.3 20 + flutter_svg: ^2.0.7 20 21 21 22 dev_dependencies: 22 23 flutter_test: