diff --git a/lib/main.dart b/lib/main.dart index 23a0540..48e423c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'package:rhythm_box/providers/scrobbler.dart'; import 'package:rhythm_box/providers/skip_segments.dart'; import 'package:rhythm_box/providers/spotify.dart'; import 'package:rhythm_box/providers/user_preferences.dart'; +import 'package:rhythm_box/providers/volume.dart'; import 'package:rhythm_box/router.dart'; import 'package:rhythm_box/services/kv_store/encrypted_kv_store.dart'; import 'package:rhythm_box/services/kv_store/kv_store.dart'; @@ -103,6 +104,7 @@ class MyApp extends StatelessWidget { Get.put(QueryingTrackInfoProvider()); Get.put(SourcedTrackProvider()); Get.put(EndlessPlaybackProvider()); + Get.put(VolumeProvider()); Get.put(ServerPlaybackRoutesProvider()); Get.put(PlaybackServerProvider()); diff --git a/lib/providers/volume.dart b/lib/providers/volume.dart new file mode 100644 index 0000000..74c2c68 --- /dev/null +++ b/lib/providers/volume.dart @@ -0,0 +1,20 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; +import 'package:rhythm_box/services/kv_store/kv_store.dart'; + +class VolumeProvider extends GetxController { + RxDouble volume = KVStoreService.volume.obs; + + @override + void onInit() { + super.onInit(); + audioPlayer.setVolume(volume.value); + } + + Future setVolume(double newVolume) async { + volume.value = newVolume; + await audioPlayer.setVolume(newVolume); + KVStoreService.setVolume(newVolume); + } +} diff --git a/lib/services/server/active_sourced_track.dart b/lib/services/server/active_sourced_track.dart index 2595c2e..8add6f7 100755 --- a/lib/services/server/active_sourced_track.dart +++ b/lib/services/server/active_sourced_track.dart @@ -29,7 +29,7 @@ class ActiveSourcedTrackProvider extends GetxController { final oldActiveIndex = audioPlayer.currentIndex; await playback.addTracksAtFirst([newTrack]); - await Future.delayed(const Duration(milliseconds: 50)); + await Future.delayed(const Duration(milliseconds: 300)); await playback.jumpToTrack(newTrack); await audioPlayer.removeTrack(oldActiveIndex); diff --git a/lib/widgets/player/bottom_player.dart b/lib/widgets/player/bottom_player.dart index 907d247..4a99f01 100644 --- a/lib/widgets/player/bottom_player.dart +++ b/lib/widgets/player/bottom_player.dart @@ -11,6 +11,7 @@ import 'package:rhythm_box/services/audio_services/image.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart'; import 'package:rhythm_box/widgets/player/track_details.dart'; import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; +import 'package:rhythm_box/widgets/volume_slider.dart'; class BottomPlayer extends StatefulWidget { final bool usePop; @@ -93,6 +94,45 @@ class _BottomPlayerState extends State @override Widget build(BuildContext context) { + final controls = Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MediaQuery.of(context).size.width >= 720 + ? MainAxisAlignment.center + : MainAxisAlignment.end, + children: [ + if (MediaQuery.of(context).size.width >= 720) + IconButton( + icon: const Icon(Icons.skip_previous), + onPressed: + _isFetchingActiveTrack ? null : audioPlayer.skipToPrevious, + ) + else + IconButton( + icon: const Icon(Icons.skip_next), + onPressed: _isFetchingActiveTrack ? null : audioPlayer.skipToNext, + ), + IconButton.filled( + icon: _isFetchingActiveTrack + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 3, + ), + ) + : Icon( + !_isPlaying ? Icons.play_arrow : Icons.pause, + ), + onPressed: _isFetchingActiveTrack ? null : _togglePlayState, + ), + if (MediaQuery.of(context).size.width >= 720) + IconButton( + icon: const Icon(Icons.skip_next), + onPressed: _isFetchingActiveTrack ? null : audioPlayer.skipToNext, + ) + ], + ); + return SizeTransition( sizeFactor: _animation, axis: Axis.vertical, @@ -118,56 +158,60 @@ class _BottomPlayerState extends State Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Hero( - tag: const Key('current-active-track-album-art'), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: _albumArt != null - ? AutoCacheImage(_albumArt!, width: 64, height: 64) - : Container( - color: Theme.of(context) - .colorScheme - .surfaceContainerHigh, - width: 64, - height: 64, - child: const Center(child: Icon(Icons.image)), - ), - ), - ), - const Gap(12), Expanded( - child: PlayerTrackDetails( - track: _playback.state.value.activeTrack, + child: Row( + children: [ + Hero( + tag: const Key('current-active-track-album-art'), + child: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: _albumArt != null + ? AutoCacheImage( + _albumArt!, + width: 64, + height: 64, + ) + : Container( + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh, + width: 64, + height: 64, + child: const Center( + child: Icon(Icons.image), + ), + ), + ), + ), + const Gap(12), + Expanded( + child: PlayerTrackDetails( + track: _playback.state.value.activeTrack, + ), + ), + ], ), ), const Gap(12), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - icon: const Icon(Icons.skip_next), - onPressed: _isFetchingActiveTrack - ? null - : audioPlayer.skipToNext, + if (MediaQuery.of(context).size.width >= 720) + Expanded(child: controls) + else + controls, + if (MediaQuery.of(context).size.width >= 720) const Gap(12), + if (MediaQuery.of(context).size.width >= 720) + const Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded( + child: VolumeSlider( + mainAxisAlignment: MainAxisAlignment.end, + ), + ) + ], ), - IconButton.filled( - icon: _isFetchingActiveTrack - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 3, - ), - ) - : Icon( - !_isPlaying ? Icons.play_arrow : Icons.pause, - ), - onPressed: - _isFetchingActiveTrack ? null : _togglePlayState, - ), - ], - ), + ), const Gap(12), ], ).paddingSymmetric(horizontal: 12, vertical: 8), diff --git a/lib/widgets/volume_slider.dart b/lib/widgets/volume_slider.dart new file mode 100644 index 0000000..77269b6 --- /dev/null +++ b/lib/widgets/volume_slider.dart @@ -0,0 +1,94 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:rhythm_box/providers/volume.dart'; + +class VolumeSlider extends StatelessWidget { + final bool isFullWidth; + final MainAxisAlignment mainAxisAlignment; + + const VolumeSlider({ + super.key, + this.isFullWidth = false, + this.mainAxisAlignment = MainAxisAlignment.start, + }); + + @override + Widget build(BuildContext context) { + return Obx(() { + final VolumeProvider vol = Get.find(); + + final slider = Listener( + onPointerSignal: (event) async { + if (event is PointerScrollEvent) { + if (event.scrollDelta.dy > 0) { + final newValue = vol.volume.value - .2; + vol.setVolume(newValue < 0 ? 0 : newValue); + } else { + final newValue = vol.volume.value + .2; + vol.setVolume(newValue > 1 ? 1 : newValue); + } + } + }, + child: SliderTheme( + data: SliderThemeData( + showValueIndicator: ShowValueIndicator.always, + trackShape: _VolumeSliderShape(), + trackHeight: 3, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6, + ), + overlayShape: SliderComponentShape.noOverlay, + ), + child: Slider( + min: 0, + max: 1, + label: (vol.volume.value * 100).toStringAsFixed(0), + value: vol.volume.value, + onChanged: vol.setVolume, + ), + ), + ).paddingOnly(right: 24, left: 8); + return Row( + mainAxisAlignment: mainAxisAlignment, + children: [ + IconButton( + icon: Icon( + vol.volume.value == 0 + ? Icons.volume_off + : vol.volume.value <= 0.5 + ? Icons.volume_down + : Icons.volume_up, + size: 18, + ), + onPressed: () { + if (vol.volume.value == 0) { + vol.setVolume(1); + } else { + vol.setVolume(0); + } + }, + ), + if (isFullWidth) Expanded(child: slider) else slider, + ], + ); + }); + } +} + +class _VolumeSliderShape extends RoundedRectSliderTrackShape { + @override + Rect getPreferredRect({ + required RenderBox parentBox, + Offset offset = Offset.zero, + required SliderThemeData sliderTheme, + bool isEnabled = false, + bool isDiscrete = false, + }) { + final trackHeight = sliderTheme.trackHeight; + final trackLeft = offset.dx; + final trackTop = offset.dy + (parentBox.size.height - trackHeight!) / 2; + final trackWidth = parentBox.size.width; + return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); + } +}