diff --git a/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart b/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart index 9230bbf..c6c6860 100644 --- a/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart +++ b/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spark/src/core/design_system/components/molecules/create_media_sheet.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/components/molecules/feed_tag_list.dart'; import 'package:spark/src/core/design_system/templates/feeds_bar_template.dart'; @@ -25,21 +24,6 @@ class FeedsBar extends ConsumerStatefulWidget implements PreferredSizeWidget { } class _FeedsBarState extends ConsumerState { - void _showCreateMenu(BuildContext context) { - showCreateMediaSheet( - context, - onRecord: CreateMediaActions.onRecord(context, storyMode: false), - onUploadVideo: CreateMediaActions.onUploadVideo( - context, - storyMode: false, - ), - onUploadImages: CreateMediaActions.onUploadImages( - context, - storyMode: false, - ), - ); - } - void _showFeedOptionsSheet(BuildContext context, Feed feed) { final isTimeline = feed.type == 'timeline' && feed.config.value == 'following'; @@ -194,7 +178,7 @@ class _FeedsBarState extends ConsumerState { return FeedsBarTemplate( tags: tags, selectedTagId: settings.activeFeed.config.id, - onLeadingPressed: () => _showCreateMenu(context), + onLeadingPressed: CreateMediaActions.onRecord(context, storyMode: false), onTagTap: (tagId) { final feed = pinnedFeeds.firstWhere((f) => f.config.id == tagId); diff --git a/lib/src/features/posting/providers/recording_provider.dart b/lib/src/features/posting/providers/recording_provider.dart index 58a4844..689dea4 100644 --- a/lib/src/features/posting/providers/recording_provider.dart +++ b/lib/src/features/posting/providers/recording_provider.dart @@ -11,6 +11,7 @@ part 'recording_provider.g.dart'; @riverpod class Recording extends _$Recording { Timer? _timer; + final List _segmentPaths = []; @override RecordingState build() { @@ -55,8 +56,9 @@ class Recording extends _$Recording { } void addSegment(XFile file) { + _segmentPaths.add(file.path); state = state.copyWith( - segmentPaths: [...state.segmentPaths, file.path], + segmentPaths: List.unmodifiable(_segmentPaths), error: null, ); } @@ -96,11 +98,20 @@ class Recording extends _$Recording { stopTimer(); final keepSet = keepPaths.toSet(); - final pathsToDelete = state.segmentPaths.where( - (path) => !keepSet.contains(path), - ); + final pathsToDelete = _segmentPaths + .where((path) => !keepSet.contains(path)) + .toList(); + + await _deleteTemporaryFiles(pathsToDelete); + _segmentPaths.clear(); + + if (!ref.mounted) return; + + state = const RecordingState(); + } - for (final path in pathsToDelete) { + Future _deleteTemporaryFiles(Iterable paths) async { + for (final path in paths) { try { final file = File(path); if (await file.exists()) { @@ -110,12 +121,11 @@ class Recording extends _$Recording { // Best-effort cleanup for temporary session files. } } - - state = const RecordingState(); } void reset() { stopTimer(); + _segmentPaths.clear(); state = const RecordingState(); } @@ -125,6 +135,9 @@ class Recording extends _$Recording { } void _dispose() { + final pathsToDelete = List.of(_segmentPaths); + _segmentPaths.clear(); stopTimer(); + unawaited(_deleteTemporaryFiles(pathsToDelete)); } } diff --git a/lib/src/features/posting/ui/pages/recording_page.dart b/lib/src/features/posting/ui/pages/recording_page.dart index 18f65f9..1be5e7c 100644 --- a/lib/src/features/posting/ui/pages/recording_page.dart +++ b/lib/src/features/posting/ui/pages/recording_page.dart @@ -65,14 +65,10 @@ class _RecordingPageState extends ConsumerState { int _guideAudioPrepareRequestId = 0; int _soundPickerSessionId = 0; - // Store notifier reference for safe disposal - Recording? _recordingNotifier; - @override void initState() { super.initState(); _logger = GetIt.instance().getLogger('RecordingPage'); - _recordingNotifier = ref.read(recordingProvider.notifier); _guideAudioPlayer = AudioPlayer(); unawaited( _guideAudioPlayer.setAudioContext( @@ -1085,11 +1081,6 @@ class _RecordingPageState extends ConsumerState { void dispose() { _soundPickerSessionId++; unawaited(_guideAudioPlayer.dispose()); - // Defer modifying provider to avoid modifying while finalizing widget tree - final notifier = _recordingNotifier; - if (notifier != null) { - unawaited(notifier.discardSession()); - } super.dispose(); } } diff --git a/lib/src/features/profile/ui/pages/profile_page.dart b/lib/src/features/profile/ui/pages/profile_page.dart index a3a4a8f..e353fbd 100644 --- a/lib/src/features/profile/ui/pages/profile_page.dart +++ b/lib/src/features/profile/ui/pages/profile_page.dart @@ -6,7 +6,6 @@ import 'package:get_it/get_it.dart'; import 'package:spark/src/core/auth/data/repositories/identity_repository.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; import 'package:spark/src/core/design_system/components/atoms/profile_tab_item.dart'; -import 'package:spark/src/core/design_system/components/molecules/create_media_sheet.dart'; import 'package:spark/src/core/design_system/components/molecules/profile_tab_bar.dart'; import 'package:spark/src/core/design_system/templates/profile_page_template.dart'; import 'package:spark/src/core/media/create_media_actions.dart'; @@ -170,21 +169,6 @@ class _ProfilePageState extends ConsumerState { ); } - void _showCreateMenu(BuildContext context) { - showCreateMediaSheet( - context, - onRecord: CreateMediaActions.onRecord(context, storyMode: false), - onUploadVideo: CreateMediaActions.onUploadVideo( - context, - storyMode: false, - ), - onUploadImages: CreateMediaActions.onUploadImages( - context, - storyMode: false, - ), - ); - } - @override Widget build(BuildContext context) { final profileStateAsync = ref.watch( @@ -338,7 +322,10 @@ class _ProfilePageState extends ConsumerState { constraints: const BoxConstraints(), splashColor: Colors.transparent, highlightColor: Colors.transparent, - onPressed: () => _showCreateMenu(context), + onPressed: CreateMediaActions.onRecord( + context, + storyMode: false, + ), icon: AppIcons.addPostFilled(size: 28), ), ) diff --git a/test/src/features/posting/providers/recording_provider_test.dart b/test/src/features/posting/providers/recording_provider_test.dart index f018073..d1dd3aa 100644 --- a/test/src/features/posting/providers/recording_provider_test.dart +++ b/test/src/features/posting/providers/recording_provider_test.dart @@ -174,5 +174,36 @@ void main() { ); }, ); + + test('auto-dispose deletes temporary segment files', () async { + final container = ProviderContainer(); + + final subscription = container.listen( + recordingProvider, + (previous, next) {}, + ); + + final tempDir = await Directory.systemTemp.createTemp( + 'recording-provider-test', + ); + addTearDown(() async { + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + + final segmentFile = File('${tempDir.path}/segment.mp4') + ..writeAsStringSync('segment'); + + container + .read(recordingProvider.notifier) + .addSegment(XFile(segmentFile.path)); + + subscription.close(); + container.dispose(); + await Future.delayed(Duration.zero); + + expect(await segmentFile.exists(), isFalse); + }); }); }