✨ 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/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());
|
||||
|
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;
|
||||
|
||||
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);
|
||||
|
@ -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<BottomPlayer>
|
||||
|
||||
@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<BottomPlayer>
|
||||
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),
|
||||
|
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