import 'dart:async'; import 'package:aves/model/actions/video_actions.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/pedantic.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/video_speed_dialog.dart'; import 'package:aves/widgets/dialogs/video_stream_selection_dialog.dart'; import 'package:aves/widgets/settings/video/video.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens? collection; VideoActionDelegate({ required this.collection, }); void onActionSelected(BuildContext context, AvesVideoController controller, VideoAction action) { switch (action) { case VideoAction.captureFrame: _captureFrame(context, controller); break; case VideoAction.replay10: if (controller.isReady) controller.seekTo(controller.currentPosition - 10000); break; case VideoAction.skip10: if (controller.isReady) controller.seekTo(controller.currentPosition + 10000); break; case VideoAction.selectStreams: _showStreamSelectionDialog(context, controller); break; case VideoAction.setSpeed: _showSpeedDialog(context, controller); break; case VideoAction.settings: _showSettings(context); break; case VideoAction.togglePlay: _togglePlayPause(context, controller); break; } } Future _captureFrame(BuildContext context, AvesVideoController controller) async { final positionMillis = controller.currentPosition; final bytes = await controller.captureFrame(); final destinationAlbum = androidFileUtils.videoCapturesPath; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; if (!await checkFreeSpace(context, bytes.length, destinationAlbum)) return; final entry = controller.entry; final rotationDegrees = entry.rotationDegrees; final dateTimeMillis = entry.catalogMetadata?.dateMillis; final latLng = entry.latLng; final exif = { if (rotationDegrees != 0) 'rotationDegrees': rotationDegrees, if (dateTimeMillis != null && dateTimeMillis != 0) 'dateTimeMillis': dateTimeMillis, if (latLng != null) ...{ 'latitude': latLng.latitude, 'longitude': latLng.longitude, } }; final newFields = await imageFileService.captureFrame( entry, desiredName: '${entry.bestTitle}_${'$positionMillis'.padLeft(8, '0')}', exif: exif, bytes: bytes, destinationAlbum: destinationAlbum, ); final success = newFields.isNotEmpty; if (success) { final _collection = collection; final showAction = _collection != null ? SnackBarAction( label: context.l10n.showButtonLabel, onPressed: () async { final highlightInfo = context.read(); final source = _collection.source; final targetCollection = CollectionLens( source: source, filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, ); unawaited(Navigator.pushAndRemoveUntil( context, MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( collection: targetCollection, ), ), (route) => false, )); await Future.delayed(Durations.staggeredAnimationPageTarget + Durations.highlightScrollInitDelay); final newUri = newFields['uri'] as String?; final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => entry.uri == newUri); if (targetEntry != null) { highlightInfo.trackItem(targetEntry, highlightItem: targetEntry); } }, ) : null; showFeedback(context, context.l10n.genericSuccessFeedback, showAction); } else { showFeedback(context, context.l10n.genericFailureFeedback); } } Future _showStreamSelectionDialog(BuildContext context, AvesVideoController controller) async { final streams = controller.streams; final currentSelectedStreams = await Future.wait(StreamType.values.map(controller.getSelectedStream)); final currentSelectedIndices = currentSelectedStreams.whereNotNull().map((v) => v.index).toSet(); final userSelectedStreams = await showDialog>( context: context, builder: (context) => VideoStreamSelectionDialog( streams: Map.fromEntries(streams.map((stream) => MapEntry(stream, currentSelectedIndices.contains(stream.index)))), ), ); if (userSelectedStreams == null || userSelectedStreams.isEmpty) return; await Future.forEach>( userSelectedStreams.entries, (kv) => controller.selectStream(kv.key, kv.value), ); } Future _showSpeedDialog(BuildContext context, AvesVideoController controller) async { final newSpeed = await showDialog( context: context, builder: (context) => VideoSpeedDialog( current: controller.speed, min: controller.minSpeed, max: controller.maxSpeed, ), ); if (newSpeed == null) return; controller.speed = newSpeed; } void _showSettings(BuildContext context) { Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: VideoSettingsPage.routeName), builder: (context) => const VideoSettingsPage(), ), ); } Future _togglePlayPause(BuildContext context, AvesVideoController controller) async { if (controller.isPlaying) { await controller.pause(); } else { await controller.play(); } } }