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

Modules

Createad the modules for feed, the right buttons and the bottom content featuring the description of the video and the username.

C3B 0de91e4d 5c7d73e0

+448 -129
+6 -20
lib/main.dart
··· 151 151 onPressed: () { 152 152 navigationProvider.updateIndex(index); 153 153 }, 154 - child: Column( 155 - mainAxisSize: MainAxisSize.min, 156 - children: [ 157 - Icon( 158 - isSelected ? iconFilled : iconOutline, 159 - color: isSelected 160 - ? (isDarkMode ? CupertinoColors.white : CupertinoColors.activeBlue) 161 - : (isDarkMode ? CupertinoColors.systemGrey : CupertinoColors.systemGrey), 162 - ), 163 - const SizedBox(height: 2), 164 - Text( 165 - label, 166 - style: TextStyle( 167 - fontSize: 10, 168 - color: isSelected 169 - ? (isDarkMode ? CupertinoColors.white : CupertinoColors.activeBlue) 170 - : (isDarkMode ? CupertinoColors.systemGrey : CupertinoColors.systemGrey), 171 - ), 172 - ), 173 - ], 154 + child: Icon( 155 + isSelected ? iconFilled : iconOutline, 156 + color: isSelected 157 + ? (isDarkMode ? CupertinoColors.white : CupertinoColors.activeBlue) 158 + : (isDarkMode ? CupertinoColors.systemGrey : CupertinoColors.systemGrey), 159 + size: 26, // Slightly larger icon since we removed the text 174 160 ), 175 161 ); 176 162 }
+40 -109
lib/screens/home_screen.dart
··· 1 1 import 'package:flutter/cupertino.dart'; 2 2 import 'package:ionicons/ionicons.dart'; 3 3 import 'package:cached_network_image/cached_network_image.dart'; 4 + import '../widgets/video_side_action_bar.dart'; 5 + import '../widgets/video_info/video_info_bar.dart'; 4 6 5 7 class HomeScreen extends StatelessWidget { 6 8 const HomeScreen({super.key}); ··· 84 86 85 87 @override 86 88 Widget build(BuildContext context) { 89 + // Sample data for the video item 90 + final String username = 'username'; 91 + final String description = 'Video caption goes here'; 92 + final List<String> hashtags = ['tiktok', 'viral', 'trending']; 93 + 87 94 return Container( 88 95 // Use constraints to ensure the video fits within available space 89 96 constraints: BoxConstraints( ··· 107 114 ), 108 115 ), 109 116 110 - // Video info 117 + // Video info - now using the modular component 111 118 Positioned( 112 119 bottom: 20, 113 120 left: 10, 114 - right: 70, 115 - child: Column( 116 - crossAxisAlignment: CrossAxisAlignment.start, 117 - children: [ 118 - Row( 119 - children: [ 120 - ClipOval( 121 - child: Container( 122 - width: 40, 123 - height: 40, 124 - color: CupertinoColors.systemGrey, 125 - child: const Center( 126 - child: Icon(Ionicons.person_outline, color: CupertinoColors.white), 127 - ), 128 - ), 129 - ), 130 - const SizedBox(width: 10), 131 - const Text( 132 - '@username', 133 - style: TextStyle( 134 - color: CupertinoColors.white, 135 - fontWeight: FontWeight.bold, 136 - ), 137 - ), 138 - const SizedBox(width: 10), 139 - Container( 140 - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 141 - decoration: BoxDecoration( 142 - border: Border.all(color: CupertinoColors.white), 143 - borderRadius: BorderRadius.circular(10), 144 - ), 145 - child: const Text( 146 - 'Follow', 147 - style: TextStyle( 148 - color: CupertinoColors.white, 149 - fontSize: 12, 150 - ), 151 - ), 152 - ), 153 - ], 154 - ), 155 - const SizedBox(height: 10), 156 - const Text( 157 - 'Video caption goes here #tiktok #viral #trending', 158 - style: TextStyle(color: CupertinoColors.white), 159 - maxLines: 2, 160 - overflow: TextOverflow.ellipsis, 161 - ), 162 - ], 121 + right: 70, // Give space for the side action bar 122 + child: VideoInfoBar( 123 + username: username, 124 + description: description, 125 + hashtags: hashtags, 126 + onUsernameTap: () { 127 + // Handle username tap 128 + }, 129 + onHashtagTap: () { 130 + // Handle hashtag tap 131 + }, 163 132 ), 164 133 ), 165 134 ··· 167 136 Positioned( 168 137 right: 10, 169 138 bottom: 100, 170 - child: Column( 171 - children: [ 172 - // Like button 173 - Column( 174 - children: [ 175 - const Icon( 176 - Ionicons.heart_outline, 177 - color: CupertinoColors.white, 178 - size: 30, 179 - ), 180 - const SizedBox(height: 4), 181 - Text( 182 - '${(index + 1) * 1000}', 183 - style: const TextStyle( 184 - color: CupertinoColors.white, 185 - fontSize: 12, 186 - ), 187 - ), 188 - ], 189 - ), 190 - const SizedBox(height: 20), 191 - // Comment button 192 - Column( 193 - children: [ 194 - const Icon( 195 - Ionicons.chatbubble_outline, 196 - color: CupertinoColors.white, 197 - size: 30, 198 - ), 199 - const SizedBox(height: 4), 200 - Text( 201 - '${(index + 1) * 100}', 202 - style: const TextStyle( 203 - color: CupertinoColors.white, 204 - fontSize: 12, 205 - ), 206 - ), 207 - ], 208 - ), 209 - const SizedBox(height: 20), 210 - // Share button 211 - Column( 212 - children: [ 213 - const Icon( 214 - Ionicons.arrow_redo_outline, 215 - color: CupertinoColors.white, 216 - size: 30, 217 - ), 218 - const SizedBox(height: 4), 219 - Text( 220 - '${(index + 1) * 10}', 221 - style: const TextStyle( 222 - color: CupertinoColors.white, 223 - fontSize: 12, 224 - ), 225 - ), 226 - ], 227 - ), 228 - ], 139 + child: VideoSideActionBar( 140 + likeCount: '250,5K', 141 + commentCount: '100K', 142 + bookmarkCount: '89K', 143 + shareCount: '132,5K', 144 + // Add any callbacks as needed 145 + onLikePressed: () { 146 + // Handle like action 147 + }, 148 + onCommentPressed: () { 149 + // Handle comment action 150 + }, 151 + onBookmarkPressed: () { 152 + // Handle bookmark action 153 + }, 154 + onSharePressed: () { 155 + // Handle share action 156 + }, 157 + onProfilePressed: () { 158 + // Handle profile action 159 + }, 229 160 ), 230 161 ), 231 162 ],
+38
lib/widgets/action_buttons/action_button.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + 3 + class ActionButton extends StatelessWidget { 4 + final IconData icon; 5 + final String label; 6 + final VoidCallback? onPressed; 7 + 8 + const ActionButton({ 9 + super.key, 10 + required this.icon, 11 + required this.label, 12 + this.onPressed, 13 + }); 14 + 15 + @override 16 + Widget build(BuildContext context) { 17 + return Column( 18 + children: [ 19 + GestureDetector( 20 + onTap: onPressed, 21 + child: Icon( 22 + icon, 23 + color: CupertinoColors.white, 24 + size: 30, 25 + ), 26 + ), 27 + const SizedBox(height: 4), 28 + Text( 29 + label, 30 + style: const TextStyle( 31 + color: CupertinoColors.white, 32 + fontSize: 12, 33 + ), 34 + ), 35 + ], 36 + ); 37 + } 38 + }
+25
lib/widgets/action_buttons/bookmark_action_button.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:ionicons/ionicons.dart'; 3 + import 'action_button.dart'; 4 + 5 + class BookmarkActionButton extends StatelessWidget { 6 + final String count; 7 + final bool isBookmarked; 8 + final VoidCallback? onPressed; 9 + 10 + const BookmarkActionButton({ 11 + super.key, 12 + required this.count, 13 + this.isBookmarked = false, 14 + this.onPressed, 15 + }); 16 + 17 + @override 18 + Widget build(BuildContext context) { 19 + return ActionButton( 20 + icon: isBookmarked ? Ionicons.bookmark : Ionicons.bookmark_outline, 21 + label: count, 22 + onPressed: onPressed, 23 + ); 24 + } 25 + }
+23
lib/widgets/action_buttons/comment_action_button.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:ionicons/ionicons.dart'; 3 + import 'action_button.dart'; 4 + 5 + class CommentActionButton extends StatelessWidget { 6 + final String count; 7 + final VoidCallback? onPressed; 8 + 9 + const CommentActionButton({ 10 + super.key, 11 + required this.count, 12 + this.onPressed, 13 + }); 14 + 15 + @override 16 + Widget build(BuildContext context) { 17 + return ActionButton( 18 + icon: Ionicons.chatbubble_outline, 19 + label: count, 20 + onPressed: onPressed, 21 + ); 22 + } 23 + }
+25
lib/widgets/action_buttons/like_action_button.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:ionicons/ionicons.dart'; 3 + import 'action_button.dart'; 4 + 5 + class LikeActionButton extends StatelessWidget { 6 + final String count; 7 + final bool isLiked; 8 + final VoidCallback? onPressed; 9 + 10 + const LikeActionButton({ 11 + super.key, 12 + required this.count, 13 + this.isLiked = false, 14 + this.onPressed, 15 + }); 16 + 17 + @override 18 + Widget build(BuildContext context) { 19 + return ActionButton( 20 + icon: isLiked ? Ionicons.heart : Ionicons.heart_outline, 21 + label: count, 22 + onPressed: onPressed, 23 + ); 24 + } 25 + }
+49
lib/widgets/action_buttons/profile_action_button.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:ionicons/ionicons.dart'; 3 + 4 + class ProfileActionButton extends StatelessWidget { 5 + final VoidCallback? onPressed; 6 + 7 + const ProfileActionButton({ 8 + super.key, 9 + this.onPressed, 10 + }); 11 + 12 + @override 13 + Widget build(BuildContext context) { 14 + return GestureDetector( 15 + onTap: onPressed, 16 + child: Stack( 17 + children: [ 18 + ClipOval( 19 + child: Container( 20 + width: 44, 21 + height: 44, 22 + color: CupertinoColors.systemGrey, 23 + child: const Center( 24 + child: Icon(Ionicons.person_outline, color: CupertinoColors.white), 25 + ), 26 + ), 27 + ), 28 + Positioned( 29 + bottom: 0, 30 + right: 0, 31 + child: Container( 32 + width: 20, 33 + height: 20, 34 + decoration: const BoxDecoration( 35 + color: CupertinoColors.systemPink, 36 + shape: BoxShape.circle, 37 + ), 38 + child: const Icon( 39 + CupertinoIcons.add, 40 + color: CupertinoColors.white, 41 + size: 14, 42 + ), 43 + ), 44 + ), 45 + ], 46 + ), 47 + ); 48 + } 49 + }
+23
lib/widgets/action_buttons/share_action_button.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:ionicons/ionicons.dart'; 3 + import 'action_button.dart'; 4 + 5 + class ShareActionButton extends StatelessWidget { 6 + final String count; 7 + final VoidCallback? onPressed; 8 + 9 + const ShareActionButton({ 10 + super.key, 11 + required this.count, 12 + this.onPressed, 13 + }); 14 + 15 + @override 16 + Widget build(BuildContext context) { 17 + return ActionButton( 18 + icon: Ionicons.arrow_redo_outline, 19 + label: count, 20 + onPressed: onPressed, 21 + ); 22 + } 23 + }
+35
lib/widgets/video_info/hashtag_list.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + 3 + class HashtagList extends StatelessWidget { 4 + final List<String> hashtags; 5 + final TextStyle? style; 6 + final VoidCallback? onHashtagTap; 7 + 8 + const HashtagList({ 9 + super.key, 10 + required this.hashtags, 11 + this.style, 12 + this.onHashtagTap, 13 + }); 14 + 15 + @override 16 + Widget build(BuildContext context) { 17 + return Wrap( 18 + spacing: 4.0, 19 + runSpacing: 4.0, 20 + children: hashtags.map((tag) { 21 + return GestureDetector( 22 + onTap: onHashtagTap, 23 + child: Text( 24 + '#$tag', 25 + style: style ?? const TextStyle( 26 + color: CupertinoColors.white, 27 + fontWeight: FontWeight.w500, 28 + fontSize: 13, 29 + ), 30 + ), 31 + ); 32 + }).toList(), 33 + ); 34 + } 35 + }
+24
lib/widgets/video_info/username_label.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + 3 + class UsernameLabel extends StatelessWidget { 4 + final String username; 5 + final TextStyle? style; 6 + 7 + const UsernameLabel({ 8 + super.key, 9 + required this.username, 10 + this.style, 11 + }); 12 + 13 + @override 14 + Widget build(BuildContext context) { 15 + return Text( 16 + '@$username', 17 + style: style ?? const TextStyle( 18 + color: CupertinoColors.white, 19 + fontWeight: FontWeight.bold, 20 + fontSize: 14, 21 + ), 22 + ); 23 + } 24 + }
+29
lib/widgets/video_info/video_description.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + 3 + class VideoDescription extends StatelessWidget { 4 + final String text; 5 + final TextStyle? style; 6 + final int maxLines; 7 + final TextOverflow overflow; 8 + 9 + const VideoDescription({ 10 + super.key, 11 + required this.text, 12 + this.style, 13 + this.maxLines = 2, 14 + this.overflow = TextOverflow.ellipsis, 15 + }); 16 + 17 + @override 18 + Widget build(BuildContext context) { 19 + return Text( 20 + text, 21 + style: style ?? const TextStyle( 22 + color: CupertinoColors.white, 23 + fontSize: 13, 24 + ), 25 + maxLines: maxLines, 26 + overflow: overflow, 27 + ); 28 + } 29 + }
+48
lib/widgets/video_info/video_info_bar.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'username_label.dart'; 3 + import 'video_description.dart'; 4 + import 'hashtag_list.dart'; 5 + 6 + class VideoInfoBar extends StatelessWidget { 7 + final String username; 8 + final String description; 9 + final List<String> hashtags; 10 + final VoidCallback? onUsernameTap; 11 + final VoidCallback? onHashtagTap; 12 + 13 + const VideoInfoBar({ 14 + super.key, 15 + required this.username, 16 + required this.description, 17 + required this.hashtags, 18 + this.onUsernameTap, 19 + this.onHashtagTap, 20 + }); 21 + 22 + @override 23 + Widget build(BuildContext context) { 24 + return Column( 25 + crossAxisAlignment: CrossAxisAlignment.start, 26 + children: [ 27 + // Username 28 + GestureDetector( 29 + onTap: onUsernameTap, 30 + child: UsernameLabel(username: username), 31 + ), 32 + 33 + const SizedBox(height: 10), 34 + 35 + // Description 36 + VideoDescription(text: description), 37 + 38 + const SizedBox(height: 6), 39 + 40 + // Hashtags 41 + HashtagList( 42 + hashtags: hashtags, 43 + onHashtagTap: onHashtagTap, 44 + ), 45 + ], 46 + ); 47 + } 48 + }
+83
lib/widgets/video_side_action_bar.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'action_buttons/profile_action_button.dart'; 3 + import 'action_buttons/like_action_button.dart'; 4 + import 'action_buttons/comment_action_button.dart'; 5 + import 'action_buttons/bookmark_action_button.dart'; 6 + import 'action_buttons/share_action_button.dart'; 7 + 8 + class VideoSideActionBar extends StatelessWidget { 9 + // Callback functions for each action 10 + final VoidCallback? onProfilePressed; 11 + final VoidCallback? onLikePressed; 12 + final VoidCallback? onCommentPressed; 13 + final VoidCallback? onBookmarkPressed; 14 + final VoidCallback? onSharePressed; 15 + 16 + // Counts and states 17 + final String likeCount; 18 + final String commentCount; 19 + final String bookmarkCount; 20 + final String shareCount; 21 + final bool isLiked; 22 + final bool isBookmarked; 23 + 24 + const VideoSideActionBar({ 25 + super.key, 26 + // Callbacks 27 + this.onProfilePressed, 28 + this.onLikePressed, 29 + this.onCommentPressed, 30 + this.onBookmarkPressed, 31 + this.onSharePressed, 32 + 33 + // Counts with defaults 34 + this.likeCount = '0', 35 + this.commentCount = '0', 36 + this.bookmarkCount = '0', 37 + this.shareCount = '0', 38 + 39 + // States 40 + this.isLiked = false, 41 + this.isBookmarked = false, 42 + }); 43 + 44 + @override 45 + Widget build(BuildContext context) { 46 + return Column( 47 + children: [ 48 + // Profile with plus button 49 + ProfileActionButton(onPressed: onProfilePressed), 50 + const SizedBox(height: 20), 51 + 52 + // Like button 53 + LikeActionButton( 54 + count: likeCount, 55 + isLiked: isLiked, 56 + onPressed: onLikePressed, 57 + ), 58 + const SizedBox(height: 20), 59 + 60 + // Comment button 61 + CommentActionButton( 62 + count: commentCount, 63 + onPressed: onCommentPressed, 64 + ), 65 + const SizedBox(height: 20), 66 + 67 + // Bookmark button 68 + BookmarkActionButton( 69 + count: bookmarkCount, 70 + isBookmarked: isBookmarked, 71 + onPressed: onBookmarkPressed, 72 + ), 73 + const SizedBox(height: 20), 74 + 75 + // Share button 76 + ShareActionButton( 77 + count: shareCount, 78 + onPressed: onSharePressed, 79 + ), 80 + ], 81 + ); 82 + } 83 + }