feat: add volume control support

This commit is contained in:
Kingkor Roy Tirtho 2024-03-26 13:36:35 +06:00
parent 7e887d54ed
commit d693624b6d
8 changed files with 104 additions and 23 deletions

View File

@ -26,6 +26,7 @@ import 'package:spotube/models/local_track.dart';
import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -378,11 +379,21 @@ class PlayerView extends HookConsumerWidget {
enabledThumbRadius: 8, enabledThumbRadius: 8,
), ),
), ),
child: const Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16), padding:
child: VolumeSlider( const EdgeInsets.symmetric(horizontal: 16),
fullWidth: true, child: Consumer(builder: (context, ref, _) {
), final volume = ref.watch(volumeProvider);
return VolumeSlider(
fullWidth: true,
value: volume,
onChanged: (value) {
ref
.read(volumeProvider.notifier)
.setVolume(value);
},
);
}),
), ),
), ),
], ],

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
@ -298,6 +299,7 @@ class PlayerQueue extends HookConsumerWidget {
), ),
), ),
), ),
const Gap(100),
], ],
), ),
), ),

View File

@ -3,37 +3,39 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/provider/volume_provider.dart';
class VolumeSlider extends HookConsumerWidget { class VolumeSlider extends HookConsumerWidget {
final bool fullWidth; final bool fullWidth;
final double value;
final ValueChanged<double> onChanged;
const VolumeSlider({ const VolumeSlider({
super.key, super.key,
this.fullWidth = false, this.fullWidth = false,
required this.value,
required this.onChanged,
}); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final volume = ref.watch(volumeProvider);
final volumeNotifier = ref.watch(volumeProvider.notifier);
var slider = Listener( var slider = Listener(
onPointerSignal: (event) async { onPointerSignal: (event) async {
if (event is PointerScrollEvent) { if (event is PointerScrollEvent) {
if (event.scrollDelta.dy > 0) { if (event.scrollDelta.dy > 0) {
final value = volume - .2; final newValue = value - .2;
volumeNotifier.setVolume(value < 0 ? 0 : value); onChanged(newValue < 0 ? 0 : newValue);
} else { } else {
final value = volume + .2; final newValue = value + .2;
volumeNotifier.setVolume(value > 1 ? 1 : value); onChanged(newValue > 1 ? 1 : newValue);
} }
} }
}, },
child: Slider( child: Slider(
min: 0, min: 0,
max: 1, max: 1,
value: volume, value: value,
onChanged: volumeNotifier.setVolume, onChanged: onChanged,
), ),
); );
return Row( return Row(
@ -42,20 +44,20 @@ class VolumeSlider extends HookConsumerWidget {
children: [ children: [
IconButton( IconButton(
icon: Icon( icon: Icon(
volume == 0 value == 0
? SpotubeIcons.volumeMute ? SpotubeIcons.volumeMute
: volume <= 0.2 : value <= 0.2
? SpotubeIcons.volumeLow ? SpotubeIcons.volumeLow
: volume <= 0.6 : value <= 0.6
? SpotubeIcons.volumeMedium ? SpotubeIcons.volumeMedium
: SpotubeIcons.volumeHigh, : SpotubeIcons.volumeHigh,
size: 16, size: 16,
), ),
onPressed: () { onPressed: () {
if (volume == 0) { if (value == 0) {
volumeNotifier.setVolume(1); onChanged(1);
} else { } else {
volumeNotifier.setVolume(0); onChanged(0);
} }
}, },
), ),

View File

@ -19,10 +19,11 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart' hide volumeProvider;
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class BottomPlayer extends HookConsumerWidget { class BottomPlayer extends HookConsumerWidget {
@ -125,7 +126,16 @@ class BottomPlayer extends HookConsumerWidget {
Container( Container(
height: 40, height: 40,
constraints: const BoxConstraints(maxWidth: 250), constraints: const BoxConstraints(maxWidth: 250),
child: const VolumeSlider(), child: Consumer(builder: (context, ref, _) {
final volume = ref.watch(volumeProvider);
return VolumeSlider(
fullWidth: true,
value: volume,
onChanged: (value) {
ref.read(volumeProvider.notifier).setVolume(value);
},
);
}),
) )
], ],
) )

View File

@ -2,6 +2,7 @@ part of 'connect.dart';
enum WsEvent { enum WsEvent {
error, error,
volume,
removeTrack, removeTrack,
addTrack, addTrack,
reorder, reorder,
@ -209,6 +210,14 @@ class WebSocketEvent<T> {
WebSocketReorderEvent.fromJson(data as Map<String, dynamic>)); WebSocketReorderEvent.fromJson(data as Map<String, dynamic>));
} }
} }
Future<void> onVolume(
EventCallback<WebSocketVolumeEvent> callback,
) async {
if (type == WsEvent.volume) {
await callback(WebSocketVolumeEvent(data as double));
}
}
} }
class WebSocketLoopEvent extends WebSocketEvent<PlaybackLoopMode> { class WebSocketLoopEvent extends WebSocketEvent<PlaybackLoopMode> {
@ -355,3 +364,7 @@ class WebSocketReorderEvent extends WebSocketEvent<ReorderData> {
}); });
} }
} }
class WebSocketVolumeEvent extends WebSocketEvent<double> {
WebSocketVolumeEvent(double data) : super(WsEvent.volume, data);
}

