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

Merge branch 'main' of https://github.com/sprksocial/spark-front-end

C3B 5728d3d0 4ad43b77

+936 -120
+18 -9
lib/main.dart
··· 10 10 import 'screens/splash_screen.dart'; 11 11 import 'utils/app_colors.dart'; 12 12 import 'utils/app_theme.dart'; 13 + import 'screens/login_screen.dart'; 14 + import 'screens/auth_prompt_screen.dart'; 15 + import 'services/auth_service.dart'; 13 16 14 17 void main() { 15 18 WidgetsFlutterBinding.ensureInitialized(); ··· 28 31 // We'll use a builder to get access to the platform brightness 29 32 return CupertinoTheme( 30 33 data: AppTheme.theme, 31 - child: ChangeNotifierProvider( 32 - create: (_) => NavigationProvider(), 34 + child: MultiProvider( 35 + providers: [ 36 + ChangeNotifierProvider(create: (_) => NavigationProvider()), 37 + ChangeNotifierProvider(create: (_) => AuthService()), 38 + ], 33 39 child: CupertinoApp( 34 40 title: 'Spark', 35 41 theme: AppTheme.theme, 36 42 home: const SplashScreen(), 37 43 routes: { 38 44 '/home': (context) => const MainScreen(), 45 + '/login': (context) => const LoginScreen(), 46 + '/auth': (context) => const AuthPromptScreen(), 39 47 }, 40 48 ), 41 49 ), ··· 80 88 index: navigationProvider.currentIndex, 81 89 children: screens, 82 90 ), 83 - 91 + 84 92 // Bottom navigation 85 93 Positioned( 86 94 left: 0, ··· 126 134 Ionicons.chatbubble, 127 135 ), 128 136 _buildNavItem( 129 - context, 130 - 4, 131 - 'Profile', 132 - Ionicons.person_outline, 137 + context, 138 + 4, 139 + 'Profile', 140 + Ionicons.person_outline, 133 141 Ionicons.person, 134 142 ), 135 143 ], ··· 146 154 Widget _buildNavItem(BuildContext context, int index, String label, IconData iconOutline, IconData iconFilled) { 147 155 final navigationProvider = Provider.of<NavigationProvider>(context); 148 156 final bool isSelected = navigationProvider.currentIndex == index; 157 + 149 158 final bool isHomePage = navigationProvider.currentIndex == 0; 150 - 159 + 151 160 return CupertinoButton( 152 161 padding: EdgeInsets.zero, 153 162 onPressed: () { ··· 155 164 }, 156 165 child: Icon( 157 166 isSelected ? iconFilled : iconOutline, 158 - color: isSelected 167 + color: isSelected 159 168 ? AppTheme.getSelectedIconColor(context, isHomePage) 160 169 : AppTheme.getUnselectedIconColor(context, isHomePage), 161 170 size: 26,
+119
lib/screens/auth_prompt_screen.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:ionicons/ionicons.dart'; 3 + import 'login_screen.dart'; 4 + 5 + class AuthPromptScreen extends StatelessWidget { 6 + final VoidCallback? onClose; 7 + 8 + const AuthPromptScreen({ 9 + super.key, 10 + this.onClose, 11 + }); 12 + 13 + @override 14 + Widget build(BuildContext context) { 15 + return CupertinoPageScaffold( 16 + backgroundColor: CupertinoColors.systemBackground, 17 + navigationBar: onClose != null ? CupertinoNavigationBar( 18 + leading: CupertinoButton( 19 + padding: EdgeInsets.zero, 20 + onPressed: onClose, 21 + child: const Icon(Ionicons.close_outline), 22 + ), 23 + backgroundColor: CupertinoColors.systemBackground, 24 + border: null, 25 + ) : null, 26 + child: SafeArea( 27 + child: Center( 28 + child: Padding( 29 + padding: const EdgeInsets.all(24.0), 30 + child: Column( 31 + mainAxisAlignment: MainAxisAlignment.center, 32 + children: [ 33 + const Icon( 34 + Ionicons.sparkles_outline, 35 + size: 80, 36 + color: CupertinoColors.systemPink, 37 + ), 38 + const SizedBox(height: 24), 39 + const Text( 40 + 'Welcome to Spark', 41 + style: TextStyle( 42 + fontSize: 22, 43 + fontWeight: FontWeight.bold, 44 + ), 45 + textAlign: TextAlign.center, 46 + ), 47 + const SizedBox(height: 16), 48 + const Text( 49 + 'Add an account to create videos, connect with friends, and more', 50 + textAlign: TextAlign.center, 51 + style: TextStyle( 52 + color: CupertinoColors.systemGrey, 53 + fontSize: 16, 54 + ), 55 + ), 56 + const SizedBox(height: 40), 57 + SizedBox( 58 + width: double.infinity, 59 + child: CupertinoButton( 60 + color: CupertinoColors.systemPink, 61 + onPressed: () { 62 + Navigator.of(context).push( 63 + CupertinoPageRoute( 64 + builder: (context) => const LoginScreen(), 65 + ), 66 + ); 67 + }, 68 + child: const Text('Login'), 69 + ), 70 + ), 71 + const SizedBox(height: 16), 72 + SizedBox( 73 + width: double.infinity, 74 + child: CupertinoButton( 75 + color: CupertinoColors.systemGrey6, 76 + onPressed: () { 77 + // Register functionality will be implemented later 78 + showCupertinoDialog( 79 + context: context, 80 + builder: (context) => CupertinoAlertDialog( 81 + title: const Text('Coming Soon'), 82 + content: const Text('Registration will be available in a future update.'), 83 + actions: [ 84 + CupertinoDialogAction( 85 + child: const Text('OK'), 86 + onPressed: () => Navigator.of(context).pop(), 87 + ), 88 + ], 89 + ), 90 + ); 91 + }, 92 + child: const Text( 93 + 'Register', 94 + style: TextStyle( 95 + color: CupertinoColors.systemPink, 96 + ), 97 + ), 98 + ), 99 + ), 100 + if (onClose != null) ...[ 101 + const SizedBox(height: 24), 102 + CupertinoButton( 103 + onPressed: onClose, 104 + child: const Text( 105 + 'Continue browsing', 106 + style: TextStyle( 107 + color: CupertinoColors.systemGrey, 108 + ), 109 + ), 110 + ), 111 + ], 112 + ], 113 + ), 114 + ), 115 + ), 116 + ), 117 + ); 118 + } 119 + }
+42 -17
lib/screens/create_video_screen.dart
··· 1 1 import 'package:flutter/cupertino.dart'; 2 2 import 'package:flutter/material.dart' show LinearProgressIndicator, AlwaysStoppedAnimation; 3 3 import 'package:ionicons/ionicons.dart'; 4 + import 'package:provider/provider.dart'; 5 + import '../services/auth_service.dart'; 6 + import 'auth_prompt_screen.dart'; 4 7 5 8 class CreateVideoScreen extends StatefulWidget { 6 9 const CreateVideoScreen({super.key}); ··· 13 16 int _selectedEffectIndex = 0; 14 17 double _zoomLevel = 1.0; 15 18 bool _isRecording = false; 16 - 19 + bool _showAuthPrompt = false; 20 + 17 21 final List<String> _effects = [ 18 22 'None', 'Beauty', 'Filters', 'Green Screen', 'Slow Motion' 19 23 ]; 20 24 25 + void _attemptRecording() { 26 + final authService = Provider.of<AuthService>(context, listen: false); 27 + if (!authService.isAuthenticated) { 28 + setState(() { 29 + _showAuthPrompt = true; 30 + }); 31 + } else { 32 + setState(() { 33 + _isRecording = !_isRecording; 34 + }); 35 + } 36 + } 37 + 21 38 @override 22 39 Widget build(BuildContext context) { 40 + final authService = Provider.of<AuthService>(context); 41 + 42 + if (!authService.isAuthenticated && _showAuthPrompt) { 43 + return AuthPromptScreen( 44 + onClose: () { 45 + setState(() { 46 + _showAuthPrompt = false; 47 + }); 48 + }, 49 + ); 50 + } 51 + 23 52 return CupertinoPageScaffold( 24 53 backgroundColor: CupertinoColors.black, 25 54 child: Stack( ··· 37 66 ), 38 67 ), 39 68 ), 40 - 69 + 41 70 // Top controls 42 71 Positioned( 43 72 top: 50, ··· 81 110 ), 82 111 ), 83 112 ), 84 - 113 + 85 114 // Zoom control 86 115 Positioned( 87 116 top: 100, ··· 149 178 ], 150 179 ), 151 180 ), 152 - 181 + 153 182 // Bottom controls 154 183 Positioned( 155 184 bottom: 0, ··· 187 216 margin: const EdgeInsets.only(right: 8), 188 217 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 189 218 decoration: BoxDecoration( 190 - color: _selectedEffectIndex == index 191 - ? CupertinoColors.systemPink 219 + color: _selectedEffectIndex == index 220 + ? CupertinoColors.systemPink 192 221 : CupertinoColors.black.withOpacity(0.5), 193 222 borderRadius: BorderRadius.circular(20), 194 223 border: Border.all( ··· 210 239 }, 211 240 ), 212 241 ), 213 - 242 + 214 243 const SizedBox(height: 20), 215 - 244 + 216 245 // Recording controls 217 246 Row( 218 247 mainAxisAlignment: MainAxisAlignment.spaceEvenly, ··· 223 252 size: 30, 224 253 ), 225 254 GestureDetector( 226 - onTap: () { 227 - setState(() { 228 - _isRecording = !_isRecording; 229 - }); 230 - }, 255 + onTap: _attemptRecording, 231 256 child: Container( 232 257 width: 80, 233 258 height: 80, ··· 257 282 ), 258 283 ], 259 284 ), 260 - 285 + 261 286 const SizedBox(height: 20), 262 - 287 + 263 288 // Duration indicator 264 289 Container( 265 290 margin: const EdgeInsets.symmetric(horizontal: 40), ··· 273 298 ), 274 299 ), 275 300 ), 276 - 301 + 277 302 const SizedBox(height: 10), 278 303 const Text( 279 304 '00:15 / 03:00', ··· 290 315 ), 291 316 ); 292 317 } 293 - } 318 + }
+7 -8
lib/screens/home_screen.dart
··· 1 1 import 'package:flutter/cupertino.dart'; 2 2 import 'package:ionicons/ionicons.dart'; 3 - import 'package:cached_network_image/cached_network_image.dart'; 4 3 import '../widgets/video_side_action_bar.dart'; 5 4 import '../widgets/video_info/video_info_bar.dart'; 6 5 ··· 57 56 ), 58 57 ), 59 58 const SizedBox(height: 10), 60 - 59 + 61 60 // Video Feed (main content) 62 61 Expanded( 63 62 child: Padding( ··· 81 80 82 81 class VideoItem extends StatelessWidget { 83 82 final int index; 84 - 83 + 85 84 const VideoItem({super.key, required this.index}); 86 85 87 86 @override ··· 94 93 return Container( 95 94 // Use constraints to ensure the video fits within available space 96 95 constraints: BoxConstraints( 97 - maxHeight: MediaQuery.of(context).size.height - 98 - MediaQuery.of(context).padding.top - 96 + maxHeight: MediaQuery.of(context).size.height - 97 + MediaQuery.of(context).padding.top - 99 98 50 - // Top navigation height 100 99 (MediaQuery.of(context).padding.bottom + 50), // Bottom nav height + safe area 101 100 ), ··· 113 112 ), 114 113 ), 115 114 ), 116 - 115 + 117 116 // Video info - now using the modular component 118 117 Positioned( 119 118 bottom: 20, ··· 131 130 }, 132 131 ), 133 132 ), 134 - 133 + 135 134 // Right side actions 136 135 Positioned( 137 136 right: 10, ··· 163 162 ), 164 163 ); 165 164 } 166 - } 165 + }
+224
lib/screens/login_screen.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:flutter/services.dart'; 3 + import 'package:provider/provider.dart'; 4 + import '../services/auth_service.dart'; 5 + 6 + class LoginScreen extends StatefulWidget { 7 + const LoginScreen({super.key}); 8 + 9 + @override 10 + State<LoginScreen> createState() => _LoginScreenState(); 11 + } 12 + 13 + class _LoginScreenState extends State<LoginScreen> { 14 + final _handleController = TextEditingController(); 15 + final _passwordController = TextEditingController(); 16 + bool _obscurePassword = true; 17 + final _formKey = GlobalKey<FormState>(); 18 + 19 + // Add focus nodes for autofill 20 + final _handleFocusNode = FocusNode(); 21 + final _passwordFocusNode = FocusNode(); 22 + 23 + // For autofill 24 + 25 + @override 26 + void initState() { 27 + super.initState(); 28 + // Schedule autofill request for after the widget is built 29 + WidgetsBinding.instance.addPostFrameCallback((_) { 30 + TextInput.ensureInitialized(); 31 + }); 32 + } 33 + 34 + @override 35 + void dispose() { 36 + _handleController.dispose(); 37 + _passwordController.dispose(); 38 + _handleFocusNode.dispose(); 39 + _passwordFocusNode.dispose(); 40 + super.dispose(); 41 + } 42 + 43 + Future<void> _login() async { 44 + if (_formKey.currentState?.validate() ?? false) { 45 + final authService = Provider.of<AuthService>(context, listen: false); 46 + final success = await authService.login( 47 + _handleController.text.trim(), 48 + _passwordController.text, 49 + ); 50 + 51 + if (success && mounted) { 52 + // Complete autofill when login is successful 53 + TextInput.finishAutofillContext(shouldSave: true); 54 + Navigator.of(context).pushReplacementNamed('/home'); 55 + } 56 + } 57 + } 58 + 59 + @override 60 + Widget build(BuildContext context) { 61 + final authService = Provider.of<AuthService>(context); 62 + 63 + return CupertinoPageScaffold( 64 + backgroundColor: CupertinoColors.black, 65 + child: SafeArea( 66 + child: Center( 67 + child: SingleChildScrollView( 68 + padding: const EdgeInsets.all(24.0), 69 + child: Form( 70 + key: _formKey, 71 + autovalidateMode: AutovalidateMode.onUserInteraction, 72 + child: Column( 73 + mainAxisAlignment: MainAxisAlignment.center, 74 + crossAxisAlignment: CrossAxisAlignment.stretch, 75 + children: [ 76 + // Logo 77 + Container( 78 + width: 100, 79 + height: 100, 80 + margin: const EdgeInsets.only(bottom: 40), 81 + decoration: BoxDecoration( 82 + gradient: const LinearGradient( 83 + colors: [ 84 + CupertinoColors.systemPink, 85 + CupertinoColors.systemBlue, 86 + ], 87 + begin: Alignment.bottomLeft, 88 + end: Alignment.topRight, 89 + ), 90 + borderRadius: BorderRadius.circular(20), 91 + ), 92 + child: const Center( 93 + child: Text( 94 + 'TT', 95 + style: TextStyle( 96 + color: CupertinoColors.white, 97 + fontSize: 40, 98 + fontWeight: FontWeight.bold, 99 + ), 100 + ), 101 + ), 102 + ), 103 + 104 + // Title 105 + const Text( 106 + 'Login to your account', 107 + style: TextStyle( 108 + color: CupertinoColors.white, 109 + fontSize: 24, 110 + fontWeight: FontWeight.bold, 111 + ), 112 + textAlign: TextAlign.center, 113 + ), 114 + const SizedBox(height: 30), 115 + 116 + // Handle field 117 + AutofillGroup( 118 + child: Column( 119 + children: [ 120 + CupertinoTextField( 121 + controller: _handleController, 122 + focusNode: _handleFocusNode, 123 + placeholder: 'Handle', 124 + padding: const EdgeInsets.all(16), 125 + decoration: BoxDecoration( 126 + color: CupertinoColors.systemGrey6, 127 + borderRadius: BorderRadius.circular(12), 128 + ), 129 + prefix: const Padding( 130 + padding: EdgeInsets.only(left: 16), 131 + child: Icon( 132 + CupertinoIcons.person, 133 + color: CupertinoColors.systemGrey, 134 + ), 135 + ), 136 + style: const TextStyle(color: CupertinoColors.black), 137 + textInputAction: TextInputAction.next, 138 + keyboardType: TextInputType.emailAddress, 139 + autofillHints: const [AutofillHints.username, AutofillHints.email], 140 + onEditingComplete: () => _passwordFocusNode.requestFocus(), 141 + ), 142 + const SizedBox(height: 16), 143 + 144 + // Password field 145 + CupertinoTextField( 146 + controller: _passwordController, 147 + focusNode: _passwordFocusNode, 148 + placeholder: 'Password', 149 + padding: const EdgeInsets.all(16), 150 + obscureText: _obscurePassword, 151 + decoration: BoxDecoration( 152 + color: CupertinoColors.systemGrey6, 153 + borderRadius: BorderRadius.circular(12), 154 + ), 155 + prefix: const Padding( 156 + padding: EdgeInsets.only(left: 16), 157 + child: Icon( 158 + CupertinoIcons.lock, 159 + color: CupertinoColors.systemGrey, 160 + ), 161 + ), 162 + suffix: Padding( 163 + padding: const EdgeInsets.only(right: 16), 164 + child: GestureDetector( 165 + onTap: () { 166 + setState(() { 167 + _obscurePassword = !_obscurePassword; 168 + }); 169 + }, 170 + child: Icon( 171 + _obscurePassword 172 + ? CupertinoIcons.eye 173 + : CupertinoIcons.eye_slash, 174 + color: CupertinoColors.systemGrey, 175 + ), 176 + ), 177 + ), 178 + style: const TextStyle(color: CupertinoColors.black), 179 + textInputAction: TextInputAction.done, 180 + keyboardType: TextInputType.visiblePassword, 181 + autofillHints: const [AutofillHints.password], 182 + onEditingComplete: () { 183 + TextInput.finishAutofillContext(); 184 + _login(); 185 + }, 186 + ), 187 + ], 188 + ), 189 + ), 190 + const SizedBox(height: 24), 191 + 192 + // Error message 193 + if (authService.error != null) 194 + Padding( 195 + padding: const EdgeInsets.only(bottom: 16), 196 + child: Text( 197 + authService.error!, 198 + style: const TextStyle( 199 + color: CupertinoColors.systemRed, 200 + fontSize: 14, 201 + ), 202 + textAlign: TextAlign.center, 203 + ), 204 + ), 205 + 206 + // Login button 207 + CupertinoButton( 208 + onPressed: authService.isLoading ? null : _login, 209 + padding: const EdgeInsets.symmetric(vertical: 16), 210 + color: CupertinoColors.activeBlue, 211 + borderRadius: BorderRadius.circular(12), 212 + child: authService.isLoading 213 + ? const CupertinoActivityIndicator(color: CupertinoColors.white) 214 + : const Text('Login'), 215 + ), 216 + ], 217 + ), 218 + ), 219 + ), 220 + ), 221 + ), 222 + ); 223 + } 224 + }
+11 -11
lib/screens/messages_screen.dart
··· 15 15 16 16 class _MessagesScreenState extends State<MessagesScreen> { 17 17 int _selectedTabIndex = 0; 18 - 18 + 19 19 // Mock data for messages 20 20 final List<MessageData> _messages = List.generate( 21 21 15, ··· 35 35 unreadCount: index % 3 == 0 ? 1 : null, 36 36 ), 37 37 ); 38 - 38 + 39 39 // Mock data for activities 40 40 final List<ActivityData> _activities = List.generate( 41 41 15, 42 42 (index) { 43 43 final ActivityType type = ActivityType.values[index % ActivityType.values.length]; 44 44 String? additionalInfo; 45 - 45 + 46 46 if (type == ActivityType.comment) { 47 47 additionalInfo = 'Wow, this looks amazing! 🔥'; 48 48 } else if (type == ActivityType.like) { ··· 50 50 } else { 51 51 additionalInfo = null; 52 52 } 53 - 53 + 54 54 return ActivityData( 55 55 id: 'act_$index', 56 56 username: 'user${index + 1}', ··· 72 72 Widget build(BuildContext context) { 73 73 final brightness = MediaQuery.of(context).platformBrightness; 74 74 final isDarkMode = brightness == Brightness.dark; 75 - 75 + 76 76 return CupertinoPageScaffold( 77 77 backgroundColor: AppTheme.getBackgroundColor(context, false), 78 78 navigationBar: CupertinoNavigationBar( ··· 108 108 borderColor: AppColors.primary, 109 109 ), 110 110 ), 111 - 111 + 112 112 // Search bar 113 113 Padding( 114 114 padding: const EdgeInsets.all(16.0), ··· 130 130 backgroundColor: isDarkMode ? AppColors.deepPurple : AppColors.white, 131 131 ), 132 132 ), 133 - 133 + 134 134 // Content based on selected tab 135 135 Expanded( 136 - child: _selectedTabIndex == 0 136 + child: _selectedTabIndex == 0 137 137 ? _buildMessagesTab() 138 138 : _buildActivitiesTab(), 139 139 ), ··· 142 142 ), 143 143 ); 144 144 } 145 - 145 + 146 146 Widget _buildMessagesTab() { 147 147 return MessageList( 148 148 messages: _messages, ··· 152 152 }, 153 153 ); 154 154 } 155 - 155 + 156 156 Widget _buildActivitiesTab() { 157 157 return ActivityList( 158 158 activities: _activities, ··· 162 162 }, 163 163 ); 164 164 } 165 - } 165 + }
+117 -44
lib/screens/profile_screen.dart
··· 2 2 import 'package:flutter/material.dart' show Colors; 3 3 import 'package:ionicons/ionicons.dart'; 4 4 import 'package:flutter_svg/flutter_svg.dart'; 5 + import 'package:provider/provider.dart'; 5 6 import '../utils/app_colors.dart'; 6 7 import '../utils/app_theme.dart'; 7 8 import '../widgets/profile/profile_stat_item.dart'; 8 9 import '../widgets/profile/profile_action_button.dart'; 9 10 import '../widgets/profile/videos_grid.dart'; 10 11 import '../widgets/profile/early_supporter_sheet.dart'; 12 + import '../services/auth_service.dart'; 13 + import 'auth_prompt_screen.dart'; 11 14 12 15 class ProfileScreen extends StatefulWidget { 13 16 const ProfileScreen({super.key}); ··· 18 21 19 22 class _ProfileScreenState extends State<ProfileScreen> { 20 23 int _selectedTabIndex = 0; 21 - 22 - // This flag would normally come from user auth 23 - final bool _isOwnProfile = true; 24 - 24 + bool _showAuthPrompt = false; 25 + 25 26 // Flags for special badges 26 27 final bool _isEarlySupporter = true; 27 - 28 + 28 29 void _showEarlySupporterInfo(BuildContext context) { 29 30 showCupertinoModalPopup( 30 31 context: context, ··· 40 41 ), 41 42 ); 42 43 } 43 - 44 + 45 + void _checkAuthAndProceed(VoidCallback action) { 46 + final authService = Provider.of<AuthService>(context, listen: false); 47 + if (!authService.isAuthenticated) { 48 + setState(() { 49 + _showAuthPrompt = true; 50 + }); 51 + } else { 52 + action(); 53 + } 54 + } 55 + 44 56 @override 45 57 Widget build(BuildContext context) { 46 58 final brightness = MediaQuery.of(context).platformBrightness; 47 59 final isDarkMode = brightness == Brightness.dark; 48 - 49 - // Get screen dimensions to ensure no overflow 50 - final screenHeight = MediaQuery.of(context).size.height; 51 - 60 + final authService = Provider.of<AuthService>(context); 61 + final isAuthenticated = authService.isAuthenticated; 62 + 63 + // Show auth prompt if needed 64 + if (_showAuthPrompt) { 65 + return AuthPromptScreen( 66 + onClose: () { 67 + setState(() { 68 + _showAuthPrompt = false; 69 + }); 70 + }, 71 + ); 72 + } 73 + 52 74 return CupertinoPageScaffold( 53 75 backgroundColor: AppTheme.getBackgroundColor(context, false), 54 76 navigationBar: CupertinoNavigationBar( ··· 122 144 ), 123 145 ], 124 146 ), 125 - 147 + 126 148 const SizedBox(width: 20), 127 - 149 + 128 150 // Stats row 129 151 Expanded( 130 152 child: Row( ··· 138 160 ), 139 161 ], 140 162 ), 141 - 163 + 142 164 const SizedBox(height: 16), 143 - 165 + 144 166 // Username and verified badge 145 167 Row( 146 168 children: [ ··· 152 174 color: AppTheme.getTextColor(context), 153 175 ), 154 176 ), 155 - 177 + 156 178 // Early Supporter badge 157 179 if (_isEarlySupporter) ...[ 158 180 const SizedBox(width: 8), ··· 163 185 height: 20, 164 186 width: 20, 165 187 colorFilter: const ColorFilter.mode( 166 - AppColors.primary, 188 + AppColors.primary, 167 189 BlendMode.srcIn 168 190 ), 169 191 ), ··· 171 193 ], 172 194 ], 173 195 ), 174 - 196 + 175 197 const SizedBox(height: 4), 176 - 198 + 177 199 // Username in the format seen in the screenshot 178 200 Text( 179 201 '@joebasser.sprk.so', ··· 182 204 fontSize: 14, 183 205 ), 184 206 ), 185 - 207 + 186 208 const SizedBox(height: 4), 187 - 209 + 188 210 // Website 189 211 Text( 190 212 'www.website.com', ··· 193 215 fontSize: 14, 194 216 ), 195 217 ), 196 - 218 + 197 219 const SizedBox(height: 16), 198 - 220 + 199 221 // Action buttons in a row 200 222 Row( 201 223 children: [ ··· 204 226 flex: 1, 205 227 child: ProfileActionButton( 206 228 label: 'Edit', 207 - onPressed: () {}, 229 + onPressed: () => _checkAuthAndProceed(() { 230 + // Edit profile logic here 231 + }), 208 232 isPrimary: true, 209 233 isOutlined: false, 210 234 ), 211 235 ), 212 - 236 + 213 237 const SizedBox(width: 8), 214 - 238 + 215 239 // Share Profile button 216 240 Expanded( 217 241 flex: 1, ··· 219 243 constraints: const BoxConstraints(minHeight: 36), 220 244 child: ProfileActionButton( 221 245 label: 'Share Profile', 222 - onPressed: () {}, 246 + onPressed: () { 247 + // Share profile doesn't require authentication 248 + }, 223 249 ), 224 250 ), 225 251 ), 226 - 252 + 227 253 const SizedBox(width: 8), 228 - 254 + 229 255 // Friends + button 230 256 Expanded( 231 257 flex: 1, 232 258 child: ProfileActionButton( 233 259 label: 'Friends +', 234 - onPressed: () {}, 260 + onPressed: () => _checkAuthAndProceed(() { 261 + // Friends management logic here 262 + }), 235 263 ), 236 264 ), 237 265 ], ··· 239 267 ], 240 268 ), 241 269 ), 242 - 270 + 243 271 // Tab bar at the bottom of content 244 272 Container( 245 273 decoration: BoxDecoration( ··· 262 290 _buildTabItem(context, 0, CupertinoIcons.film), 263 291 _buildTabItem(context, 1, CupertinoIcons.heart), 264 292 _buildTabItem(context, 2, CupertinoIcons.arrow_2_squarepath), 265 - if (_isOwnProfile) _buildTabItem(context, 3, CupertinoIcons.bookmark), 266 - if (_isOwnProfile) _buildTabItem(context, 4, CupertinoIcons.lock), 293 + if (isAuthenticated) _buildTabItem(context, 3, CupertinoIcons.bookmark), 294 + if (isAuthenticated) _buildTabItem(context, 4, CupertinoIcons.lock), 267 295 ], 268 296 ), 269 297 ), 270 298 ), 271 - 299 + 272 300 // Tab content - with fixed height to prevent scrolling of the entire screen 273 301 Expanded( 274 302 child: _buildTabContent(), ··· 278 306 ), 279 307 ); 280 308 } 281 - 309 + 282 310 Widget _buildTabItem(BuildContext context, int index, IconData icon) { 283 311 final brightness = MediaQuery.of(context).platformBrightness; 284 312 final isDarkMode = brightness == Brightness.dark; 285 313 final isSelected = _selectedTabIndex == index; 286 - 314 + 287 315 // Get filled icon variants based on the outline icon 288 316 IconData getFilledIcon(IconData outlineIcon) { 289 317 if (outlineIcon == CupertinoIcons.film) { ··· 300 328 return outlineIcon; 301 329 } 302 330 } 303 - 331 + 304 332 return CupertinoButton( 305 333 padding: EdgeInsets.zero, 306 334 onPressed: () { ··· 313 341 decoration: BoxDecoration( 314 342 border: Border( 315 343 bottom: BorderSide( 316 - color: isSelected 344 + color: isSelected 317 345 ? AppColors.primary 318 346 : Colors.transparent, 319 347 width: 2, ··· 322 350 ), 323 351 child: Icon( 324 352 isSelected ? getFilledIcon(icon) : icon, 325 - color: isSelected 353 + color: isSelected 326 354 ? AppColors.primary 327 355 : (isDarkMode ? AppColors.textLight : AppColors.textSecondary), 328 356 size: 26, ··· 330 358 ), 331 359 ); 332 360 } 333 - 361 + 334 362 Widget _buildTabContent() { 363 + final authService = Provider.of<AuthService>(context); 364 + 365 + // For tabs that require authentication, show auth prompt if not authenticated 366 + if ((_selectedTabIndex == 3 || _selectedTabIndex == 4) && !authService.isAuthenticated) { 367 + return Center( 368 + child: Column( 369 + mainAxisAlignment: MainAxisAlignment.center, 370 + children: [ 371 + Icon( 372 + _selectedTabIndex == 3 ? CupertinoIcons.bookmark : CupertinoIcons.lock, 373 + size: 60, 374 + color: AppTheme.getSecondaryTextColor(context), 375 + ), 376 + const SizedBox(height: 20), 377 + Text( 378 + _selectedTabIndex == 3 ? 'Saved videos' : 'Private videos', 379 + style: TextStyle( 380 + fontWeight: FontWeight.bold, 381 + fontSize: 18, 382 + color: AppTheme.getTextColor(context), 383 + ), 384 + ), 385 + const SizedBox(height: 10), 386 + Text( 387 + 'Login to view your saved content', 388 + style: TextStyle( 389 + color: AppTheme.getSecondaryTextColor(context), 390 + ), 391 + textAlign: TextAlign.center, 392 + ), 393 + const SizedBox(height: 24), 394 + CupertinoButton( 395 + color: CupertinoColors.systemPink, 396 + onPressed: () { 397 + setState(() { 398 + _showAuthPrompt = true; 399 + }); 400 + }, 401 + child: const Text('Login'), 402 + ), 403 + ], 404 + ), 405 + ); 406 + } 407 + 335 408 switch (_selectedTabIndex) { 336 409 case 0: 337 410 return _buildPostsGrid(); ··· 356 429 return const SizedBox.shrink(); 357 430 } 358 431 } 359 - 432 + 360 433 Widget _buildPostsGrid() { 361 434 return GridView.builder( 362 435 physics: const NeverScrollableScrollPhysics(), // Prevents scrolling within the grid ··· 371 444 itemBuilder: (context, index) { 372 445 // Alternate between video and image posts 373 446 final bool isVideo = index % 2 == 0; 374 - 447 + 375 448 return GestureDetector( 376 449 onTap: () { 377 450 debugPrint('Post clicked: ${isVideo ? "Video" : "Image"} at index $index'); 378 451 }, 379 452 child: Container( 380 - color: isVideo 453 + color: isVideo 381 454 ? AppColors.richPurple.withOpacity(0.7) 382 455 : AppColors.orange.withOpacity(0.7), 383 456 child: Stack( ··· 436 509 }, 437 510 ); 438 511 } 439 - 512 + 440 513 Widget _buildPrivateTab() { 441 514 return Center( 442 515 child: Column( ··· 468 541 ), 469 542 ); 470 543 } 471 - } 544 + }
+9 -9
lib/screens/search_screen.dart
··· 12 12 13 13 class _SearchScreenState extends State<SearchScreen> { 14 14 final TextEditingController _searchController = TextEditingController(); 15 - 15 + 16 16 @override 17 17 void dispose() { 18 18 _searchController.dispose(); ··· 23 23 Widget build(BuildContext context) { 24 24 final brightness = MediaQuery.of(context).platformBrightness; 25 25 final isDarkMode = brightness == Brightness.dark; 26 - 26 + 27 27 return CupertinoPageScaffold( 28 28 backgroundColor: AppTheme.getBackgroundColor(context, false), 29 29 navigationBar: CupertinoNavigationBar( ··· 57 57 }, 58 58 ), 59 59 ), 60 - 60 + 61 61 // Trending hashtags 62 62 Padding( 63 63 padding: const EdgeInsets.all(12.0), ··· 74 74 ], 75 75 ), 76 76 ), 77 - 77 + 78 78 // Trending hashtags horizontal scroll 79 79 SizedBox( 80 80 height: 40, ··· 103 103 }, 104 104 ), 105 105 ), 106 - 106 + 107 107 const SizedBox(height: 12), 108 - 108 + 109 109 // Content grid 110 110 Expanded( 111 111 child: GridView.builder( ··· 119 119 itemCount: 30, 120 120 itemBuilder: (context, index) { 121 121 return Container( 122 - color: index % 3 == 0 122 + color: index % 3 == 0 123 123 ? AppColors.brightPurple.withOpacity(0.7) 124 - : index % 3 == 1 124 + : index % 3 == 1 125 125 ? AppColors.richPurple.withOpacity(0.7) 126 126 : AppColors.primary.withOpacity(0.7), 127 127 child: Stack( ··· 165 165 ), 166 166 ); 167 167 } 168 - } 168 + }
+8 -5
lib/screens/splash_screen.dart
··· 20 20 vsync: this, 21 21 duration: const Duration(seconds: 2), 22 22 ); 23 - 23 + 24 24 _animation = Tween<double>(begin: 0.0, end: 1.0).animate( 25 25 CurvedAnimation( 26 26 parent: _animationController, 27 27 curve: Curves.easeInOut, 28 28 ), 29 29 ); 30 - 30 + 31 31 _animationController.forward(); 32 - 32 + 33 + // Simply go to the main screen after a delay 33 34 Timer(const Duration(seconds: 3), () { 34 - Navigator.of(context).pushReplacementNamed('/home'); 35 + if (mounted) { 36 + Navigator.of(context).pushReplacementNamed('/home'); 37 + } 35 38 }); 36 39 } 37 40 ··· 95 98 ), 96 99 ); 97 100 } 98 - } 101 + }
+115
lib/services/auth_service.dart
··· 1 + import 'package:atproto/atproto.dart'; 2 + import 'package:flutter/foundation.dart'; 3 + import 'dart:convert'; 4 + import 'package:http/http.dart' as http; 5 + 6 + class AuthService extends ChangeNotifier { 7 + dynamic _session; 8 + bool _isLoading = false; 9 + String? _error; 10 + ATProto? _atProto; 11 + 12 + // Getters 13 + bool get isAuthenticated => _session != null; 14 + bool get isLoading => _isLoading; 15 + String? get error => _error; 16 + dynamic get session => _session; 17 + ATProto? get atproto => _atProto; 18 + 19 + // Login with handle and password 20 + Future<bool> login(String handle, String password) async { 21 + _isLoading = true; 22 + _error = null; 23 + notifyListeners(); 24 + 25 + try { 26 + ATProto at = ATProto.anonymous( 27 + service: 'shimeji.us-east.host.bsky.network', 28 + ); 29 + final didRes = await at.identity.resolveHandle(handle: handle); 30 + String did = didRes.data.did; 31 + 32 + // Fetch DID document from PLC directory 33 + final didDocResponse = await http.get( 34 + Uri.parse('https://plc.directory/$did/data'), 35 + ); 36 + 37 + if (didDocResponse.statusCode != 200) { 38 + print(didDocResponse); 39 + throw Exception( 40 + 'Failed to fetch DID document: ${didDocResponse.statusCode}', 41 + ); 42 + } 43 + 44 + final didDoc = json.decode(didDocResponse.body); 45 + 46 + // Extract PDS endpoint from DID document 47 + String? pdsUrl = didDoc['services']['atproto_pds']['endpoint']; 48 + 49 + if (pdsUrl == null) { 50 + throw Exception('PDS endpoint not found in DID document'); 51 + } 52 + 53 + String pdsDomain = pdsUrl.replaceFirst('http://', '').replaceFirst('https://', '').replaceFirst('/', ''); 54 + final session = await createSession( 55 + identifier: handle, 56 + password: password, 57 + service: pdsDomain, 58 + ); 59 + 60 + _session = session.data; 61 + _atProto = ATProto.fromSession(_session); 62 + _isLoading = false; 63 + notifyListeners(); 64 + return true; 65 + } catch (e) { 66 + _error = e.toString(); 67 + _isLoading = false; 68 + notifyListeners(); 69 + return false; 70 + } 71 + } 72 + 73 + // Logout 74 + Future<void> logout() async { 75 + _isLoading = true; 76 + notifyListeners(); 77 + 78 + try { 79 + if (_atProto != null) { 80 + // Create a new session with the ATProto client 81 + // final atproto = ATProto.fromSession(_session); 82 + _session = null; 83 + _atProto = null; 84 + } 85 + } catch (e) { 86 + _error = e.toString(); 87 + } finally { 88 + _isLoading = false; 89 + notifyListeners(); 90 + } 91 + } 92 + 93 + // Get current user profile 94 + Future<Map<String, dynamic>?> getCurrentUserProfile() async { 95 + if (_atProto == null) return null; 96 + 97 + try { 98 + // final response = await _atProto!.repo.getProfile( 99 + // actor: _session.did, 100 + // ); 101 + return <String, dynamic>{}; 102 + // return response.data.toJson(); 103 + } catch (e) { 104 + _error = e.toString(); 105 + notifyListeners(); 106 + return null; 107 + } 108 + } 109 + 110 + // Clear error 111 + void clearError() { 112 + _error = null; 113 + notifyListeners(); 114 + } 115 + }
+4 -5
lib/widgets/activities/activity_content.dart
··· 1 1 import 'package:flutter/cupertino.dart'; 2 2 import 'activity_icon.dart'; 3 - import '../../utils/app_colors.dart'; 4 3 import '../../utils/app_theme.dart'; 5 4 6 5 class ActivityContent extends StatelessWidget { ··· 41 40 ), 42 41 ], 43 42 ), 44 - 43 + 45 44 // Optional additional info 46 45 if (additionalInfo != null) ...[ 47 46 const SizedBox(height: 4), ··· 70 69 @override 71 70 Widget build(BuildContext context) { 72 71 final String actionText = _getActionText(type); 73 - 72 + 74 73 return RichText( 75 74 maxLines: 1, 76 75 overflow: TextOverflow.ellipsis, ··· 93 92 ), 94 93 ); 95 94 } 96 - 95 + 97 96 String _getActionText(ActivityType type) { 98 97 switch (type) { 99 98 case ActivityType.like: ··· 150 149 overflow: TextOverflow.ellipsis, 151 150 ); 152 151 } 153 - } 152 + }
+2 -3
lib/widgets/profile/profile_stat_item.dart
··· 1 1 import 'package:flutter/cupertino.dart'; 2 - import '../../utils/app_colors.dart'; 3 2 import '../../utils/app_theme.dart'; 4 3 5 4 class ProfileStatItem extends StatelessWidget { 6 5 final String count; 7 6 final String label; 8 - 7 + 9 8 const ProfileStatItem({ 10 9 super.key, 11 10 required this.count, ··· 37 36 ], 38 37 ); 39 38 } 40 - } 39 + }
+5 -8
lib/widgets/profile/profile_tab_bar.dart
··· 1 1 import 'package:flutter/cupertino.dart'; 2 - import 'package:flutter/material.dart' show Colors; 3 - import 'package:ionicons/ionicons.dart'; 4 2 import '../../utils/app_colors.dart'; 5 - import '../../utils/app_theme.dart'; 6 3 7 4 class ProfileTabBar extends StatelessWidget { 8 5 final int selectedIndex; 9 6 final Function(int) onTabSelected; 10 - 7 + 11 8 const ProfileTabBar({ 12 9 super.key, 13 10 required this.selectedIndex, ··· 37 34 ), 38 35 ); 39 36 } 40 - 37 + 41 38 Widget _buildTabItem(BuildContext context, int index, IconData icon) { 42 39 final brightness = MediaQuery.of(context).platformBrightness; 43 40 final isDarkMode = brightness == Brightness.dark; 44 41 final isSelected = selectedIndex == index; 45 - 42 + 46 43 return CupertinoButton( 47 44 padding: EdgeInsets.zero, 48 45 onPressed: () => onTabSelected(index), ··· 50 47 padding: const EdgeInsets.symmetric(vertical: 12), 51 48 child: Icon( 52 49 icon, 53 - color: isSelected 50 + color: isSelected 54 51 ? AppColors.primary 55 52 : (isDarkMode ? AppColors.textLight : AppColors.textSecondary), 56 53 size: 26, ··· 58 55 ), 59 56 ); 60 57 } 61 - } 58 + }
+2
macos/Flutter/GeneratedPluginRegistrant.swift
··· 6 6 import Foundation 7 7 8 8 import path_provider_foundation 9 + import shared_preferences_foundation 9 10 import sqflite_darwin 10 11 import video_player_avfoundation 11 12 12 13 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 13 14 PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 15 + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 14 16 SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 15 17 FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) 16 18 }
+249 -1
pubspec.lock
··· 25 25 url: "https://pub.dev" 26 26 source: hosted 27 27 version: "2.12.0" 28 + at_identifier: 29 + dependency: transitive 30 + description: 31 + name: at_identifier 32 + sha256: "7c8778202d17ec4e63b38a6a58480503fbf0d7fc1d62e0d64580a9b6cbe142f7" 33 + url: "https://pub.dev" 34 + source: hosted 35 + version: "0.2.2" 36 + at_uri: 37 + dependency: transitive 38 + description: 39 + name: at_uri 40 + sha256: "1156d9d70460fcfcb30e744d7f8c7d544eff073b3142b772f0d02aca10dd064f" 41 + url: "https://pub.dev" 42 + source: hosted 43 + version: "0.4.0" 44 + atproto: 45 + dependency: "direct main" 46 + description: 47 + name: atproto 48 + sha256: "0f3d342c4d629e9994d58dbadd4281074641ac75a18cd514b212a3b15f86019e" 49 + url: "https://pub.dev" 50 + source: hosted 51 + version: "0.13.3" 52 + atproto_core: 53 + dependency: transitive 54 + description: 55 + name: atproto_core 56 + sha256: "13e7f5f0f3d9e5be59eefd5f427adf45ffdeaa59001d4ea7c91764ba21f1e9ba" 57 + url: "https://pub.dev" 58 + source: hosted 59 + version: "0.11.2" 60 + atproto_oauth: 61 + dependency: transitive 62 + description: 63 + name: atproto_oauth 64 + sha256: "8a0c64455c38c45773ebab5fdd55bf214541461f3a97fe0e6184a5eeb8222f03" 65 + url: "https://pub.dev" 66 + source: hosted 67 + version: "0.1.0" 68 + base_codecs: 69 + dependency: transitive 70 + description: 71 + name: base_codecs 72 + sha256: "41701a12ede9912663decd708279924ece5018566daa7d1f484d5f4f10894f91" 73 + url: "https://pub.dev" 74 + source: hosted 75 + version: "1.0.1" 76 + bluesky: 77 + dependency: "direct main" 78 + description: 79 + name: bluesky 80 + sha256: "207135e189278936dfc6bad0d59835a359f06b97ecd73eee1bccf6b993969428" 81 + url: "https://pub.dev" 82 + source: hosted 83 + version: "0.18.10" 28 84 boolean_selector: 29 85 dependency: transitive 30 86 description: ··· 33 89 url: "https://pub.dev" 34 90 source: hosted 35 91 version: "2.1.2" 92 + buffer: 93 + dependency: transitive 94 + description: 95 + name: buffer 96 + sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1" 97 + url: "https://pub.dev" 98 + source: hosted 99 + version: "1.2.3" 36 100 cached_network_image: 37 101 dependency: "direct main" 38 102 description: ··· 97 161 url: "https://pub.dev" 98 162 source: hosted 99 163 version: "0.3.5" 164 + cbor: 165 + dependency: transitive 166 + description: 167 + name: cbor 168 + sha256: e60380c7329da6b415841be93884b8d4380cbd86cd4cecb2067baa221b8d88b5 169 + url: "https://pub.dev" 170 + source: hosted 171 + version: "6.3.5" 100 172 characters: 101 173 dependency: transitive 102 174 description: ··· 137 209 url: "https://pub.dev" 138 210 source: hosted 139 211 version: "1.19.1" 212 + convert: 213 + dependency: transitive 214 + description: 215 + name: convert 216 + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 217 + url: "https://pub.dev" 218 + source: hosted 219 + version: "3.1.2" 140 220 cross_file: 141 221 dependency: transitive 142 222 description: ··· 169 249 url: "https://pub.dev" 170 250 source: hosted 171 251 version: "1.0.8" 252 + dart_multihash: 253 + dependency: transitive 254 + description: 255 + name: dart_multihash 256 + sha256: "7bef7091497c531f94bf82102805a69d97e4e5d120000dcbbc4a1da679060e0a" 257 + url: "https://pub.dev" 258 + source: hosted 259 + version: "1.0.1" 172 260 fake_async: 173 261 dependency: transitive 174 262 description: ··· 256 344 description: flutter 257 345 source: sdk 258 346 version: "0.0.0" 347 + freezed_annotation: 348 + dependency: transitive 349 + description: 350 + name: freezed_annotation 351 + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 352 + url: "https://pub.dev" 353 + source: hosted 354 + version: "2.4.4" 355 + hex: 356 + dependency: transitive 357 + description: 358 + name: hex 359 + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" 360 + url: "https://pub.dev" 361 + source: hosted 362 + version: "0.2.0" 259 363 html: 260 364 dependency: transitive 261 365 description: ··· 265 369 source: hosted 266 370 version: "0.15.5" 267 371 http: 268 - dependency: transitive 372 + dependency: "direct main" 269 373 description: 270 374 name: http 271 375 sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f ··· 280 384 url: "https://pub.dev" 281 385 source: hosted 282 386 version: "4.1.2" 387 + ieee754: 388 + dependency: transitive 389 + description: 390 + name: ieee754 391 + sha256: "7d87451c164a56c156180d34a4e93779372edd191d2c219206100b976203128c" 392 + url: "https://pub.dev" 393 + source: hosted 394 + version: "1.0.3" 283 395 image: 284 396 dependency: transitive 285 397 description: ··· 296 408 url: "https://pub.dev" 297 409 source: hosted 298 410 version: "0.2.2" 411 + js: 412 + dependency: transitive 413 + description: 414 + name: js 415 + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" 416 + url: "https://pub.dev" 417 + source: hosted 418 + version: "0.7.2" 299 419 json_annotation: 300 420 dependency: transitive 301 421 description: ··· 360 480 url: "https://pub.dev" 361 481 source: hosted 362 482 version: "1.16.0" 483 + mime: 484 + dependency: transitive 485 + description: 486 + name: mime 487 + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" 488 + url: "https://pub.dev" 489 + source: hosted 490 + version: "1.0.6" 491 + multiformats: 492 + dependency: transitive 493 + description: 494 + name: multiformats 495 + sha256: aa2fa36d2e4d0069dac993b35ee52e5165d67f15b995d68f797466065a6d05a5 496 + url: "https://pub.dev" 497 + source: hosted 498 + version: "0.2.3" 499 + nanoid: 500 + dependency: transitive 501 + description: 502 + name: nanoid 503 + sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e 504 + url: "https://pub.dev" 505 + source: hosted 506 + version: "1.0.0" 363 507 nested: 364 508 dependency: transitive 365 509 description: ··· 368 512 url: "https://pub.dev" 369 513 source: hosted 370 514 version: "1.0.0" 515 + nsid: 516 + dependency: transitive 517 + description: 518 + name: nsid 519 + sha256: f0e58c3899f7c224a7c9fb991be5bb2c18de0f920bec4e807ae2d3572cb718c1 520 + url: "https://pub.dev" 521 + source: hosted 522 + version: "0.4.1" 371 523 octo_image: 372 524 dependency: transitive 373 525 description: ··· 464 616 url: "https://pub.dev" 465 617 source: hosted 466 618 version: "2.1.8" 619 + pointycastle: 620 + dependency: transitive 621 + description: 622 + name: pointycastle 623 + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" 624 + url: "https://pub.dev" 625 + source: hosted 626 + version: "3.9.1" 467 627 posix: 468 628 dependency: transitive 469 629 description: ··· 488 648 url: "https://pub.dev" 489 649 source: hosted 490 650 version: "0.28.0" 651 + shared_preferences: 652 + dependency: "direct main" 653 + description: 654 + name: shared_preferences 655 + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" 656 + url: "https://pub.dev" 657 + source: hosted 658 + version: "2.5.2" 659 + shared_preferences_android: 660 + dependency: transitive 661 + description: 662 + name: shared_preferences_android 663 + sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" 664 + url: "https://pub.dev" 665 + source: hosted 666 + version: "2.4.8" 667 + shared_preferences_foundation: 668 + dependency: transitive 669 + description: 670 + name: shared_preferences_foundation 671 + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" 672 + url: "https://pub.dev" 673 + source: hosted 674 + version: "2.5.4" 675 + shared_preferences_linux: 676 + dependency: transitive 677 + description: 678 + name: shared_preferences_linux 679 + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" 680 + url: "https://pub.dev" 681 + source: hosted 682 + version: "2.4.1" 683 + shared_preferences_platform_interface: 684 + dependency: transitive 685 + description: 686 + name: shared_preferences_platform_interface 687 + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" 688 + url: "https://pub.dev" 689 + source: hosted 690 + version: "2.4.1" 691 + shared_preferences_web: 692 + dependency: transitive 693 + description: 694 + name: shared_preferences_web 695 + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 696 + url: "https://pub.dev" 697 + source: hosted 698 + version: "2.4.3" 699 + shared_preferences_windows: 700 + dependency: transitive 701 + description: 702 + name: shared_preferences_windows 703 + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" 704 + url: "https://pub.dev" 705 + source: hosted 706 + version: "2.4.1" 491 707 sky_engine: 492 708 dependency: transitive 493 709 description: flutter ··· 613 829 url: "https://pub.dev" 614 830 source: hosted 615 831 version: "1.4.0" 832 + universal_io: 833 + dependency: transitive 834 + description: 835 + name: universal_io 836 + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" 837 + url: "https://pub.dev" 838 + source: hosted 839 + version: "2.2.2" 616 840 uuid: 617 841 dependency: transitive 618 842 description: ··· 709 933 url: "https://pub.dev" 710 934 source: hosted 711 935 version: "1.1.1" 936 + web_socket: 937 + dependency: transitive 938 + description: 939 + name: web_socket 940 + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" 941 + url: "https://pub.dev" 942 + source: hosted 943 + version: "0.1.6" 944 + web_socket_channel: 945 + dependency: transitive 946 + description: 947 + name: web_socket_channel 948 + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" 949 + url: "https://pub.dev" 950 + source: hosted 951 + version: "3.0.2" 712 952 xdg_directories: 713 953 dependency: transitive 714 954 description: ··· 725 965 url: "https://pub.dev" 726 966 source: hosted 727 967 version: "6.5.0" 968 + xrpc: 969 + dependency: transitive 970 + description: 971 + name: xrpc 972 + sha256: bacfa0f6824fdeaa631aad1a5fd064c3f140c771fed94cbd04df3b7d1e008709 973 + url: "https://pub.dev" 974 + source: hosted 975 + version: "0.6.1" 728 976 yaml: 729 977 dependency: transitive 730 978 description:
+4
pubspec.yaml
··· 18 18 provider: ^6.1.1 19 19 flutter_launcher_icons: ^0.14.3 20 20 flutter_svg: ^2.0.7 21 + atproto: ^0.13.3 22 + bluesky: ^0.18.10 23 + http: ^1.1.0 24 + shared_preferences: ^2.5.2 21 25 22 26 dev_dependencies: 23 27 flutter_test: