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/provider/authentication_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:url_launcher/url_launcher_string.dart';
@ -378,11 +379,21 @@ class PlayerView extends HookConsumerWidget {
enabledThumbRadius: 8,
),
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: VolumeSlider(
fullWidth: true,
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16),
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_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:spotube/collections/spotube_icons.dart';
import 'package:spotube/provider/volume_provider.dart';
class VolumeSlider extends HookConsumerWidget {
final bool fullWidth;
final double value;
final ValueChanged<double> onChanged;
const VolumeSlider({
super.key,
this.fullWidth = false,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context, ref) {
final volume = ref.watch(volumeProvider);
final volumeNotifier = ref.watch(volumeProvider.notifier);
var slider = Listener(
onPointerSignal: (event) async {
if (event is PointerScrollEvent) {
if (event.scrollDelta.dy > 0) {
final value = volume - .2;
volumeNotifier.setVolume(value < 0 ? 0 : value);
final newValue = value - .2;
onChanged(newValue < 0 ? 0 : newValue);
} else {
final value = volume + .2;
volumeNotifier.setVolume(value > 1 ? 1 : value);
final newValue = value + .2;
onChanged(newValue > 1 ? 1 : newValue);
}
}
},
child: Slider(
min: 0,
max: 1,
value: volume,
onChanged: volumeNotifier.setVolume,
value: value,
onChanged: onChanged,
),
);
return Row(
@ -42,20 +44,20 @@ class VolumeSlider extends HookConsumerWidget {
children: [
IconButton(
icon: Icon(
volume == 0
value == 0
? SpotubeIcons.volumeMute
: volume <= 0.2
: value <= 0.2
? SpotubeIcons.volumeLow
: volume <= 0.6
: value <= 0.6
? SpotubeIcons.volumeMedium
: SpotubeIcons.volumeHigh,
size: 16,
),
onPressed: () {
if (volume == 0) {
volumeNotifier.setVolume(1);
if (value == 0) {
onChanged(1);
} 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:flutter/material.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/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/utils/platform.dart';
class BottomPlayer extends HookConsumerWidget {
@ -125,7 +126,16 @@ class BottomPlayer extends HookConsumerWidget {
Container(
height: 40,
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 {
error,
volume,
removeTrack,
addTrack,
reorder,
@ -209,6 +210,14 @@ class WebSocketEvent<T> {
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> {
@ -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:spotube/collections/spotube_icons.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/links/anchor_button.dart';
import 'package:spotube/components/shared/links/artist_link.dart';
@ -263,6 +264,22 @@ class ConnectControlPage extends HookConsumerWidget {
),
),
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)
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 20),

View File

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

View File

@ -74,6 +74,9 @@ final connectServerProvider = FutureProvider((ref) async {
channel.sink.add(
WebSocketLoopEvent(audioPlayer.loopMode).toJson(),
);
channel.sink.add(
WebSocketVolumeEvent(audioPlayer.volume).toJson(),
);
subscriptions.addAll([
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(
(message) {
try {
@ -181,6 +191,10 @@ final connectServerProvider = FutureProvider((ref) async {
event.data.newIndex,
);
});
event.onVolume((event) async {
await audioPlayer.setVolume(event.data);
});
} catch (e, stackTrace) {
Catcher2.reportCheckedError(e, stackTrace);
channel.sink.add(WebSocketErrorEvent(e.toString()).toJson());