View File

@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_queue.dart'; import 'package:spotube/components/player/player_queue.dart';
import 'package:spotube/components/player/volume_slider.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/components/shared/links/anchor_button.dart'; import 'package:spotube/components/shared/links/anchor_button.dart';
import 'package:spotube/components/shared/links/artist_link.dart'; import 'package:spotube/components/shared/links/artist_link.dart';
@ -263,6 +264,22 @@ class ConnectControlPage extends HookConsumerWidget {
), ),
), ),
const SliverGap(30), const SliverGap(30),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 20),
sliver: SliverToBoxAdapter(
child: Consumer(builder: (context, ref, _) {
final volume = ref.watch(volumeProvider);
return VolumeSlider(
fullWidth: true,
value: volume,
onChanged: (value) {
ref.read(volumeProvider.notifier).state = value;
},
);
}),
),
),
const SliverGap(30),
if (constrains.mdAndDown) if (constrains.mdAndDown)
SliverPadding( SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),

View File

@ -34,6 +34,10 @@ final queueProvider = StateProvider<ProxyPlaylist>(
(ref) => ProxyPlaylist({}), (ref) => ProxyPlaylist({}),
); );
final volumeProvider = StateProvider<double>(
(ref) => 1.0,
);
class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> { class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
@override @override
build() async { build() async {
@ -85,6 +89,10 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
event.onLoop((event) { event.onLoop((event) {
ref.read(loopModeProvider.notifier).state = event.data; ref.read(loopModeProvider.notifier).state = event.data;
}); });
event.onVolume((event) {
ref.read(volumeProvider.notifier).state = event.data;
});
}, },
onError: (error) { onError: (error) {
Catcher2.reportCheckedError( Catcher2.reportCheckedError(
@ -164,6 +172,10 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
Future<void> reorder(ReorderData data) async { Future<void> reorder(ReorderData data) async {
emit(WebSocketReorderEvent(data)); emit(WebSocketReorderEvent(data));
} }
Future<void> setVolume(double value) async {
emit(WebSocketVolumeEvent(value));
}
} }
final connectProvider = final connectProvider =

View File

@ -74,6 +74,9 @@ final connectServerProvider = FutureProvider((ref) async {
channel.sink.add( channel.sink.add(
WebSocketLoopEvent(audioPlayer.loopMode).toJson(), WebSocketLoopEvent(audioPlayer.loopMode).toJson(),
); );
channel.sink.add(
WebSocketVolumeEvent(audioPlayer.volume).toJson(),
);
subscriptions.addAll([ subscriptions.addAll([
audioPlayer.positionStream.listen( audioPlayer.positionStream.listen(
@ -111,6 +114,13 @@ final connectServerProvider = FutureProvider((ref) async {
); );
}, },
), ),
audioPlayer.volumeStream.listen(
(volume) {
channel.sink.add(
WebSocketVolumeEvent(volume).toJson(),
);
},
),
channel.stream.listen( channel.stream.listen(
(message) { (message) {
try { try {
@ -181,6 +191,10 @@ final connectServerProvider = FutureProvider((ref) async {
event.data.newIndex, event.data.newIndex,
); );
}); });
event.onVolume((event) async {
await audioPlayer.setVolume(event.data);
});
} catch (e, stackTrace) { } catch (e, stackTrace) {
Catcher2.reportCheckedError(e, stackTrace); Catcher2.reportCheckedError(e, stackTrace);
channel.sink.add(WebSocketErrorEvent(e.toString()).toJson()); channel.sink.add(WebSocketErrorEvent(e.toString()).toJson());