[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: stories progress bar consistency

+87 -52
+45 -15
lib/src/features/stories/ui/pages/author_stories_page.dart
··· 38 38 39 39 class _AuthorStoriesPageState extends ConsumerState<AuthorStoriesPage> 40 40 with TickerProviderStateMixin { 41 + static const _defaultStoryDuration = Duration(seconds: 5); 41 42 late final PageController _pageController; 42 43 late final List<AnimationController> _progressControllers; 44 + late final List<bool> _storyLoadingStates; 43 45 int _currentStoryIndex = 0; 44 46 double _dragOffset = 0; 45 47 double _dragScale = 1; ··· 71 73 _progressControllers = List.generate( 72 74 widget.stories.length, 73 75 (_) => AnimationController( 74 - duration: const Duration(seconds: 5), 76 + duration: _defaultStoryDuration, 75 77 vsync: this, 76 78 ), 77 79 ); 80 + _storyLoadingStates = List<bool>.filled(widget.stories.length, true); 81 + } 82 + 83 + void _onStoryDurationChanged(int index, Duration duration) { 84 + if (index < 0 || index >= _progressControllers.length) return; 85 + final normalized = duration > Duration.zero 86 + ? duration 87 + : _defaultStoryDuration; 88 + _progressControllers[index].duration = normalized; 78 89 } 79 90 80 91 void _startCurrentStory() { 81 92 if (_currentStoryIndex >= widget.stories.length) return; 82 93 83 94 if (!_isCurrentStoryLoading) { 84 - _progressControllers[_currentStoryIndex].forward().whenComplete( 85 - _nextStory, 86 - ); 95 + _startProgressForCurrentStory(); 87 96 } 88 97 } 89 98 99 + void _startProgressForCurrentStory() { 100 + final storyIndex = _currentStoryIndex; 101 + final controller = _progressControllers[storyIndex]; 102 + controller.forward().whenComplete(() { 103 + if (!mounted) return; 104 + if (_currentStoryIndex != storyIndex) return; 105 + if (controller.status == AnimationStatus.completed) { 106 + _nextStory(); 107 + } 108 + }); 109 + } 110 + 90 111 void _pause() { 91 112 _progressControllers[_currentStoryIndex].stop(); 92 113 } ··· 95 116 final controller = _progressControllers[_currentStoryIndex]; 96 117 if (controller.status != AnimationStatus.completed && 97 118 !_isCurrentStoryLoading) { 98 - controller.forward(); 119 + _startProgressForCurrentStory(); 99 120 } 100 121 } 101 122 102 - void _onStoryLoadingStateChanged(bool isLoading) { 123 + void _onStoryLoadingStateChanged(int index, bool isLoading) { 124 + if (index < 0 || index >= _storyLoadingStates.length) return; 125 + _storyLoadingStates[index] = isLoading; 126 + if (index != _currentStoryIndex) return; 127 + 103 128 if (_isCurrentStoryLoading != isLoading) { 104 129 setState(() { 105 130 _isCurrentStoryLoading = isLoading; ··· 109 134 _pause(); 110 135 } else { 111 136 final controller = _progressControllers[_currentStoryIndex]; 112 - if (controller.status == AnimationStatus.dismissed) { 113 - controller.forward().whenComplete(_nextStory); 114 - } else if (controller.status == AnimationStatus.forward) { 115 - controller.forward(); 137 + if (controller.status != AnimationStatus.completed && 138 + !controller.isAnimating) { 139 + _startProgressForCurrentStory(); 116 140 } 117 141 } 118 142 } ··· 251 275 itemCount: widget.stories.length, 252 276 onPageChanged: (index) { 253 277 if (!_isDragging) { 254 - _progressControllers[_currentStoryIndex].reset(); 278 + final previousIndex = _currentStoryIndex; 279 + _progressControllers[previousIndex].stop(); 280 + _progressControllers[index].reset(); 255 281 setState(() { 256 282 _currentStoryIndex = index; 257 - _isCurrentStoryLoading = true; 283 + _isCurrentStoryLoading = _storyLoadingStates[index]; 258 284 }); 285 + if (!_isCurrentStoryLoading) { 286 + _startProgressForCurrentStory(); 287 + } 259 288 } 260 289 }, 261 290 itemBuilder: (context, index) { 262 291 final story = widget.stories[index]; 263 292 return StoryPage( 264 293 story: story, 265 - onLoadingStateChanged: index == _currentStoryIndex 266 - ? _onStoryLoadingStateChanged 267 - : null, 294 + onLoadingStateChanged: (isLoading) => 295 + _onStoryLoadingStateChanged(index, isLoading), 296 + onStoryDurationChanged: (duration) => 297 + _onStoryDurationChanged(index, duration), 268 298 ); 269 299 }, 270 300 ),
+33
lib/src/features/stories/ui/pages/story_page.dart
··· 11 11 required this.story, 12 12 super.key, 13 13 this.onLoadingStateChanged, 14 + this.onStoryDurationChanged, 14 15 }); 15 16 16 17 final StoryView story; 17 18 final ValueChanged<bool>? onLoadingStateChanged; 19 + final ValueChanged<Duration>? onStoryDurationChanged; 18 20 19 21 @override 20 22 ConsumerState<ConsumerStatefulWidget> createState() => _StoryPageState(); ··· 22 24 23 25 class _StoryPageState extends ConsumerState<StoryPage> 24 26 with TickerProviderStateMixin { 27 + static const _defaultStoryDuration = Duration(seconds: 5); 25 28 VideoPlayerController? _videoController; 26 29 bool _isVideoInitialized = false; 27 30 bool _isImageLoaded = false; ··· 30 33 @override 31 34 void initState() { 32 35 super.initState(); 36 + if (!_isVideoStory(widget.story)) { 37 + widget.onStoryDurationChanged?.call(_defaultStoryDuration); 38 + } 33 39 _initializeMedia(); 34 40 } 35 41 ··· 39 45 super.dispose(); 40 46 } 41 47 48 + @override 49 + void didUpdateWidget(covariant StoryPage oldWidget) { 50 + super.didUpdateWidget(oldWidget); 51 + 52 + // If this page becomes active after being prebuilt, ensure the parent gets 53 + // the latest loading state immediately. 54 + if (oldWidget.onLoadingStateChanged != widget.onLoadingStateChanged && 55 + widget.onLoadingStateChanged != null) { 56 + widget.onLoadingStateChanged!(_isLoading); 57 + } 58 + if (oldWidget.onStoryDurationChanged != widget.onStoryDurationChanged && 59 + widget.onStoryDurationChanged != null) { 60 + widget.onStoryDurationChanged!(_resolvedStoryDuration()); 61 + } 62 + } 63 + 64 + Duration _resolvedStoryDuration() { 65 + if (_isVideoStory(widget.story)) { 66 + final duration = _videoController?.value.duration; 67 + if (duration != null && duration > Duration.zero) { 68 + return duration; 69 + } 70 + } 71 + return _defaultStoryDuration; 72 + } 73 + 42 74 void _updateLoadingState() { 43 75 final isLoading = _isVideoStory(widget.story) 44 76 ? !_isVideoInitialized ··· 60 92 try { 61 93 await _videoController!.initialize(); 62 94 await _videoController!.setLooping(true); 95 + widget.onStoryDurationChanged?.call(_resolvedStoryDuration()); 63 96 await _videoController!.play(); 64 97 if (mounted) { 65 98 setState(() {
+5 -37
pubspec.lock
··· 411 411 url: "https://pub.dev" 412 412 source: hosted 413 413 version: "1.1.2" 414 - code_assets: 415 - dependency: transitive 416 - description: 417 - name: code_assets 418 - sha256: ae0db647e668cbb295a3527f0938e4039e004c80099dce2f964102373f5ce0b5 419 - url: "https://pub.dev" 420 - source: hosted 421 - version: "0.19.10" 422 414 code_builder: 423 415 dependency: transitive 424 416 description: ··· 887 879 url: "https://pub.dev" 888 880 source: hosted 889 881 version: "0.2.0" 890 - hooks: 891 - dependency: transitive 892 - description: 893 - name: hooks 894 - sha256: "5410b9f4f6c9f01e8ff0eb81c9801ea13a3c3d39f8f0b1613cda08e27eab3c18" 895 - url: "https://pub.dev" 896 - source: hosted 897 - version: "0.20.5" 898 882 hotreloader: 899 883 dependency: transitive 900 884 description: ··· 1159 1143 url: "https://pub.dev" 1160 1144 source: hosted 1161 1145 version: "1.0.0" 1162 - native_toolchain_c: 1163 - dependency: transitive 1164 - description: 1165 - name: native_toolchain_c 1166 - sha256: f8872ea6c7a50ce08db9ae280ca2b8efdd973157ce462826c82f3c3051d154ce 1167 - url: "https://pub.dev" 1168 - source: hosted 1169 - version: "0.17.2" 1170 1146 nested: 1171 1147 dependency: transitive 1172 1148 description: ··· 1183 1159 url: "https://pub.dev" 1184 1160 source: hosted 1185 1161 version: "2.0.2" 1186 - objective_c: 1187 - dependency: transitive 1188 - description: 1189 - name: objective_c 1190 - sha256: "55eb67ede1002d9771b3f9264d2c9d30bc364f0267bc1c6cc0883280d5f0c7cb" 1191 - url: "https://pub.dev" 1192 - source: hosted 1193 - version: "9.2.2" 1194 1162 octo_image: 1195 1163 dependency: transitive 1196 1164 description: ··· 1256 1224 source: hosted 1257 1225 version: "2.2.22" 1258 1226 path_provider_foundation: 1259 - dependency: transitive 1227 + dependency: "direct overridden" 1260 1228 description: 1261 1229 name: path_provider_foundation 1262 - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" 1230 + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" 1263 1231 url: "https://pub.dev" 1264 1232 source: hosted 1265 - version: "2.6.0" 1233 + version: "2.5.1" 1266 1234 path_provider_linux: 1267 1235 dependency: transitive 1268 1236 description: ··· 2006 1974 source: hosted 2007 1975 version: "3.1.3" 2008 1976 sdks: 2009 - dart: ">=3.10.3 <4.0.0" 2010 - flutter: ">=3.38.4" 1977 + dart: ">=3.10.0 <4.0.0" 1978 + flutter: ">=3.38.0"
+2
pubspec.yaml
··· 79 79 git: 80 80 url: https://github.com/knotbin/atproto.dart.git 81 81 path: packages/atproto_oauth 82 + # temporary env fix 83 + path_provider_foundation: 2.5.1 82 84 83 85 dev_dependencies: 84 86 auto_route_generator: ^10.2.3
+2
widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift
··· 15 15 import flutter_web_auth_2 16 16 import fvp 17 17 import package_info_plus 18 + import path_provider_foundation 18 19 import posthog_flutter 19 20 import pro_video_editor 20 21 import shared_preferences_foundation ··· 35 36 FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) 36 37 FvpPlugin.register(with: registry.registrar(forPlugin: "FvpPlugin")) 37 38 FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 39 + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 38 40 PosthogFlutterPlugin.register(with: registry.registrar(forPlugin: "PosthogFlutterPlugin")) 39 41 ProVideoEditorPlugin.register(with: registry.registrar(forPlugin: "ProVideoEditorPlugin")) 40 42 SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))