diff --git a/assets/config/dev.json b/assets/config/dev.json index 42e03e393b..0c787c8d69 100644 --- a/assets/config/dev.json +++ b/assets/config/dev.json @@ -7,7 +7,8 @@ "allowedDataItemSizeForTurbo": 500000, "enableQuickSyncAuthoring": true, "enableMultipleFileDownload": true, - "enableVideoPreview": false, + "enableVideoPreview": true, + "enableAudioPreview": true, "stripePublishableKey": "pk_test_51JUAtwC8apPOWkDLh2FPZkQkiKZEkTo6wqgLCtQoClL6S4l2jlbbc5MgOdwOUdU9Tn93NNvqAGbu115lkJChMikG00XUfTmo2z", "enablePins": true } diff --git a/assets/config/prod.json b/assets/config/prod.json index b55cff45c2..5d3e2ce3cd 100644 --- a/assets/config/prod.json +++ b/assets/config/prod.json @@ -8,6 +8,7 @@ "enableQuickSyncAuthoring": true, "enableMultipleFileDownload": true, "enableVideoPreview": false, + "enableAudioPreview": true, "stripePublishableKey": "pk_live_51JUAtwC8apPOWkDLMQqNF9sPpfneNSPnwX8YZ8y1FNDl6v94hZIwzgFSYl27bWE4Oos8CLquunUswKrKcaDhDO6m002Yj9AeKj", "enablePins": true } diff --git a/assets/config/staging.json b/assets/config/staging.json index b55cff45c2..5d3e2ce3cd 100644 --- a/assets/config/staging.json +++ b/assets/config/staging.json @@ -8,6 +8,7 @@ "enableQuickSyncAuthoring": true, "enableMultipleFileDownload": true, "enableVideoPreview": false, + "enableAudioPreview": true, "stripePublishableKey": "pk_live_51JUAtwC8apPOWkDLMQqNF9sPpfneNSPnwX8YZ8y1FNDl6v94hZIwzgFSYl27bWE4Oos8CLquunUswKrKcaDhDO6m002Yj9AeKj", "enablePins": true } diff --git a/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart b/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart index 73c5cc3c22..589390de68 100644 --- a/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart +++ b/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart @@ -85,6 +85,15 @@ class FsEntryPreviewCubit extends Cubit { } break; + + case 'audio': + _previewAudio( + fileKey != null, + selectedItem, + previewUrl, + ); + break; + case 'video': _previewVideo( fileKey != null, @@ -175,6 +184,16 @@ class FsEntryPreviewCubit extends Cubit { case 'image': emitImagePreview(file, previewUrl); break; + + case 'audio': + _previewAudio( + (selectedItem as FileDataTableItem).pinnedDataOwnerAddress == + null && + drive.isPrivate, + selectedItem, + previewUrl, + ); + break; case 'video': _previewVideo( drive.isPrivate, @@ -193,6 +212,23 @@ class FsEntryPreviewCubit extends Cubit { } } + void _previewAudio( + bool isPrivate, FileDataTableItem selectedItem, previewUrl) { + if (_configService.config.enableAudioPreview) { + if (isPrivate) { + emit(FsEntryPreviewUnavailable()); + return; + } + + emit(FsEntryPreviewAudio( + filename: selectedItem.name, previewUrl: previewUrl)); + + return; + } + + emit(FsEntryPreviewUnavailable()); + } + void _previewVideo( bool isPrivate, FileDataTableItem selectedItem, previewUrl) { if (_configService.config.enableVideoPreview) { @@ -201,7 +237,8 @@ class FsEntryPreviewCubit extends Cubit { return; } - emit(FsEntryPreviewVideo(previewUrl: previewUrl)); + emit(FsEntryPreviewVideo( + filename: selectedItem.name, previewUrl: previewUrl)); return; } @@ -298,6 +335,9 @@ class FsEntryPreviewCubit extends Cubit { case 'image': return supportedImageTypesInFilePreview .any((element) => element.contains(fileExtension)); + case 'audio': + return audioContentTypes + .any((element) => element.contains(fileExtension)); case 'video': return videoContentTypes .any((element) => element.contains(fileExtension)); diff --git a/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart b/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart index 91cc9ee043..f7ed5d9a4f 100644 --- a/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart +++ b/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart @@ -37,21 +37,25 @@ class FsEntryPreviewImage extends FsEntryPreviewSuccess { } class FsEntryPreviewAudio extends FsEntryPreviewSuccess { - const FsEntryPreviewAudio({ - required String previewUrl, - }) : super(previewUrl: previewUrl); + final String filename; + const FsEntryPreviewAudio( + {required String previewUrl, required this.filename}) + : super(previewUrl: previewUrl); @override - List get props => [previewUrl]; + List get props => [previewUrl, filename]; } class FsEntryPreviewVideo extends FsEntryPreviewSuccess { + final String filename; + const FsEntryPreviewVideo({ required String previewUrl, + required this.filename, }) : super(previewUrl: previewUrl); @override - List get props => [previewUrl]; + List get props => [previewUrl, filename]; } class FsEntryPreviewMemory extends FsEntryPreviewSuccess { diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index 6b42f75317..fa17f022aa 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -233,6 +233,19 @@ class AppConfigWindowManagerState extends State { type: ArDriveDevToolOptionType.bool, ); + ArDriveDevToolOption enableAudioPreviewOption = ArDriveDevToolOption( + name: 'enableAudioPreview', + value: settings.enableAudioPreview, + onChange: (value) { + setState(() { + configService.updateAppConfig( + settings.copyWith(enableAudioPreview: value), + ); + }); + }, + type: ArDriveDevToolOptionType.bool, + ); + ArDriveDevToolOption autoSyncIntervalInSecondsOption = ArDriveDevToolOption( name: 'autoSyncIntervalInSeconds', value: settings.autoSyncIntervalInSeconds, @@ -323,6 +336,7 @@ class AppConfigWindowManagerState extends State { enableQuickSyncAuthoringOption, enableMultipleFileDownloadOption, enableVideoPreviewOption, + enableAudioPreviewOption, enableSeedPhreaseLogin, allowedDataItemSizeForTurboOption, defaultArweaveGatewayUrlOption, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e00e186472..e17d8cbcd9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1302,6 +1302,10 @@ "@noOneWillSee": { "description": "Indicates that private content won't reach to the wrong hands" }, + "normal": "Normal", + "@normal": { + "description": "Menu entry for \"Normal\" speed playback on Audio and Video previews." + }, "noSubscriptions": "No subscriptions are needed!", "@noSubscriptions": { "description": "Payment philosophy" diff --git a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart index 1e6219d74b..b6f65d00b3 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -1,5 +1,7 @@ part of '../drive_detail_page.dart'; +const List _speedOptions = [.25, .5, .75, 1, 1.25, 1.5, 1.75, 2]; + class FsEntryPreviewWidget extends StatefulWidget { const FsEntryPreviewWidget({ Key? key, @@ -35,18 +37,51 @@ class _FsEntryPreviewWidgetState extends State { ), ); + case FsEntryPreviewAudio: + return AudioPlayerWidget( + filename: (widget.state as FsEntryPreviewAudio).filename, + audioUrl: (widget.state as FsEntryPreviewAudio).previewUrl, + ); + default: return VideoPlayerWidget( + filename: (widget.state as FsEntryPreviewVideo).filename, videoUrl: (widget.state as FsEntryPreviewVideo).previewUrl, ); } } } +String getTimeString(Duration duration) { + int durSeconds = duration.inSeconds; + const hour = 60 * 60; + const minute = 60; + + final hours = (durSeconds / hour).floor(); + final minutes = ((durSeconds % hour) / minute).floor(); + final seconds = durSeconds % minute; + + String timeString = ''; + + if (hours > 0) { + timeString = '${hours.floor()}:'; + } + + timeString += + hours > 0 ? minutes.toString().padLeft(2, '0') : minutes.toString(); + timeString += ':'; + timeString += seconds.toString().padLeft(2, '0'); + + return timeString; +} + class VideoPlayerWidget extends StatefulWidget { final String videoUrl; + final String filename; - const VideoPlayerWidget({Key? key, required this.videoUrl}) : super(key: key); + const VideoPlayerWidget( + {Key? key, required this.filename, required this.videoUrl}) + : super(key: key); @override // ignore: library_private_types_in_public_api @@ -55,8 +90,7 @@ class VideoPlayerWidget extends StatefulWidget { class _VideoPlayerWidgetState extends State { late VideoPlayerController _videoPlayerController; - late ChewieController _chewieController; - bool _isPlaying = false; + late VideoPlayer _videoPlayer; @override void initState() { @@ -64,57 +98,707 @@ class _VideoPlayerWidgetState extends State { super.initState(); _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController, - autoPlay: false, - looping: true, - showControls: true, - allowFullScreen: false, - aspectRatio: 1, - errorBuilder: (context, errorMessage) { - return Center( - child: Text( - errorMessage, - style: ArDriveTypography.body.buttonXLargeRegular( - color: - ArDriveTheme.of(context).themeData.colors.themeErrorDefault, - ), - ), - ); - }, - ); + _videoPlayerController.initialize(); + _videoPlayerController.addListener(_listener); + _videoPlayer = + VideoPlayer(_videoPlayerController, key: const Key('videoPlayer')); } @override void dispose() { logger.d('Disposing video player'); - _chewieController.videoPlayerController.dispose(); - _chewieController.dispose(); + _videoPlayerController.removeListener(_listener); + _videoPlayerController.dispose(); + super.dispose(); + } + + void _listener() { + setState(() { + if (_videoPlayerController.value.hasError) { + logger.d('>>> ${_videoPlayerController.value.errorDescription}'); + + // FIXME: This is a hack to deal with Chrome having problems on pressing + // play after pause rapidly. Also happens when a video reaches its end + // and a user plays it again right away. + // The error message is: + // "The play() request was interrupted by a call to pause(). https://goo.gl/LdLk22" + // A better fix is required but putting this in for now. + _videoPlayerController.removeListener(_listener); + _videoPlayerController.dispose(); + + _videoPlayerController = + VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); + _videoPlayerController.initialize(); + _videoPlayerController.addListener(_listener); + + _videoPlayer = + VideoPlayer(_videoPlayerController, key: const Key('videoPlayer')); + } + }); + } + void goFullScreen() { + final fsController = + VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); + fsController.initialize().then((_) { + fsController.seekTo(_videoPlayerController.value.position); + fsController.play(); + }); + + Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (context) => Scaffold( + body: Center( + child: TapRegion( + onTapInside: (v) { + _videoPlayerController.seekTo(fsController.value.position); + Navigator.of(context).pop(); + }, + child: AspectRatio( + aspectRatio: _videoPlayerController.value.aspectRatio, + child: VideoPlayer(fsController))), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + var colors = ArDriveTheme.of(context).themeData.colors; + var videoValue = _videoPlayerController.value; + var currentTime = getTimeString(videoValue.position); + var duration = getTimeString(videoValue.duration); + + return VisibilityDetector( + key: const Key('video-player'), + onVisibilityChanged: (VisibilityInfo info) { + if (mounted) { + setState( + () { + // if (_videoPlayerController.value.isInitialized) { + // _isPlaying = info.visibleFraction > 0.5; + // _isPlaying + // ? _videoPlayerController.play() + // : _videoPlayerController.pause(); + // } + }, + ); + } + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + child: Column(children: [ + Expanded( + child: AspectRatio( + aspectRatio: _videoPlayerController.value.aspectRatio, + child: TapRegion( + onTapInside: (v) { + goFullScreen(); + }, + child: _videoPlayer))), + const SizedBox(height: 8), + Column(children: [ + Text(widget.filename, + style: ArDriveTypography.body + .smallBold700(color: colors.themeFgDefault)), + const SizedBox(height: 4), + const Text('metadata'), + const SizedBox(height: 8), + Slider( + value: min(videoValue.position.inMilliseconds.toDouble(), + videoValue.duration.inMilliseconds.toDouble()), + min: 0.0, + max: videoValue.duration.inMilliseconds.toDouble(), + onChangeStart: (v) { + setState(() { + if (_videoPlayerController.value.duration > + Duration.zero) { + _videoPlayerController.pause(); + } + }); + }, + onChanged: (v) { + setState(() { + if (_videoPlayerController.value.duration > + Duration.zero) { + _videoPlayerController + .seekTo(Duration(milliseconds: v.toInt())); + } + }); + }, + onChangeEnd: (v) { + setState(() { + if (_videoPlayerController.value.duration > + Duration.zero) { + // _videoPlayerController + // .seekTo(Duration(milliseconds: v.toInt())); + _videoPlayerController.play(); + } + }); + }), + const SizedBox(height: 4), + Row( + children: [ + Text(currentTime), + const Expanded(child: SizedBox.shrink()), + Text(duration) + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + setState(() { + if (!_videoPlayerController.value.isInitialized || + _videoPlayerController.value.isBuffering || + _videoPlayerController.value.duration <= + Duration.zero) { + return; + } + final videoValue = _videoPlayerController.value; + final newPosition = videoValue.position - + const Duration(seconds: 15); + _videoPlayerController.seekTo(newPosition); + }); + }, + icon: const Icon(Icons.fast_rewind_outlined, size: 24)), + MaterialButton( + onPressed: () { + setState(() { + final value = _videoPlayerController.value; + if (!value.isInitialized || + value.isBuffering || + value.duration <= Duration.zero) { + return; + } + if (_videoPlayerController.value.isPlaying) { + _videoPlayerController.pause(); + } else { + if (value.position >= value.duration) { + _videoPlayerController.seekTo(Duration.zero); + } + _videoPlayerController.play(); + } + }); + }, + color: colors.themeAccentBrand, + shape: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(8), + child: (_videoPlayerController.value.isPlaying) + ? const Icon(Icons.pause_outlined, size: 32) + : const Icon(Icons.play_arrow_outlined, + size: 32)), + ), + IconButton( + onPressed: () { + setState(() { + if (!_videoPlayerController.value.isInitialized || + _videoPlayerController.value.isBuffering || + _videoPlayerController.value.duration <= + Duration.zero) { + return; + } + final videoValue = _videoPlayerController.value; + final newPosition = videoValue.position + + const Duration(seconds: 15); + + _videoPlayerController.seekTo(newPosition); + }); + }, + icon: const Icon(Icons.fast_forward_outlined, size: 24)) + ], + ) + ]) + ]))); + } +} + +class AudioPlayerWidget extends StatefulWidget { + final String audioUrl; + final String filename; + + const AudioPlayerWidget( + {Key? key, required this.filename, required this.audioUrl}) + : super(key: key); + + @override + // ignore: library_private_types_in_public_api + _AudioPlayerWidgetState createState() => _AudioPlayerWidgetState(); +} + +enum LoadState { loading, loaded, failed } + +class _AudioPlayerWidgetState extends State + with AutomaticKeepAliveClientMixin { + late AudioPlayer player; + LoadState _loadState = LoadState.loading; + bool _isVolumeSliderVisible = false; + bool _wasPlaying = false; + final _menuController = MenuController(); + StreamSubscription? _positionListener; + StreamSubscription? _playStateListener; + + @override + void initState() { + logger.d('Initializing audio player: ${widget.audioUrl}'); + player = AudioPlayer(); + player.setUrl(widget.audioUrl).then((value) { + setState(() { + _loadState = LoadState.loaded; + _positionListener = player.positionStream.listen((event) { + setState(() {}); + }); + + _playStateListener = player.playerStateStream.listen((event) { + if (event.processingState == ProcessingState.completed) { + player.stop(); + } + setState(() {}); + }); + }); + }).catchError((e) { + logger.d('Error setting audio url: $e'); + setState(() { + _loadState = LoadState.failed; + }); + }); + + super.initState(); + } + + @override + void dispose() { + logger.d('Disposing audio player'); + player.stop(); + _playStateListener?.cancel(); + _positionListener?.cancel(); + player.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + super.build(context); + var colors = ArDriveTheme.of(context).themeData.colors; + + var currentTime = getTimeString(player.position); + var duration = + player.duration != null ? getTimeString(player.duration!) : '0:00'; + return VisibilityDetector( - key: const Key('video-player'), - onVisibilityChanged: (VisibilityInfo info) { - if (mounted) { - setState( - () { - if (_videoPlayerController.value.isInitialized) { - _isPlaying = info.visibleFraction > 0.5; - _isPlaying - ? _videoPlayerController.play() - : _videoPlayerController.pause(); - } + key: const Key('audio-player'), + onVisibilityChanged: (VisibilityInfo info) { + if (mounted) { + setState( + () { + if (player.playing && info.visibleFraction < 0.5) { + player.pause(); + } + }, + ); + } + }, + child: _loadState == LoadState.loading + ? const Center( + child: SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(), + ), + ) + : Column(children: [ + Expanded( + child: Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + Container(color: colors.themeBgSubtle), + Align( + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.contain, + child: _loadState == LoadState.failed + ? Column( + children: [ + const Icon(Icons.error_outline_outlined, + size: 20), + Text( + appLocalizationsOf(context) + .couldNotLoadFile, + style: ArDriveTypography.body + .smallBold700( + color: colors.themeFgMuted) + .copyWith(fontSize: 13)), + ], + ) + : ArDriveIcons.music( + size: 100, color: colors.themeFgMuted), + )), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 32), + child: Column(children: [ + Text(widget.filename, + style: ArDriveTypography.body + .smallBold700(color: colors.themeFgDefault)), + const SizedBox(height: 8), + SliderTheme( + data: SliderThemeData( + trackHeight: 4, + trackShape: + _NoAdditionalHeightRoundedRectSliderTrackShape(), + inactiveTrackColor: colors.themeBgSubtle, + disabledThumbColor: colors.themeAccentBrand, + disabledInactiveTrackColor: colors.themeBgSubtle, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + )), + child: Slider( + value: _loadState == LoadState.failed + ? 0 + : min( + player.position.inMilliseconds.toDouble(), + player.duration?.inMilliseconds + .toDouble() ?? + 0, + ), + min: 0.0, + max: player.duration?.inMilliseconds.toDouble() ?? + 0, + onChangeStart: _loadState == LoadState.failed + ? null + : (v) { + setState(() { + _wasPlaying = player.playing; + if (_wasPlaying) { + player.pause(); + } + }); + }, + onChanged: _loadState == LoadState.failed + ? null + : (v) { + setState(() { + player.seek( + Duration(milliseconds: v.toInt())); + }); + }, + onChangeEnd: _loadState == LoadState.failed + ? null + : (v) { + setState(() { + if (_wasPlaying) { + player.play(); + } + }); + })), + const SizedBox(height: 4), + Row( + children: [ + Text(currentTime), + const Expanded(child: SizedBox.shrink()), + Text(duration) + ], + ), + const SizedBox(height: 8), + MouseRegion( + onExit: (event) { + setState(() { + _isVolumeSliderVisible = false; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: ScreenTypeLayout.builder( + mobile: (context) => + const SizedBox.shrink(), + desktop: (context) => + VolumeSliderWidget( + volume: player.volume, + setVolume: (v) { + setState(() { + player.setVolume(v); + }); + }, + sliderVisible: _isVolumeSliderVisible, + setSliderVisible: (v) { + setState(() { + _isVolumeSliderVisible = v; + }); + }, + ), + ))), + MaterialButton( + onPressed: _loadState == LoadState.failed + ? null + : () { + setState(() { + if (player.playerState + .processingState == + ProcessingState.completed || + !player.playing) { + if (player.position == + player.duration) { + player.stop(); + player.seek(Duration.zero); + } + player.play(); + } else { + player.pause(); + } + }); + }, + color: colors.themeAccentBrand, + disabledColor: colors.themeAccentDisabled, + shape: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(8), + child: (player.playerState.processingState == + ProcessingState.completed || + !player.playing) + ? Icon( + Icons.play_arrow_outlined, + size: 32, + color: colors.themeFgOnAccent, + ) + : Icon( + Icons.pause_outlined, + size: 32, + color: colors.themeFgOnAccent, + ), + ), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: ScreenTypeLayout.builder( + desktop: (context) => MenuAnchor( + menuChildren: [ + ..._speedOptions.map((v) { + return ListTile( + tileColor: + colors.themeBgSurface, + onTap: () { + setState(() { + player.setSpeed(v); + _menuController + .close(); + }); + }, + title: Text( + v == 1.0 + ? appLocalizationsOf( + context) + .normal + : '$v', + style: ArDriveTypography + .body + .buttonNormalBold( + color: colors + .themeFgDefault), + ), + ); + }) + ], + controller: _menuController, + child: IconButton( + onPressed: () { + _menuController.open(); + }, + icon: const Icon( + Icons.settings_outlined, + size: 24)), + ), + mobile: (context) => IconButton( + onPressed: () { + _displaySpeedOptionsModal( + context, (v) { + setState(() { + player.setSpeed(v); + }); + }); + }, + icon: const Icon( + Icons.settings_outlined, + size: 24))))), + ], + )) + ])) + ])); + } + + @override + bool get wantKeepAlive => true; +} + +class VolumeSliderWidget extends StatefulWidget { + const VolumeSliderWidget({ + Key? key, + required this.volume, + required this.setVolume, + required this.sliderVisible, + required this.setSliderVisible, + }) : super(key: key); + + final double volume; + final Function(double) setVolume; + final bool sliderVisible; + final Function(bool) setSliderVisible; + + @override + State createState() => _VolumeSliderWidgetState(); +} + +class _VolumeSliderWidgetState extends State { + double _lastVolume = 1.0; + + @override + Widget build(BuildContext context) { + var colors = ArDriveTheme.of(context).themeData.colors; + + bool isMuted = widget.volume <= 0; + + return Row(children: [ + MouseRegion( + onEnter: (event) { + widget.setSliderVisible(true); + }, + child: IconButton( + onPressed: () { + setState(() { + if (isMuted) { + widget.setVolume(_lastVolume); + } else { + if (widget.volume > 0) { + _lastVolume = widget.volume; + widget.setVolume(0); + } + } + }); }, - ); - } - }, - child: Chewie( - controller: _chewieController, + icon: Icon( + isMuted ? Icons.volume_off_outlined : Icons.volume_up_outlined, + size: 24, + )), ), - ); + Expanded( + child: ClipRect( + child: AnimatedSlide( + offset: Offset(widget.sliderVisible ? 0 : -1, 0), + duration: const Duration(milliseconds: 100), + child: SliderTheme( + data: SliderThemeData( + trackHeight: 4, + trackShape: + _NoAdditionalHeightRoundedRectSliderTrackShape(), + inactiveTrackColor: colors.themeBgSubtle, + activeTrackColor: colors.themeFgMuted, + overlayShape: SliderComponentShape.noOverlay, + thumbColor: colors.themeFgMuted, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + )), + child: Slider( + value: widget.volume, + min: 0.0, + max: 1.0, + onChanged: (v) { + widget.setVolume(v); + }, + onChangeStart: (v) { + setState(() { + _lastVolume = v; + }); + }, + ), + )))) + ]); + } +} + +class _NoAdditionalHeightRoundedRectSliderTrackShape + extends RoundedRectSliderTrackShape { + @override + void paint( + PaintingContext context, + Offset offset, { + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required Animation enableAnimation, + required TextDirection textDirection, + required Offset thumbCenter, + Offset? secondaryOffset, + bool isDiscrete = false, + bool isEnabled = false, + double additionalActiveTrackHeight = 2, + }) { + super.paint(context, offset, + parentBox: parentBox, + sliderTheme: sliderTheme, + enableAnimation: enableAnimation, + textDirection: textDirection, + thumbCenter: thumbCenter, + secondaryOffset: secondaryOffset, + isDiscrete: isDiscrete, + isEnabled: isEnabled, + additionalActiveTrackHeight: 0); } } + +void _displaySpeedOptionsModal( + BuildContext context, + Function(double) setPlaybackSpeed, +) { + final colors = ArDriveTheme.of(context).themeData.colors; + final dropDownTheme = ArDriveTheme.of(context).themeData.dropdownTheme; + + showModalBottomSheet( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + context: context, + builder: (context) { + return ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: _speedOptions.length, + itemBuilder: (context, index) { + final speed = _speedOptions[index]; + return ListTile( + tileColor: dropDownTheme.backgroundColor, + hoverColor: dropDownTheme.hoverColor, + textColor: colors.themeFgDefault, + onTap: () { + setPlaybackSpeed(speed); + Navigator.of(context).pop(); + }, + title: Text( + speed == 1.0 ? appLocalizationsOf(context).normal : '$speed'), + ); + }, + ), + ); + }, + ); +} diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index f74c977cc7..6da5fa614d 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:math'; + import 'package:ardrive/app_shell.dart'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; @@ -31,11 +34,11 @@ import 'package:ardrive/utils/size_constants.dart'; import 'package:ardrive/utils/user_utils.dart'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; -import 'package:chewie/chewie.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:responsive_builder/responsive_builder.dart'; import 'package:timeago/timeago.dart'; import 'package:video_player/video_player.dart'; diff --git a/lib/services/config/app_config.dart b/lib/services/config/app_config.dart index ec9405b101..3b3725c5a0 100644 --- a/lib/services/config/app_config.dart +++ b/lib/services/config/app_config.dart @@ -13,6 +13,7 @@ class AppConfig { final bool enableQuickSyncAuthoring; final bool enableMultipleFileDownload; final bool enableVideoPreview; + final bool enableAudioPreview; final int autoSyncIntervalInSeconds; final bool enableSyncFromSnapshot; final bool enableSeedPhraseLogin; @@ -28,6 +29,7 @@ class AppConfig { this.enableQuickSyncAuthoring = false, this.enableMultipleFileDownload = false, this.enableVideoPreview = false, + this.enableAudioPreview = false, this.autoSyncIntervalInSeconds = 5 * 60, this.enableSyncFromSnapshot = true, this.enableSeedPhraseLogin = true, @@ -44,6 +46,7 @@ class AppConfig { bool? enableQuickSyncAuthoring, bool? enableMultipleFileDownload, bool? enableVideoPreview, + bool? enableAudioPreview, int? autoSyncIntervalInSeconds, bool? enableSyncFromSnapshot, bool? enableSeedPhraseLogin, @@ -65,6 +68,7 @@ class AppConfig { enableQuickSyncAuthoring: enableQuickSyncAuthoring ?? this.enableQuickSyncAuthoring, enableVideoPreview: enableVideoPreview ?? this.enableVideoPreview, + enableAudioPreview: enableAudioPreview ?? this.enableAudioPreview, autoSyncIntervalInSeconds: autoSyncIntervalInSeconds ?? this.autoSyncIntervalInSeconds, enableSyncFromSnapshot: diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 7903aabf11..2d709daebb 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -6,8 +6,17 @@ const List supportedImageTypesInFilePreview = [ 'image/bmp', ]; +const List audioContentTypes = [ + 'audio/aac', + 'audio/x-wav', + 'audio/ogg', + 'audio/x-flac', + 'audio/mpeg', +]; + const List videoContentTypes = [ 'video/mp4', + 'video/quicktime', ]; const profileQueryMaxRetries = 6; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 95febef503..471da09221 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import audio_session import connectivity_plus import desktop_drop import device_info_plus @@ -13,6 +14,7 @@ import file_selector_macos import firebase_core import firebase_crashlytics import flutter_secure_storage_macos +import just_audio import package_info_plus import path_provider_foundation import share_plus @@ -20,9 +22,9 @@ import shared_preferences_foundation import sqflite import sqlite3_flutter_libs import url_launcher_macos -import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) @@ -31,6 +33,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) @@ -38,5 +41,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index f04de53e91..c9157fd185 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,6 +117,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + audio_session: + dependency: transitive + description: + name: audio_session + sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad" + url: "https://pub.dev" + source: hosted + version: "0.1.16" auto_size_text: dependency: transitive description: @@ -285,14 +293,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - chewie: - dependency: "direct main" - description: - name: chewie - sha256: "745e81e84c6d7f3835f89f85bb49771c0a66099e4caf8f8e9e9a372bc66fb2c1" - url: "https://pub.dev" - source: hosted - version: "1.5.0" cli_util: dependency: transitive description: @@ -413,14 +413,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.2" - cupertino_icons: - dependency: transitive - description: - name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be - url: "https://pub.dev" - source: hosted - version: "1.0.5" dart_style: dependency: transitive description: @@ -1191,6 +1183,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: "890cd0fc41a1a4530c171e375a2a3fb6a09d84e9d508c5195f40bcff54330327" + url: "https://pub.dev" + source: hosted + version: "0.9.34" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df + url: "https://pub.dev" + source: hosted + version: "4.2.1" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13 + url: "https://pub.dev" + source: hosted + version: "0.4.8" jwk: dependency: transitive description: @@ -2066,10 +2082,10 @@ packages: dependency: "direct main" description: name: video_player - sha256: "3fd106c74da32f336dc7feb65021da9b0207cb3124392935f1552834f7cce822" + sha256: d3910a8cefc0de8a432a4411dcf85030e885d8fef3ddea291f162253a05dbf01 url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1" video_player_android: dependency: transitive description: @@ -2118,46 +2134,6 @@ packages: url: "https://pub.dev" source: hosted version: "11.3.0" - wakelock: - dependency: transitive - description: - name: wakelock - sha256: "769ecf42eb2d07128407b50cb93d7c10bd2ee48f0276ef0119db1d25cc2f87db" - url: "https://pub.dev" - source: hosted - version: "0.6.2" - wakelock_macos: - dependency: transitive - description: - name: wakelock_macos - sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd" - url: "https://pub.dev" - source: hosted - version: "0.4.0" - wakelock_platform_interface: - dependency: transitive - description: - name: wakelock_platform_interface - sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621" - url: "https://pub.dev" - source: hosted - version: "0.3.0" - wakelock_web: - dependency: transitive - description: - name: wakelock_web - sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5" - url: "https://pub.dev" - source: hosted - version: "0.4.0" - wakelock_windows: - dependency: transitive - description: - name: wakelock_windows - sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567" - url: "https://pub.dev" - source: hosted - version: "0.2.1" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3782aded93..1c09bc37a7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -96,8 +96,7 @@ dependencies: animations: ^2.0.7 connectivity_plus: ^4.0.0 archive: ^3.3.0 - video_player: ^2.6.0 - chewie: ^1.4.0 + video_player: ^2.7.1 lottie: ^2.3.0 visibility_detector: ^0.4.0+2 bip39: ^1.0.6 @@ -119,6 +118,7 @@ dependencies: tuple: ^2.0.2 share_plus: ^6.3.4 flutter_email_sender: ^6.0.1 + just_audio: ^0.9.34 dependency_overrides: ardrive_io: