✨ Volume slider
This commit is contained in:
parent
989440013c
commit
bb09c43135
@ -14,6 +14,7 @@ import 'package:rhythm_box/providers/scrobbler.dart';
|
|||||||
import 'package:rhythm_box/providers/skip_segments.dart';
|
import 'package:rhythm_box/providers/skip_segments.dart';
|
||||||
import 'package:rhythm_box/providers/spotify.dart';
|
import 'package:rhythm_box/providers/spotify.dart';
|
||||||
import 'package:rhythm_box/providers/user_preferences.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/router.dart';
|
||||||
import 'package:rhythm_box/services/kv_store/encrypted_kv_store.dart';
|
import 'package:rhythm_box/services/kv_store/encrypted_kv_store.dart';
|
||||||
import 'package:rhythm_box/services/kv_store/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(QueryingTrackInfoProvider());
|
||||||
Get.put(SourcedTrackProvider());
|
Get.put(SourcedTrackProvider());
|
||||||
Get.put(EndlessPlaybackProvider());
|
Get.put(EndlessPlaybackProvider());
|
||||||
|
Get.put(VolumeProvider());
|
||||||
|
|
||||||
Get.put(ServerPlaybackRoutesProvider());
|
Get.put(ServerPlaybackRoutesProvider());
|
||||||
Get.put(PlaybackServerProvider());
|
Get.put(PlaybackServerProvider());
|
||||||
|
20
lib/providers/volume.dart
Normal file
20
lib/providers/volume.dart
Normal file
@ -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<void> setVolume(double newVolume) async {
|
||||||
|
volume.value = newVolume;
|
||||||
|
await audioPlayer.setVolume(newVolume);
|
||||||
|
KVStoreService.setVolume(newVolume);
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ class ActiveSourcedTrackProvider extends GetxController {
|
|||||||
final oldActiveIndex = audioPlayer.currentIndex;
|
final oldActiveIndex = audioPlayer.currentIndex;
|
||||||
|
|
||||||
await playback.addTracksAtFirst([newTrack]);
|
await playback.addTracksAtFirst([newTrack]);
|
||||||
await Future.delayed(const Duration(milliseconds: 50));
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
await playback.jumpToTrack(newTrack);
|
await playback.jumpToTrack(newTrack);
|
||||||
|
|
||||||
await audioPlayer.removeTrack(oldActiveIndex);
|
await audioPlayer.removeTrack(oldActiveIndex);
|
||||||
|
@ -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/auto_cache_image.dart';
|
||||||
import 'package:rhythm_box/widgets/player/track_details.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/tracks/querying_track_info.dart';
|
||||||
|
import 'package:rhythm_box/widgets/volume_slider.dart';
|
||||||
|
|
||||||
class BottomPlayer extends StatefulWidget {
|
class BottomPlayer extends StatefulWidget {
|
||||||
final bool usePop;
|
final bool usePop;
|
||||||
@ -93,6 +94,45 @@ class _BottomPlayerState extends State<BottomPlayer>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return SizeTransition(
|
||||||
sizeFactor: _animation,
|
sizeFactor: _animation,
|
||||||
axis: Axis.vertical,
|
axis: Axis.vertical,
|
||||||
@ -118,56 +158,60 @@ class _BottomPlayerState extends State<BottomPlayer>
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
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(
|
Expanded(
|
||||||
child: PlayerTrackDetails(
|
child: Row(
|
||||||
track: _playback.state.value.activeTrack,
|
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),
|
const Gap(12),
|
||||||
Row(
|
if (MediaQuery.of(context).size.width >= 720)
|
||||||
mainAxisSize: MainAxisSize.min,
|
Expanded(child: controls)
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
else
|
||||||
children: [
|
controls,
|
||||||
IconButton(
|
if (MediaQuery.of(context).size.width >= 720) const Gap(12),
|
||||||
icon: const Icon(Icons.skip_next),
|
if (MediaQuery.of(context).size.width >= 720)
|
||||||
onPressed: _isFetchingActiveTrack
|
const Expanded(
|
||||||
? null
|
child: Row(
|
||||||
: audioPlayer.skipToNext,
|
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),
|
const Gap(12),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
).paddingSymmetric(horizontal: 12, vertical: 8),
|
||||||
|
94
lib/widgets/volume_slider.dart
Normal file
94
lib/widgets/volume_slider.dart
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user