mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
feat: add volume control support
This commit is contained in:
parent
7e887d54ed
commit
d693624b6d
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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 =
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user