From 7a58b6fa1fa60343b75595dc364c987008016b8c Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Thu, 31 Aug 2023 14:44:12 -0400 Subject: [PATCH 01/19] initial work on new video player preview --- assets/config/dev.json | 2 +- .../fs_entry_preview_cubit.dart | 3 +- .../fs_entry_preview_state.dart | 5 +- .../components/fs_entry_preview_widget.dart | 212 ++++++++++++++---- lib/pages/drive_detail/drive_detail_page.dart | 1 - lib/utils/constants.dart | 1 + pubspec.lock | 60 +---- pubspec.yaml | 3 +- 8 files changed, 179 insertions(+), 108 deletions(-) diff --git a/assets/config/dev.json b/assets/config/dev.json index 42e03e393b..5ed1c08677 100644 --- a/assets/config/dev.json +++ b/assets/config/dev.json @@ -7,7 +7,7 @@ "allowedDataItemSizeForTurbo": 500000, "enableQuickSyncAuthoring": true, "enableMultipleFileDownload": true, - "enableVideoPreview": false, + "enableVideoPreview": true, "stripePublishableKey": "pk_test_51JUAtwC8apPOWkDLh2FPZkQkiKZEkTo6wqgLCtQoClL6S4l2jlbbc5MgOdwOUdU9Tn93NNvqAGbu115lkJChMikG00XUfTmo2z", "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 48d74a763a..d6a05267f3 100644 --- a/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart +++ b/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart @@ -197,7 +197,8 @@ class FsEntryPreviewCubit extends Cubit { return; } - emit(FsEntryPreviewVideo(previewUrl: previewUrl)); + emit(FsEntryPreviewVideo( + filename: selectedItem.name, previewUrl: previewUrl)); return; } 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..145af8a1f6 100644 --- a/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart +++ b/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart @@ -46,12 +46,15 @@ class FsEntryPreviewAudio extends FsEntryPreviewSuccess { } 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/pages/drive_detail/components/fs_entry_preview_widget.dart b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart index 1e6219d74b..2b98cb7e91 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -37,6 +37,7 @@ class _FsEntryPreviewWidgetState extends State { default: return VideoPlayerWidget( + filename: (widget.state as FsEntryPreviewVideo).filename, videoUrl: (widget.state as FsEntryPreviewVideo).previewUrl, ); } @@ -45,8 +46,11 @@ class _FsEntryPreviewWidgetState extends State { 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 +59,10 @@ class VideoPlayerWidget extends StatefulWidget { class _VideoPlayerWidgetState extends State { late VideoPlayerController _videoPlayerController; - late ChewieController _chewieController; - bool _isPlaying = false; + + late Null Function() listener; + // late ChewieController _chewieController; + // bool _isPlaying = false; @override void initState() { @@ -64,57 +70,175 @@ 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(); + + listener = () { + // print('buffer data:'); + // for (final bufferRange in _videoPlayerController.value.buffered) { + // print(bufferRange); + // } + setState(() {}); + }; + + _videoPlayerController.addListener(listener); + // _chewieController = ChewieController( + // videoPlayerController: _videoPlayerController, + // autoPlay: false, + // looping: true, + // showControls: true, + // allowFullScreen: true, + // aspectRatio: 1, + // errorBuilder: (context, errorMessage) { + // return Center( + // child: Text( + // errorMessage, + // style: ArDriveTypography.body.buttonXLargeRegular( + // color: + // ArDriveTheme.of(context).themeData.colors.themeErrorDefault, + // ), + // ), + // ); + // }, + // ); } @override void dispose() { logger.d('Disposing video player'); - _chewieController.videoPlayerController.dispose(); - _chewieController.dispose(); - + // _chewieController.videoPlayerController.dispose(); + // _chewieController.dispose(); + _videoPlayerController.removeListener(listener); + _videoPlayerController.dispose(); super.dispose(); } + 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; + } + @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: Chewie( - controller: _chewieController, - ), - ); + 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: VideoPlayer(_videoPlayerController))), + 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: videoValue.position.inSeconds.toDouble(), + min: 0.0, + max: videoValue.duration.inSeconds.toDouble(), + onChangeStart: (v) async { + await _videoPlayerController.pause(); + }, + onChanged: (v) async { + await _videoPlayerController + .seekTo(Duration(seconds: v.toInt())); + }, + onChangeEnd: (v) async { + await _videoPlayerController + .seekTo(Duration(seconds: v.toInt())); + await _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: () { + final videoValue = _videoPlayerController.value; + final newPosition = + videoValue.position - const Duration(seconds: 15); + _videoPlayerController.seekTo(newPosition); + }, + icon: const Icon(Icons.fast_rewind_outlined, size: 24)), + IconButton.filled( + onPressed: () async { + if (_videoPlayerController.value.isPlaying) { + await _videoPlayerController.pause(); + } else { + await _videoPlayerController.play(); + } + }, + icon: _videoPlayerController.value.isPlaying + ? const Icon(Icons.pause_outlined, size: 24) + : const Icon(Icons.play_arrow_outlined, size: 24), + ), + IconButton( + onPressed: () { + final videoValue = _videoPlayerController.value; + final newPosition = + videoValue.position + const Duration(seconds: 15); + + _videoPlayerController.seekTo(newPosition); + }, + icon: const Icon(Icons.fast_forward_outlined, size: 24)) + ], + ) + ]) + ])) + + // Chewie( + // controller: _chewieController, + // ), + ); } } diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 878123842b..c5ca499ea5 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -31,7 +31,6 @@ 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'; diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 7903aabf11..1fd8409444 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -8,6 +8,7 @@ const List supportedImageTypesInFilePreview = [ const List videoContentTypes = [ 'video/mp4', + 'video/quicktime', ]; const profileQueryMaxRetries = 6; diff --git a/pubspec.lock b/pubspec.lock index 89f6a6827b..4c6467b414 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -285,14 +285,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 +405,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: @@ -2066,10 +2050,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 +2102,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 0e53f0c43b..6ca70ca8bf 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 From b47e7d464fad85bc46d47ce134134a02f76912ef Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Fri, 1 Sep 2023 17:35:21 -0400 Subject: [PATCH 02/19] continuing work to deal with Chrome pause interrupting play call issue --- .../components/fs_entry_preview_widget.dart | 163 ++++++++++-------- lib/pages/drive_detail/drive_detail_page.dart | 2 + 2 files changed, 97 insertions(+), 68 deletions(-) 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 2b98cb7e91..00e9bcb4dd 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -60,10 +60,6 @@ class VideoPlayerWidget extends StatefulWidget { class _VideoPlayerWidgetState extends State { late VideoPlayerController _videoPlayerController; - late Null Function() listener; - // late ChewieController _chewieController; - // bool _isPlaying = false; - @override void initState() { logger.d('Initializing video player: ${widget.videoUrl}'); @@ -71,47 +67,39 @@ class _VideoPlayerWidgetState extends State { _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); _videoPlayerController.initialize(); - - listener = () { - // print('buffer data:'); - // for (final bufferRange in _videoPlayerController.value.buffered) { - // print(bufferRange); - // } - setState(() {}); - }; - - _videoPlayerController.addListener(listener); - // _chewieController = ChewieController( - // videoPlayerController: _videoPlayerController, - // autoPlay: false, - // looping: true, - // showControls: true, - // allowFullScreen: true, - // aspectRatio: 1, - // errorBuilder: (context, errorMessage) { - // return Center( - // child: Text( - // errorMessage, - // style: ArDriveTypography.body.buttonXLargeRegular( - // color: - // ArDriveTheme.of(context).themeData.colors.themeErrorDefault, - // ), - // ), - // ); - // }, - // ); + _videoPlayerController.addListener(_listener); } @override void dispose() { logger.d('Disposing video player'); - // _chewieController.videoPlayerController.dispose(); - // _chewieController.dispose(); - _videoPlayerController.removeListener(listener); + _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); + } + }); + } + String getTimeString(Duration duration) { int durSeconds = duration.inSeconds; const hour = 60 * 60; @@ -164,7 +152,8 @@ class _VideoPlayerWidgetState extends State { Expanded( child: AspectRatio( aspectRatio: _videoPlayerController.value.aspectRatio, - child: VideoPlayer(_videoPlayerController))), + child: VideoPlayer(_videoPlayerController, + key: const Key('video-player-view')))), const SizedBox(height: 8), Column(children: [ Text(widget.filename, @@ -174,20 +163,36 @@ class _VideoPlayerWidgetState extends State { const Text('metadata'), const SizedBox(height: 8), Slider( - value: videoValue.position.inSeconds.toDouble(), + value: min(videoValue.position.inMilliseconds.toDouble(), + videoValue.duration.inMilliseconds.toDouble()), min: 0.0, - max: videoValue.duration.inSeconds.toDouble(), - onChangeStart: (v) async { - await _videoPlayerController.pause(); + max: videoValue.duration.inMilliseconds.toDouble(), + onChangeStart: (v) { + setState(() { + if (_videoPlayerController.value.duration > + Duration.zero) { + _videoPlayerController.pause(); + } + }); }, - onChanged: (v) async { - await _videoPlayerController - .seekTo(Duration(seconds: v.toInt())); + onChanged: (v) { + setState(() { + if (_videoPlayerController.value.duration > + Duration.zero) { + _videoPlayerController + .seekTo(Duration(milliseconds: v.toInt())); + } + }); }, - onChangeEnd: (v) async { - await _videoPlayerController - .seekTo(Duration(seconds: v.toInt())); - await _videoPlayerController.play(); + onChangeEnd: (v) { + setState(() { + if (_videoPlayerController.value.duration > + Duration.zero) { + // _videoPlayerController + // .seekTo(Duration(milliseconds: v.toInt())); + _videoPlayerController.play(); + } + }); }), const SizedBox(height: 4), Row( @@ -204,19 +209,38 @@ class _VideoPlayerWidgetState extends State { children: [ IconButton( onPressed: () { - final videoValue = _videoPlayerController.value; - final newPosition = - videoValue.position - const Duration(seconds: 15); - _videoPlayerController.seekTo(newPosition); + 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)), IconButton.filled( - onPressed: () async { - if (_videoPlayerController.value.isPlaying) { - await _videoPlayerController.pause(); - } else { - await _videoPlayerController.play(); - } + 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(); + } + }); }, icon: _videoPlayerController.value.isPlaying ? const Icon(Icons.pause_outlined, size: 24) @@ -224,21 +248,24 @@ class _VideoPlayerWidgetState extends State { ), IconButton( onPressed: () { - final videoValue = _videoPlayerController.value; - final newPosition = - videoValue.position + const Duration(seconds: 15); + 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); + _videoPlayerController.seekTo(newPosition); + }); }, icon: const Icon(Icons.fast_forward_outlined, size: 24)) ], ) ]) - ])) - - // Chewie( - // controller: _chewieController, - // ), - ); + ]))); } } diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index c5ca499ea5..3e707587b5 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:ardrive/app_shell.dart'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; From f508786b888fade7978e6db297763e36e822a596 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Fri, 1 Sep 2023 18:18:18 -0400 Subject: [PATCH 03/19] experimenting with full screen implementation --- .../components/fs_entry_preview_widget.dart | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) 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 00e9bcb4dd..724781983e 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -59,6 +59,7 @@ class VideoPlayerWidget extends StatefulWidget { class _VideoPlayerWidgetState extends State { late VideoPlayerController _videoPlayerController; + late VideoPlayer _videoPlayer; @override void initState() { @@ -68,6 +69,8 @@ class _VideoPlayerWidgetState extends State { VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); _videoPlayerController.initialize(); _videoPlayerController.addListener(_listener); + _videoPlayer = + VideoPlayer(_videoPlayerController, key: const Key('videoPlayer')); } @override @@ -96,6 +99,9 @@ class _VideoPlayerWidgetState extends State { VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); _videoPlayerController.initialize(); _videoPlayerController.addListener(_listener); + + _videoPlayer = + VideoPlayer(_videoPlayerController, key: const Key('videoPlayer')); } }); } @@ -123,6 +129,31 @@ class _VideoPlayerWidgetState extends State { return timeString; } + 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: VideoPlayer(fsController)), + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { var colors = ArDriveTheme.of(context).themeData.colors; @@ -152,8 +183,11 @@ class _VideoPlayerWidgetState extends State { Expanded( child: AspectRatio( aspectRatio: _videoPlayerController.value.aspectRatio, - child: VideoPlayer(_videoPlayerController, - key: const Key('video-player-view')))), + child: TapRegion( + onTapInside: (v) { + goFullScreen(); + }, + child: _videoPlayer))), const SizedBox(height: 8), Column(children: [ Text(widget.filename, From 03cf42d603a8121f71cc62e7b16331c8c87d8eff Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Tue, 5 Sep 2023 14:10:16 -0400 Subject: [PATCH 04/19] added AspectRatio to fix view on fullscreen player --- .../drive_detail/components/fs_entry_preview_widget.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 724781983e..d25f66f172 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -147,7 +147,9 @@ class _VideoPlayerWidgetState extends State { _videoPlayerController.seekTo(fsController.value.position); Navigator.of(context).pop(); }, - child: VideoPlayer(fsController)), + child: AspectRatio( + aspectRatio: _videoPlayerController.value.aspectRatio, + child: VideoPlayer(fsController))), ), ), ), From 183d0bb1b021c1deebfdee415a5f733b24e17ada Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Tue, 5 Sep 2023 17:16:38 -0400 Subject: [PATCH 05/19] implement audio playing using just_audio package --- .../fs_entry_preview_cubit.dart | 37 +++ .../fs_entry_preview_state.dart | 9 +- .../components/fs_entry_preview_widget.dart | 228 ++++++++++++++++++ lib/pages/drive_detail/drive_detail_page.dart | 1 + lib/utils/constants.dart | 8 + pubspec.lock | 32 +++ pubspec.yaml | 1 + 7 files changed, 312 insertions(+), 4 deletions(-) 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 d6a05267f3..69578f8431 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, @@ -171,6 +180,14 @@ class FsEntryPreviewCubit extends Cubit { case 'image': emitImagePreview(file, previewUrl); break; + + case 'audio': + _previewAudio( + drive.isPrivate, + selectedItem as FileDataTableItem, + previewUrl, + ); + break; case 'video': _previewVideo( drive.isPrivate, @@ -189,6 +206,23 @@ class FsEntryPreviewCubit extends Cubit { } } + void _previewAudio( + bool isPrivate, FileDataTableItem selectedItem, previewUrl) { + if (_configService.config.enableVideoPreview) { + 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) { @@ -286,6 +320,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 145af8a1f6..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,12 +37,13 @@ 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 { 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 d25f66f172..7a63148aa4 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -35,6 +35,12 @@ 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, @@ -305,3 +311,225 @@ class _VideoPlayerWidgetState extends State { ]))); } } + +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 { + late AudioPlayer player; + LoadState _loadState = LoadState.loading; + + @override + void initState() { + logger.d('Initializing audio player: ${widget.audioUrl}'); + player = AudioPlayer(); + player.setUrl(widget.audioUrl).then((value) { + setState(() { + _loadState = LoadState.loaded; + player.positionStream.listen((event) { + setState(() {}); + }); + + player.playerStateStream.listen((event) { + // logger.d('Player state: $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.dispose(); + super.dispose(); + } + + 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; + } + + @override + Widget build(BuildContext context) { + var colors = ArDriveTheme.of(context).themeData.colors; + + player.duration; + + var currentTime = getTimeString(player.position); + var duration = + player.duration != null ? getTimeString(player.duration!) : '0:00'; + + return VisibilityDetector( + key: const Key('audio-player'), + onVisibilityChanged: (VisibilityInfo info) { + if (mounted) { + setState( + () { + // if (_videoPlayerController.value.isInitialized) { + // _isPlaying = info.visibleFraction > 0.5; + // _isPlaying + // ? _videoPlayerController.play() + // : _videoPlayerController.pause(); + // } + }, + ); + } + }, + child: _loadState == LoadState.loading + ? const Center( + child: SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(), + ), + ) + : _loadState == LoadState.failed + ? const Center( + child: Text('Failed to load audio'), + ) + : Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + child: Column(children: [ + Expanded(child: Container(color: Colors.black)), + 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( + player.position.inMilliseconds.toDouble(), + player.duration?.inMilliseconds.toDouble() ?? + 0), + min: 0.0, + max: + player.duration?.inMilliseconds.toDouble() ?? 0, + onChangeStart: (v) { + setState(() { + player.pause(); + }); + }, + onChanged: (v) { + setState(() { + player.seek(Duration(milliseconds: v.toInt())); + }); + }, + onChangeEnd: (v) { + setState(() { + player.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 (player.duration != null) { + final newPosition = player.position - + const Duration(seconds: 15); + player.seek(newPosition >= Duration.zero + ? newPosition + : Duration.zero); + } + }); + }, + icon: const Icon(Icons.fast_rewind_outlined, + size: 24)), + IconButton.filled( + onPressed: () { + 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(); + } + }); + }, + icon: (player.playerState.processingState == + ProcessingState.completed || + !player.playing) + ? const Icon(Icons.play_arrow_outlined, + size: 24) + : const Icon(Icons.pause_outlined, size: 24), + ), + IconButton( + onPressed: () { + setState(() { + if (player.duration != null) { + final newPosition = player.position + + const Duration(seconds: 15); + player.seek(newPosition > player.duration! + ? player.duration! + : newPosition); + } + }); + }, + icon: const Icon(Icons.fast_forward_outlined, + size: 24)) + ], + ) + ]) + ]))); + } +} diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 3e707587b5..301d2acf48 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -37,6 +37,7 @@ 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/utils/constants.dart b/lib/utils/constants.dart index 1fd8409444..2d709daebb 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -6,6 +6,14 @@ 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', diff --git a/pubspec.lock b/pubspec.lock index 4c6467b414..ce74ac85b7 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: @@ -1175,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: diff --git a/pubspec.yaml b/pubspec.yaml index 6ca70ca8bf..6cb71facff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -118,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: From 8db9d5da87020867ad645b3cd4bcad289dd49fab Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Mon, 11 Sep 2023 12:42:39 -0400 Subject: [PATCH 06/19] working on audio player UI --- .../components/fs_entry_preview_widget.dart | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) 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 7a63148aa4..23edfda56c 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -437,8 +437,6 @@ class _AudioPlayerWidgetState extends State { Text(widget.filename, style: ArDriveTypography.body .smallBold700(color: colors.themeFgDefault)), - const SizedBox(height: 4), - const Text('metadata'), const SizedBox(height: 8), Slider( value: min( @@ -476,20 +474,29 @@ class _AudioPlayerWidgetState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - IconButton( - onPressed: () { - setState(() { - if (player.duration != null) { - final newPosition = player.position - - const Duration(seconds: 15); - player.seek(newPosition >= Duration.zero - ? newPosition - : Duration.zero); - } - }); - }, - icon: const Icon(Icons.fast_rewind_outlined, - size: 24)), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Row(children: [ + IconButton( + onPressed: () { + // setState(() { + // }); + }, + icon: const Icon( + Icons.volume_up_outlined, + size: 24)), + Expanded( + child: Slider( + value: player.volume, + min: 0.0, + max: 1.0, + onChanged: (v) { + setState(() { + player.setVolume(v); + }); + })) + ]))), IconButton.filled( onPressed: () { setState(() { @@ -513,20 +520,17 @@ class _AudioPlayerWidgetState extends State { size: 24) : const Icon(Icons.pause_outlined, size: 24), ), - IconButton( - onPressed: () { - setState(() { - if (player.duration != null) { - final newPosition = player.position + - const Duration(seconds: 15); - player.seek(newPosition > player.duration! - ? player.duration! - : newPosition); - } - }); - }, - icon: const Icon(Icons.fast_forward_outlined, - size: 24)) + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: IconButton( + onPressed: () { + // setState(() { + // }); + }, + icon: const Icon( + Icons.settings_outlined, + size: 24)))), ], ) ]) From 760cc8f623ccff53a35db319fa2db0766f1be765 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Mon, 11 Sep 2023 13:12:41 -0400 Subject: [PATCH 07/19] added enableAudioPreview feature flag --- assets/config/dev.json | 1 + assets/config/prod.json | 1 + assets/config/staging.json | 1 + .../fs_entry_preview/fs_entry_preview_cubit.dart | 2 +- lib/dev_tools/app_dev_tools.dart | 14 ++++++++++++++ lib/services/config/app_config.dart | 4 ++++ 6 files changed, 22 insertions(+), 1 deletion(-) diff --git a/assets/config/dev.json b/assets/config/dev.json index 5ed1c08677..0c787c8d69 100644 --- a/assets/config/dev.json +++ b/assets/config/dev.json @@ -8,6 +8,7 @@ "enableQuickSyncAuthoring": true, "enableMultipleFileDownload": true, "enableVideoPreview": true, + "enableAudioPreview": true, "stripePublishableKey": "pk_test_51JUAtwC8apPOWkDLh2FPZkQkiKZEkTo6wqgLCtQoClL6S4l2jlbbc5MgOdwOUdU9Tn93NNvqAGbu115lkJChMikG00XUfTmo2z", "enablePins": true } diff --git a/assets/config/prod.json b/assets/config/prod.json index b55cff45c2..e88edbcb17 100644 --- a/assets/config/prod.json +++ b/assets/config/prod.json @@ -8,6 +8,7 @@ "enableQuickSyncAuthoring": true, "enableMultipleFileDownload": true, "enableVideoPreview": false, + "enableAudioPreview": false, "stripePublishableKey": "pk_live_51JUAtwC8apPOWkDLMQqNF9sPpfneNSPnwX8YZ8y1FNDl6v94hZIwzgFSYl27bWE4Oos8CLquunUswKrKcaDhDO6m002Yj9AeKj", "enablePins": true } diff --git a/assets/config/staging.json b/assets/config/staging.json index b55cff45c2..e88edbcb17 100644 --- a/assets/config/staging.json +++ b/assets/config/staging.json @@ -8,6 +8,7 @@ "enableQuickSyncAuthoring": true, "enableMultipleFileDownload": true, "enableVideoPreview": false, + "enableAudioPreview": false, "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 69578f8431..c56c274eb9 100644 --- a/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart +++ b/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart @@ -208,7 +208,7 @@ class FsEntryPreviewCubit extends Cubit { void _previewAudio( bool isPrivate, FileDataTableItem selectedItem, previewUrl) { - if (_configService.config.enableVideoPreview) { + if (_configService.config.enableAudioPreview) { if (isPrivate) { emit(FsEntryPreviewUnavailable()); return; diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index 4a6c7e7d62..cc0817108e 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 enablePinsOption = ArDriveDevToolOption( name: 'enablePins', value: settings.enablePins, @@ -336,6 +349,7 @@ class AppConfigWindowManagerState extends State { enableQuickSyncAuthoringOption, enableMultipleFileDownloadOption, enableVideoPreviewOption, + enableAudioPreviewOption, enableSeedPhreaseLogin, enablePinsOption, allowedDataItemSizeForTurboOption, diff --git a/lib/services/config/app_config.dart b/lib/services/config/app_config.dart index 4d442c9437..03572acd34 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; @@ -29,6 +30,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, @@ -46,6 +48,7 @@ class AppConfig { bool? enableQuickSyncAuthoring, bool? enableMultipleFileDownload, bool? enableVideoPreview, + bool? enableAudioPreview, int? autoSyncIntervalInSeconds, bool? enableSyncFromSnapshot, bool? enableSeedPhraseLogin, @@ -68,6 +71,7 @@ class AppConfig { enableQuickSyncAuthoring: enableQuickSyncAuthoring ?? this.enableQuickSyncAuthoring, enableVideoPreview: enableVideoPreview ?? this.enableVideoPreview, + enableAudioPreview: enableAudioPreview ?? this.enableAudioPreview, autoSyncIntervalInSeconds: autoSyncIntervalInSeconds ?? this.autoSyncIntervalInSeconds, enableSyncFromSnapshot: From 862a3c3451595a9f591ed6c5451fc08a81444488 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Mon, 11 Sep 2023 14:44:54 -0400 Subject: [PATCH 08/19] adjust play button design per Figma --- .../components/fs_entry_preview_widget.dart | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) 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 23edfda56c..c7518e5228 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -265,7 +265,7 @@ class _VideoPlayerWidgetState extends State { }); }, icon: const Icon(Icons.fast_rewind_outlined, size: 24)), - IconButton.filled( + MaterialButton( onPressed: () { setState(() { final value = _videoPlayerController.value; @@ -284,9 +284,14 @@ class _VideoPlayerWidgetState extends State { } }); }, - icon: _videoPlayerController.value.isPlaying - ? const Icon(Icons.pause_outlined, size: 24) - : const Icon(Icons.play_arrow_outlined, size: 24), + 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: () { @@ -497,7 +502,7 @@ class _AudioPlayerWidgetState extends State { }); })) ]))), - IconButton.filled( + MaterialButton( onPressed: () { setState(() { if (player.playerState.processingState == @@ -513,12 +518,17 @@ class _AudioPlayerWidgetState extends State { } }); }, - icon: (player.playerState.processingState == - ProcessingState.completed || - !player.playing) - ? const Icon(Icons.play_arrow_outlined, - size: 24) - : const Icon(Icons.pause_outlined, size: 24), + color: colors.themeAccentBrand, + shape: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(8), + child: (player.playerState.processingState == + ProcessingState.completed || + !player.playing) + ? const Icon(Icons.play_arrow_outlined, + size: 32) + : const Icon(Icons.pause_outlined, + size: 32)), ), Expanded( child: Align( From a932f396cd4084c33ebfdf5cae3b66956fe30275 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Mon, 11 Sep 2023 18:12:28 -0400 Subject: [PATCH 09/19] adjust slider to more closely match Figma --- .../components/fs_entry_preview_widget.dart | 217 +++++++++++------- 1 file changed, 131 insertions(+), 86 deletions(-) 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 c7518e5228..eff8a995e4 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -50,6 +50,29 @@ class _FsEntryPreviewWidgetState extends State { } } +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; @@ -112,29 +135,6 @@ class _VideoPlayerWidgetState extends State { }); } - 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; - } - void goFullScreen() { final fsController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); @@ -368,33 +368,11 @@ class _AudioPlayerWidgetState extends State { @override void dispose() { logger.d('Disposing audio player'); + player.stop(); player.dispose(); super.dispose(); } - 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; - } - @override Widget build(BuildContext context) { var colors = ArDriveTheme.of(context).themeData.colors; @@ -443,29 +421,42 @@ class _AudioPlayerWidgetState extends State { style: ArDriveTypography.body .smallBold700(color: colors.themeFgDefault)), const SizedBox(height: 8), - Slider( - value: min( - player.position.inMilliseconds.toDouble(), - player.duration?.inMilliseconds.toDouble() ?? - 0), - min: 0.0, - max: - player.duration?.inMilliseconds.toDouble() ?? 0, - onChangeStart: (v) { - setState(() { - player.pause(); - }); - }, - onChanged: (v) { - setState(() { - player.seek(Duration(milliseconds: v.toInt())); - }); - }, - onChangeEnd: (v) { - setState(() { - player.play(); - }); - }), + SliderTheme( + data: SliderThemeData( + trackHeight: 4, + trackShape: + _NoAdditionalHeightRoundedRectSliderTrackShape(), + inactiveTrackColor: colors.themeBgSubtle, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + )), + child: Slider( + value: min( + player.position.inMilliseconds.toDouble(), + player.duration?.inMilliseconds.toDouble() ?? + 0, + ), + min: 0.0, + max: player.duration?.inMilliseconds + .toDouble() ?? + 0, + onChangeStart: (v) { + setState(() { + player.pause(); + }); + }, + onChanged: (v) { + setState(() { + player.seek( + Duration(milliseconds: v.toInt())); + }); + }, + onChangeEnd: (v) { + setState(() { + player.play(); + }); + })), const SizedBox(height: 4), Row( children: [ @@ -492,15 +483,33 @@ class _AudioPlayerWidgetState extends State { Icons.volume_up_outlined, size: 24)), Expanded( - child: Slider( - value: player.volume, - min: 0.0, - max: 1.0, - onChanged: (v) { - setState(() { - player.setVolume(v); - }); - })) + child: SliderTheme( + data: SliderThemeData( + trackHeight: 4, + trackShape: + _NoAdditionalHeightRoundedRectSliderTrackShape(), + inactiveTrackColor: + colors.themeBgSubtle, + activeTrackColor: + colors.themeFgDefault, + overlayShape: + SliderComponentShape + .noOverlay, + thumbColor: + colors.themeFgDefault, + thumbShape: + const RoundSliderThumbShape( + enabledThumbRadius: 8, + )), + child: Slider( + value: player.volume, + min: 0.0, + max: 1.0, + onChanged: (v) { + setState(() { + player.setVolume(v); + }); + }))) ]))), MaterialButton( onPressed: () { @@ -521,14 +530,21 @@ class _AudioPlayerWidgetState extends State { color: colors.themeAccentBrand, shape: const CircleBorder(), child: Padding( - padding: const EdgeInsets.all(8), - child: (player.playerState.processingState == - ProcessingState.completed || - !player.playing) - ? const Icon(Icons.play_arrow_outlined, - size: 32) - : const Icon(Icons.pause_outlined, - size: 32)), + 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( @@ -547,3 +563,32 @@ class _AudioPlayerWidgetState extends State { ]))); } } + +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); + } +} From 00c576d8cf3d32444f0c48236497a2b8620d5cd1 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Tue, 12 Sep 2023 09:31:39 -0400 Subject: [PATCH 10/19] implement sliding volume control slider and mute functionality --- .../components/fs_entry_preview_widget.dart | 254 +++++++++++------- 1 file changed, 162 insertions(+), 92 deletions(-) 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 eff8a995e4..eeaa4b29e2 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -335,6 +335,7 @@ enum LoadState { loading, loaded, failed } class _AudioPlayerWidgetState extends State { late AudioPlayer player; LoadState _loadState = LoadState.loading; + bool _isVolumeSliderVisible = false; @override void initState() { @@ -466,104 +467,173 @@ class _AudioPlayerWidgetState extends State { ], ), const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Row(children: [ - IconButton( - onPressed: () { - // setState(() { - // }); + MouseRegion( + onExit: (event) { + setState(() { + _isVolumeSliderVisible = false; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: VolumeSliderWidget( + volume: player.volume, + setVolume: (v) { + setState(() { + player.setVolume(v); + }); }, - icon: const Icon( - Icons.volume_up_outlined, - size: 24)), - Expanded( - child: SliderTheme( - data: SliderThemeData( - trackHeight: 4, - trackShape: - _NoAdditionalHeightRoundedRectSliderTrackShape(), - inactiveTrackColor: - colors.themeBgSubtle, - activeTrackColor: - colors.themeFgDefault, - overlayShape: - SliderComponentShape - .noOverlay, - thumbColor: - colors.themeFgDefault, - thumbShape: - const RoundSliderThumbShape( - enabledThumbRadius: 8, - )), - child: Slider( - value: player.volume, - min: 0.0, - max: 1.0, - onChanged: (v) { - setState(() { - player.setVolume(v); - }); - }))) - ]))), - MaterialButton( - onPressed: () { - 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, - 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: IconButton( - onPressed: () { - // setState(() { - // }); - }, - icon: const Icon( - Icons.settings_outlined, - size: 24)))), - ], - ) + sliderVisible: _isVolumeSliderVisible, + setSliderVisible: (v) { + setState(() { + _isVolumeSliderVisible = v; + }); + }, + ))), + MaterialButton( + onPressed: () { + 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, + 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: IconButton( + onPressed: () { + // setState(() { + // }); + }, + icon: const Icon( + Icons.settings_outlined, + size: 24)))), + ], + )) ]) ]))); } } +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); + } + } + }); + }, + 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.themeFgDefault, + overlayShape: SliderComponentShape.noOverlay, + thumbColor: colors.themeFgDefault, + 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 From 48e67c3e67a92577c4819a5d3ab97638d5fe6f01 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Tue, 12 Sep 2023 15:03:53 -0400 Subject: [PATCH 11/19] only resume playing on seek if previously was playing when slider initially changed --- .../components/fs_entry_preview_widget.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 eeaa4b29e2..de09001f28 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -336,6 +336,7 @@ class _AudioPlayerWidgetState extends State { late AudioPlayer player; LoadState _loadState = LoadState.loading; bool _isVolumeSliderVisible = false; + bool _wasPlaying = false; @override void initState() { @@ -378,8 +379,6 @@ class _AudioPlayerWidgetState extends State { Widget build(BuildContext context) { var colors = ArDriveTheme.of(context).themeData.colors; - player.duration; - var currentTime = getTimeString(player.position); var duration = player.duration != null ? getTimeString(player.duration!) : '0:00'; @@ -444,7 +443,10 @@ class _AudioPlayerWidgetState extends State { 0, onChangeStart: (v) { setState(() { - player.pause(); + _wasPlaying = player.playing; + if (_wasPlaying) { + player.pause(); + } }); }, onChanged: (v) { @@ -455,7 +457,9 @@ class _AudioPlayerWidgetState extends State { }, onChangeEnd: (v) { setState(() { - player.play(); + if (_wasPlaying) { + player.play(); + } }); })), const SizedBox(height: 4), @@ -610,9 +614,9 @@ class _VolumeSliderWidgetState extends State { trackShape: _NoAdditionalHeightRoundedRectSliderTrackShape(), inactiveTrackColor: colors.themeBgSubtle, - activeTrackColor: colors.themeFgDefault, + activeTrackColor: colors.themeFgMuted, overlayShape: SliderComponentShape.noOverlay, - thumbColor: colors.themeFgDefault, + thumbColor: colors.themeFgMuted, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 8, )), From b1764a2f52bcb8931b566f58b1658950fcdd9541 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Tue, 12 Sep 2023 17:52:23 -0400 Subject: [PATCH 12/19] implemented music note visual for top preview area of audio player --- .../components/fs_entry_preview_widget.dart | 294 +++++++++--------- 1 file changed, 155 insertions(+), 139 deletions(-) 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 de09001f28..9f54debbf9 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -407,149 +407,165 @@ class _AudioPlayerWidgetState extends State { child: CircularProgressIndicator(), ), ) - : _loadState == LoadState.failed - ? const Center( - child: Text('Failed to load audio'), - ) - : Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), - child: Column(children: [ - Expanded(child: Container(color: Colors.black)), - const SizedBox(height: 8), - 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, - overlayShape: SliderComponentShape.noOverlay, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 8, - )), - child: Slider( - value: min( - player.position.inMilliseconds.toDouble(), - player.duration?.inMilliseconds.toDouble() ?? - 0, - ), - min: 0.0, - max: player.duration?.inMilliseconds - .toDouble() ?? - 0, - onChangeStart: (v) { - setState(() { - _wasPlaying = player.playing; - if (_wasPlaying) { - player.pause(); - } - }); - }, - onChanged: (v) { - setState(() { - player.seek( - Duration(milliseconds: v.toInt())); - }); - }, - onChangeEnd: (v) { - setState(() { - if (_wasPlaying) { - player.play(); - } - }); - })), - const SizedBox(height: 4), - Row( - children: [ - Text(currentTime), - const Expanded(child: SizedBox.shrink()), - Text(duration) - ], + // : _loadState == LoadState.failed + // ? const Center( + // child: Text('Failed to load audio'), + // ) + : Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + child: Column(children: [ + Expanded( + child: Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + Container(color: Colors.black), + Align( + alignment: Alignment.center, + child: ArDriveIcons.music( + size: 100, color: colors.themeFgMuted), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text('No Preview Available', + style: ArDriveTypography.body.smallBold700( + color: colors.themeBgSubtle))), ), - const SizedBox(height: 8), - MouseRegion( - onExit: (event) { + ], + ), + ), + const SizedBox(height: 8), + 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, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + )), + child: Slider( + value: min( + player.position.inMilliseconds.toDouble(), + player.duration?.inMilliseconds.toDouble() ?? 0, + ), + min: 0.0, + max: + player.duration?.inMilliseconds.toDouble() ?? 0, + onChangeStart: (v) { + setState(() { + _wasPlaying = player.playing; + if (_wasPlaying) { + player.pause(); + } + }); + }, + onChanged: (v) { setState(() { - _isVolumeSliderVisible = false; + player.seek(Duration(milliseconds: v.toInt())); }); }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: VolumeSliderWidget( - volume: player.volume, - setVolume: (v) { - setState(() { - player.setVolume(v); - }); - }, - sliderVisible: _isVolumeSliderVisible, - setSliderVisible: (v) { - setState(() { - _isVolumeSliderVisible = v; - }); - }, - ))), - MaterialButton( - onPressed: () { - 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, - 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: IconButton( - onPressed: () { - // setState(() { - // }); - }, - icon: const Icon( - Icons.settings_outlined, - size: 24)))), - ], - )) - ]) - ]))); + onChangeEnd: (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: VolumeSliderWidget( + volume: player.volume, + setVolume: (v) { + setState(() { + player.setVolume(v); + }); + }, + sliderVisible: _isVolumeSliderVisible, + setSliderVisible: (v) { + setState(() { + _isVolumeSliderVisible = v; + }); + }, + ))), + MaterialButton( + onPressed: () { + 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, + 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: IconButton( + onPressed: () { + // setState(() { + // }); + }, + icon: const Icon( + Icons.settings_outlined, + size: 24)))), + ], + )) + ]) + ]))); } } From 278c1534bf4e4169bc3f1254a70611c9e4b4d5ff Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Thu, 14 Sep 2023 14:38:53 -0400 Subject: [PATCH 13/19] implemented speed popup menu for desktop and mobile --- .../components/fs_entry_preview_widget.dart | 369 +++++++++++------- 1 file changed, 224 insertions(+), 145 deletions(-) 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 9f54debbf9..3381709ab3 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, @@ -337,6 +339,7 @@ class _AudioPlayerWidgetState extends State { LoadState _loadState = LoadState.loading; bool _isVolumeSliderVisible = false; bool _wasPlaying = false; + MenuController _menuController = MenuController(); @override void initState() { @@ -411,161 +414,194 @@ class _AudioPlayerWidgetState extends State { // ? const Center( // child: Text('Failed to load audio'), // ) - : Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), - child: Column(children: [ - Expanded( - child: Stack( - fit: StackFit.expand, - alignment: Alignment.center, - children: [ - Container(color: Colors.black), - Align( + // : + : Column(children: [ + Expanded( + child: Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + Container(color: colors.themeBgSubtle), + Align( alignment: Alignment.center, - child: ArDriveIcons.music( - size: 100, color: colors.themeFgMuted), - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text('No Preview Available', - style: ArDriveTypography.body.smallBold700( - color: colors.themeBgSubtle))), - ), - ], - ), + child: FittedBox( + fit: BoxFit.contain, + child: ArDriveIcons.music( + size: 100, color: colors.themeFgMuted), + )), + ], ), - const SizedBox(height: 8), - 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, - overlayShape: SliderComponentShape.noOverlay, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 8, - )), - child: Slider( - value: min( - player.position.inMilliseconds.toDouble(), - player.duration?.inMilliseconds.toDouble() ?? 0, - ), - min: 0.0, - max: + ), + 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, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + )), + child: Slider( + value: min( + player.position.inMilliseconds.toDouble(), player.duration?.inMilliseconds.toDouble() ?? 0, - onChangeStart: (v) { - setState(() { - _wasPlaying = player.playing; - if (_wasPlaying) { - player.pause(); - } - }); - }, - onChanged: (v) { - setState(() { - player.seek(Duration(milliseconds: v.toInt())); - }); - }, - onChangeEnd: (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: VolumeSliderWidget( - volume: player.volume, - setVolume: (v) { - setState(() { - player.setVolume(v); - }); - }, - sliderVisible: _isVolumeSliderVisible, - setSliderVisible: (v) { - setState(() { - _isVolumeSliderVisible = v; - }); - }, - ))), - MaterialButton( - onPressed: () { + ), + min: 0.0, + max: player.duration?.inMilliseconds.toDouble() ?? + 0, + onChangeStart: (v) { setState(() { - if (player.playerState.processingState == - ProcessingState.completed || - !player.playing) { - if (player.position == player.duration) { - player.stop(); - player.seek(Duration.zero); - } - player.play(); - } else { + _wasPlaying = player.playing; + if (_wasPlaying) { player.pause(); } }); }, - color: colors.themeAccentBrand, - shape: const CircleBorder(), - child: Padding( - padding: const EdgeInsets.all(8), - child: (player.playerState.processingState == + onChanged: (v) { + setState(() { + player + .seek(Duration(milliseconds: v.toInt())); + }); + }, + onChangeEnd: (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: VolumeSliderWidget( + volume: player.volume, + setVolume: (v) { + setState(() { + player.setVolume(v); + }); + }, + sliderVisible: _isVolumeSliderVisible, + setSliderVisible: (v) { + setState(() { + _isVolumeSliderVisible = v; + }); + }, + ))), + MaterialButton( + onPressed: () { + setState(() { + if (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, - ), + !player.playing) { + if (player.position == player.duration) { + player.stop(); + player.seek(Duration.zero); + } + player.play(); + } else { + player.pause(); + } + }); + }, + color: colors.themeAccentBrand, + 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: IconButton( - onPressed: () { - // setState(() { - // }); - }, - icon: const Icon( - Icons.settings_outlined, - size: 24)))), - ], - )) - ]) - ]))); + 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', + 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))))), + ], + )) + ])) + ])); } } @@ -682,3 +718,46 @@ class _NoAdditionalHeightRoundedRectSliderTrackShape 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'), + ); + }, + ), + ); + }, + ); +} From dd30870ce573af7922d579db24bf5f3f9a2d26d7 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Thu, 14 Sep 2023 18:03:29 -0400 Subject: [PATCH 14/19] implemented file loading error path, also preserving state when tab switches --- .../components/fs_entry_preview_widget.dart | 171 +++++++++++------- lib/pages/drive_detail/drive_detail_page.dart | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 6 +- 3 files changed, 111 insertions(+), 67 deletions(-) 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 3381709ab3..8886c3997c 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -334,12 +334,15 @@ class AudioPlayerWidget extends StatefulWidget { enum LoadState { loading, loaded, failed } -class _AudioPlayerWidgetState extends State { +class _AudioPlayerWidgetState extends State + with AutomaticKeepAliveClientMixin { late AudioPlayer player; LoadState _loadState = LoadState.loading; bool _isVolumeSliderVisible = false; bool _wasPlaying = false; - MenuController _menuController = MenuController(); + final _menuController = MenuController(); + StreamSubscription? _positionListener; + StreamSubscription? _playStateListener; @override void initState() { @@ -348,11 +351,11 @@ class _AudioPlayerWidgetState extends State { player.setUrl(widget.audioUrl).then((value) { setState(() { _loadState = LoadState.loaded; - player.positionStream.listen((event) { + _positionListener = player.positionStream.listen((event) { setState(() {}); }); - player.playerStateStream.listen((event) { + _playStateListener = player.playerStateStream.listen((event) { // logger.d('Player state: $event'); if (event.processingState == ProcessingState.completed) { player.stop(); @@ -374,12 +377,15 @@ class _AudioPlayerWidgetState extends State { 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); @@ -392,12 +398,9 @@ class _AudioPlayerWidgetState extends State { if (mounted) { setState( () { - // if (_videoPlayerController.value.isInitialized) { - // _isPlaying = info.visibleFraction > 0.5; - // _isPlaying - // ? _videoPlayerController.play() - // : _videoPlayerController.pause(); - // } + if (player.playing && info.visibleFraction < 0.5) { + player.pause(); + } }, ); } @@ -426,8 +429,21 @@ class _AudioPlayerWidgetState extends State { alignment: Alignment.center, child: FittedBox( fit: BoxFit.contain, - child: ArDriveIcons.music( - size: 100, color: colors.themeFgMuted), + child: _loadState == LoadState.failed + ? Column( + children: [ + const Icon(Icons.error_outline_outlined, + size: 20), + // FIXME: localization + Text('Could not load file', + style: ArDriveTypography.body + .smallBold700( + color: colors.themeFgMuted) + .copyWith(fontSize: 13)), + ], + ) + : ArDriveIcons.music( + size: 100, color: colors.themeFgMuted), )), ], ), @@ -445,39 +461,51 @@ class _AudioPlayerWidgetState extends State { trackShape: _NoAdditionalHeightRoundedRectSliderTrackShape(), inactiveTrackColor: colors.themeBgSubtle, + disabledThumbColor: colors.themeAccentBrand, + disabledInactiveTrackColor: colors.themeBgSubtle, overlayShape: SliderComponentShape.noOverlay, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 8, )), child: Slider( - value: min( - player.position.inMilliseconds.toDouble(), - player.duration?.inMilliseconds.toDouble() ?? 0, - ), + 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: (v) { - setState(() { - _wasPlaying = player.playing; - if (_wasPlaying) { - player.pause(); - } - }); - }, - onChanged: (v) { - setState(() { - player - .seek(Duration(milliseconds: v.toInt())); - }); - }, - onChangeEnd: (v) { - setState(() { - if (_wasPlaying) { - player.play(); - } - }); - })), + 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: [ @@ -500,37 +528,47 @@ class _AudioPlayerWidgetState extends State { Expanded( child: Align( alignment: Alignment.centerLeft, - child: VolumeSliderWidget( - volume: player.volume, - setVolume: (v) { - setState(() { - player.setVolume(v); - }); - }, - sliderVisible: _isVolumeSliderVisible, - setSliderVisible: (v) { - setState(() { - _isVolumeSliderVisible = v; - }); - }, + 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: () { - 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(); - } - }); - }, + 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), @@ -603,6 +641,9 @@ class _AudioPlayerWidgetState extends State { ])) ])); } + + @override + bool get wantKeepAlive => true; } class VolumeSliderWidget extends StatefulWidget { diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 85312296a2..f3f202b92d 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:ardrive/app_shell.dart'; 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")) } From 397932e30fdf22581ad7a8df55b052d50d5739a6 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Thu, 14 Sep 2023 18:11:44 -0400 Subject: [PATCH 15/19] localization for couldNotLoadFile --- lib/l10n/app_en.arb | 8 ++++++-- .../drive_detail/components/fs_entry_preview_widget.dart | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0619f9786b..40dcdef9d8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -310,6 +310,10 @@ "@costUpload": { "description": "Cost" }, + "couldNotLoadFile": "Could not load file", + "@couldNotLoadFile": { + "description": "Message shown when user can not load audio or media files for playback" + }, "country": "Country", "@country": {}, "create": "Create", @@ -1216,7 +1220,7 @@ "@movingItemsEmphasized": { "description": "Moving Items dialog title" }, - "multiDownloadCompleteWithSkippedFiles": "Download Complete with {numSkippedFiles} skipped file(s)", + "multiDownloadCompleteWithSkippedFiles": "Download complete with {numSkippedFiles} skipped file(s)", "@multiDownloadCompleteWithSkippedFiles": { "description": "Title to modal shown when user completes multi-file download and has skipped files. " }, @@ -2062,4 +2066,4 @@ "@zippingYourFiles": { "description": "Download failure message when a file is too big" } -} +} \ No newline at end of file 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 8886c3997c..bd2aceeb34 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -434,8 +434,9 @@ class _AudioPlayerWidgetState extends State children: [ const Icon(Icons.error_outline_outlined, size: 20), - // FIXME: localization - Text('Could not load file', + Text( + appLocalizationsOf(context) + .couldNotLoadFile, style: ArDriveTypography.body .smallBold700( color: colors.themeFgMuted) From 8d581a2072f4109418bf54a58bd7bcc9361aae0c Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Thu, 14 Sep 2023 18:24:55 -0400 Subject: [PATCH 16/19] make work with public pins on private drives --- lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 0bd6a724c0..589390de68 100644 --- a/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart +++ b/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart @@ -187,8 +187,10 @@ class FsEntryPreviewCubit extends Cubit { case 'audio': _previewAudio( - drive.isPrivate, - selectedItem as FileDataTableItem, + (selectedItem as FileDataTableItem).pinnedDataOwnerAddress == + null && + drive.isPrivate, + selectedItem, previewUrl, ); break; From ef37dc4c1a6c531119048a04d2f316b6dec7d9f2 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Mon, 18 Sep 2023 10:54:22 -0400 Subject: [PATCH 17/19] removed dead commented code --- .../drive_detail/components/fs_entry_preview_widget.dart | 6 ------ 1 file changed, 6 deletions(-) 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 bd2aceeb34..174ec3b450 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -356,7 +356,6 @@ class _AudioPlayerWidgetState extends State }); _playStateListener = player.playerStateStream.listen((event) { - // logger.d('Player state: $event'); if (event.processingState == ProcessingState.completed) { player.stop(); } @@ -413,11 +412,6 @@ class _AudioPlayerWidgetState extends State child: CircularProgressIndicator(), ), ) - // : _loadState == LoadState.failed - // ? const Center( - // child: Text('Failed to load audio'), - // ) - // : : Column(children: [ Expanded( child: Stack( From a46b1354b872848268da399b7859587e82561239 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Mon, 25 Sep 2023 09:38:18 -0400 Subject: [PATCH 18/19] rename 1.0 speed option to 'Normal' --- lib/l10n/app_en.arb | 4 ++++ .../drive_detail/components/fs_entry_preview_widget.dart | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) 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 174ec3b450..b6f65d00b3 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -600,7 +600,11 @@ class _AudioPlayerWidgetState extends State }); }, title: Text( - '$v', + v == 1.0 + ? appLocalizationsOf( + context) + .normal + : '$v', style: ArDriveTypography .body .buttonNormalBold( @@ -789,7 +793,8 @@ void _displaySpeedOptionsModal( setPlaybackSpeed(speed); Navigator.of(context).pop(); }, - title: Text('$speed'), + title: Text( + speed == 1.0 ? appLocalizationsOf(context).normal : '$speed'), ); }, ), From d90e7df0b0291963418628add0794d1b9e1fcd01 Mon Sep 17 00:00:00 2001 From: Steven Yi Date: Wed, 27 Sep 2023 09:28:16 -0400 Subject: [PATCH 19/19] enable audio preview feature flag for staging and prod --- assets/config/prod.json | 2 +- assets/config/staging.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/config/prod.json b/assets/config/prod.json index e88edbcb17..5d3e2ce3cd 100644 --- a/assets/config/prod.json +++ b/assets/config/prod.json @@ -8,7 +8,7 @@ "enableQuickSyncAuthoring": true, "enableMultipleFileDownload": true, "enableVideoPreview": false, - "enableAudioPreview": false, + "enableAudioPreview": true, "stripePublishableKey": "pk_live_51JUAtwC8apPOWkDLMQqNF9sPpfneNSPnwX8YZ8y1FNDl6v94hZIwzgFSYl27bWE4Oos8CLquunUswKrKcaDhDO6m002Yj9AeKj", "enablePins": true } diff --git a/assets/config/staging.json b/assets/config/staging.json index e88edbcb17..5d3e2ce3cd 100644 --- a/assets/config/staging.json +++ b/assets/config/staging.json @@ -8,7 +8,7 @@ "enableQuickSyncAuthoring": true, "enableMultipleFileDownload": true, "enableVideoPreview": false, - "enableAudioPreview": false, + "enableAudioPreview": true, "stripePublishableKey": "pk_live_51JUAtwC8apPOWkDLMQqNF9sPpfneNSPnwX8YZ8y1FNDl6v94hZIwzgFSYl27bWE4Oos8CLquunUswKrKcaDhDO6m002Yj9AeKj", "enablePins": true }