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

fix: show exceeds 100mb error to user

+178 -38
+30 -5
lib/src/core/network/atproto/data/repositories/feed_repository_impl.dart
··· 23 23 import 'package:spark/src/core/utils/logging/log_service.dart'; 24 24 import 'package:spark/src/core/utils/logging/logger.dart'; 25 25 import 'package:spark/src/core/utils/share_urls.dart'; 26 + import 'package:spark/src/core/utils/video_upload_exception.dart'; 26 27 27 28 /// Implementation of Feed-related API endpoints 28 29 class FeedRepositoryImpl implements FeedRepository { ··· 1194 1195 } 1195 1196 1196 1197 // Check if the video is in a compatible format 1197 - final videoBytes = await file.readAsBytes(); 1198 - if (videoBytes.isEmpty) { 1198 + final videoSizeBytes = await file.length(); 1199 + if (videoSizeBytes == 0) { 1199 1200 throw Exception('Video file is empty'); 1200 1201 } 1201 1202 1202 - _logger.i('Video file size: ${videoBytes.length} bytes'); 1203 + _logger.i('Video file size: $videoSizeBytes bytes'); 1204 + final maxUploadSizeBytes = (AppConfig.maxUploadSizeMB * 1024 * 1024) 1205 + .round(); 1206 + if (maxUploadSizeBytes > 0 && videoSizeBytes > maxUploadSizeBytes) { 1207 + _logger.w( 1208 + 'Video file exceeds upload limit: $videoSizeBytes bytes ' 1209 + '(limit: $maxUploadSizeBytes bytes)', 1210 + ); 1211 + throw VideoUploadException( 1212 + 'Video is too large to upload.', 1213 + statusCode: 413, 1214 + uploadSizeBytes: videoSizeBytes, 1215 + limitBytes: maxUploadSizeBytes, 1216 + ); 1217 + } 1218 + final videoBytes = await file.readAsBytes(); 1203 1219 1204 1220 final pdsService = authAtProto.service; 1205 1221 final serviceTokenRes = await authAtProto.server.getServiceAuth( ··· 1226 1242 ); 1227 1243 1228 1244 if (response.statusCode != 200) { 1229 - throw Exception( 1230 - 'Failed to upload video: ${response.statusCode} ${response.body}', 1245 + _logger.e( 1246 + 'Video upload failed: ${response.statusCode} ${response.body}', 1247 + ); 1248 + throw VideoUploadException( 1249 + response.statusCode == 413 1250 + ? 'Video is too large to upload.' 1251 + : 'Failed to upload video.', 1252 + statusCode: response.statusCode, 1253 + uploadSizeBytes: videoSizeBytes, 1254 + limitBytes: maxUploadSizeBytes > 0 ? maxUploadSizeBytes : null, 1255 + responseBody: response.body, 1231 1256 ); 1232 1257 } 1233 1258
+39
lib/src/core/utils/error_messages.dart
··· 1 + import 'package:spark/src/core/utils/video_upload_exception.dart'; 2 + 1 3 /// Utility for converting exceptions into user-friendly error messages. 2 4 /// This prevents exposing internal implementation details to users while still 3 5 /// providing helpful feedback. ··· 8 10 return 'An unexpected error occurred'; 9 11 } 10 12 13 + if (error is VideoUploadException) { 14 + if (error.isPayloadTooLarge) { 15 + final uploadSize = error.uploadSizeBytes; 16 + final limit = error.limitBytes; 17 + if (uploadSize != null && limit != null) { 18 + return 'This video is too large to upload ' 19 + '(${_formatBytes(uploadSize)}). Please trim or compress it under ' 20 + '${_formatBytes(limit)} and try again.'; 21 + } 22 + return 'This video is too large to upload. Please trim or compress it and try again.'; 23 + } 24 + return 'Unable to upload video. Please try again'; 25 + } 26 + 11 27 final errorStr = error.toString().toLowerCase(); 28 + 29 + // Upload size errors 30 + if (errorStr.contains('413') || 31 + errorStr.contains('payload too large') || 32 + errorStr.contains('too large')) { 33 + return 'This file is too large to upload. Please trim or compress it and try again.'; 34 + } 12 35 13 36 // Network errors 14 37 if (errorStr.contains('socketexception') || ··· 111 134 } 112 135 113 136 return baseMessage; 137 + } 138 + 139 + static String _formatBytes(int bytes) { 140 + const mb = 1024 * 1024; 141 + if (bytes >= mb) { 142 + final value = bytes / mb; 143 + final formatted = value >= 10 144 + ? value.toStringAsFixed(0) 145 + : value.toStringAsFixed(1); 146 + return '$formatted MB'; 147 + } 148 + const kb = 1024; 149 + if (bytes >= kb) { 150 + return '${(bytes / kb).toStringAsFixed(0)} KB'; 151 + } 152 + return '$bytes B'; 114 153 } 115 154 }
+25
lib/src/core/utils/video_upload_exception.dart
··· 1 + /// Error raised when the video processing service rejects an upload. 2 + class VideoUploadException implements Exception { 3 + const VideoUploadException( 4 + this.message, { 5 + this.statusCode, 6 + this.uploadSizeBytes, 7 + this.limitBytes, 8 + this.responseBody, 9 + }); 10 + 11 + final String message; 12 + final int? statusCode; 13 + final int? uploadSizeBytes; 14 + final int? limitBytes; 15 + final String? responseBody; 16 + 17 + bool get isPayloadTooLarge => 18 + statusCode == 413 || 19 + (uploadSizeBytes != null && 20 + limitBytes != null && 21 + uploadSizeBytes! > limitBytes!); 22 + 23 + @override 24 + String toString() => message; 25 + }
+2 -2
lib/src/features/posting/providers/video_upload_provider.dart
··· 33 33 error: error, 34 34 stackTrace: stackTrace, 35 35 ); 36 - return null; 36 + rethrow; 37 37 } 38 38 } 39 39 ··· 95 95 return finalResult; 96 96 } catch (error, stackTrace) { 97 97 logger.e('Error posting video', error: error, stackTrace: stackTrace); 98 + rethrow; 98 99 } 99 - return null; 100 100 } 101 101 102 102 /// Process video and post it in one step
+13 -4
lib/src/features/posting/ui/pages/recording_page.dart
··· 10 10 import 'package:spark/src/core/pro_video_editor/models/video_editor_result.dart'; 11 11 import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository.dart'; 12 12 import 'package:spark/src/core/routing/app_router.dart'; 13 + import 'package:spark/src/core/utils/error_messages.dart'; 13 14 import 'package:spark/src/core/utils/logging/logging.dart'; 14 15 import 'package:spark/src/features/posting/providers/camera_provider.dart'; 15 16 import 'package:spark/src/features/posting/providers/recording_provider.dart'; ··· 240 241 _isExiting = false; 241 242 }); 242 243 ScaffoldMessenger.of(context).showSnackBar( 243 - SnackBar(content: Text('Failed to post story: $e')), 244 + SnackBar( 245 + content: Text( 246 + ErrorMessages.getOperationErrorMessage('post', e), 247 + ), 248 + ), 244 249 ); 245 250 } 246 251 } ··· 373 378 stackTrace: stackTrace, 374 379 ); 375 380 if (mounted) { 376 - ScaffoldMessenger.of( 377 - context, 378 - ).showSnackBar(SnackBar(content: Text('Failed to post story: $e'))); 381 + ScaffoldMessenger.of(context).showSnackBar( 382 + SnackBar( 383 + content: Text( 384 + ErrorMessages.getOperationErrorMessage('post', e), 385 + ), 386 + ), 387 + ); 379 388 } 380 389 } 381 390 // If posting failed or was cancelled, reset state
+2 -1
lib/src/features/posting/ui/pages/story_post_page.dart
··· 7 7 import 'package:spark/src/core/design_system/tokens/colors.dart'; 8 8 import 'package:spark/src/core/network/atproto/data/models/models.dart'; 9 9 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 10 + import 'package:spark/src/core/utils/error_messages.dart'; 10 11 import 'package:spark/src/features/posting/providers/post_story.dart'; 11 12 import 'package:spark/src/features/posting/providers/video_upload_provider.dart'; 12 13 ··· 72 73 if (mounted) { 73 74 setState(() { 74 75 _isPosting = false; 75 - _error = e.toString(); 76 + _error = ErrorMessages.getOperationErrorMessage('post', e); 76 77 }); 77 78 } 78 79 }
+33 -26
lib/src/features/posting/ui/pages/video_review_page.dart
··· 10 10 import 'package:spark/src/core/design_system/tokens/constants.dart'; 11 11 import 'package:spark/src/core/routing/app_router.dart'; 12 12 import 'package:spark/src/core/ui/widgets/alt_text_editor_dialog.dart'; 13 + import 'package:spark/src/core/utils/error_messages.dart'; 13 14 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 14 15 import 'package:spark/src/features/posting/models/mention_controller.dart'; 15 16 import 'package:spark/src/features/posting/providers/video_upload_provider.dart'; ··· 106 107 ).future, 107 108 ); 108 109 110 + if (!mounted) return; 109 111 setState(() { 110 112 _isPosting = false; 111 113 }); 112 114 113 - if (mounted) { 114 - context.router.popUntilRoot(); 115 - final did = ref.read(currentDidProvider); 116 - if (did != null) { 117 - ref 118 - ..invalidate( 119 - profileFeedProvider(AtUri.parse('at://$did'), false, false), 120 - ) 121 - ..invalidate( 122 - profileFeedProvider(AtUri.parse('at://$did'), true, false), 123 - ); 124 - } 125 - if (postRef == null) { 126 - return; 127 - } else { 128 - if (!widget.storyMode) { 129 - context.router.push( 130 - StandalonePostRoute(postUri: postRef.uri.toString()), 131 - ); 132 - } 133 - } 115 + if (postRef == null) { 116 + _showPostError('Unable to create post. Please try again'); 117 + return; 134 118 } 135 - } catch (e) { 136 - if (mounted) { 137 - setState(() { 138 - _isPosting = false; 139 - }); 119 + 120 + final did = ref.read(currentDidProvider); 121 + if (did != null) { 122 + ref 123 + ..invalidate( 124 + profileFeedProvider(AtUri.parse('at://$did'), false, false), 125 + ) 126 + ..invalidate( 127 + profileFeedProvider(AtUri.parse('at://$did'), true, false), 128 + ); 129 + } 130 + 131 + final router = context.router; 132 + router.popUntilRoot(); 133 + if (!widget.storyMode) { 134 + router.push(StandalonePostRoute(postUri: postRef.uri.toString())); 140 135 } 136 + } catch (e) { 137 + if (!mounted) return; 138 + setState(() { 139 + _isPosting = false; 140 + }); 141 + _showPostError(ErrorMessages.getOperationErrorMessage('post', e)); 141 142 } 142 143 return; 144 + } 145 + 146 + void _showPostError(String message) { 147 + ScaffoldMessenger.of(context) 148 + ..hideCurrentSnackBar() 149 + ..showSnackBar(SnackBar(content: Text(message))); 143 150 } 144 151 145 152 @override
+34
test/src/core/utils/error_messages_test.dart
··· 1 + import 'package:flutter_test/flutter_test.dart'; 2 + import 'package:spark/src/core/utils/error_messages.dart'; 3 + import 'package:spark/src/core/utils/video_upload_exception.dart'; 4 + 5 + void main() { 6 + group('ErrorMessages', () { 7 + test('describes oversized video uploads with sizes', () { 8 + const error = VideoUploadException( 9 + 'Video is too large to upload.', 10 + statusCode: 413, 11 + uploadSizeBytes: 120 * 1024 * 1024, 12 + limitBytes: 100 * 1024 * 1024, 13 + ); 14 + 15 + final message = ErrorMessages.getOperationErrorMessage('post', error); 16 + 17 + expect(message, contains('This video is too large to upload')); 18 + expect(message, contains('120 MB')); 19 + expect(message, contains('under 100 MB')); 20 + }); 21 + 22 + test('maps raw 413 payload errors to a safe upload message', () { 23 + final message = ErrorMessages.getOperationErrorMessage( 24 + 'post', 25 + Exception('Failed to upload video: 413 Payload Too Large'), 26 + ); 27 + 28 + expect( 29 + message, 30 + 'This file is too large to upload. Please trim or compress it and try again.', 31 + ); 32 + }); 33 + }); 34 + }