diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 2489fd71..6de21284 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -120,4 +120,5 @@ abstract class SpotubeIcons { static const speaker = FeatherIcons.speaker; static const monitor = FeatherIcons.monitor; static const power = FeatherIcons.power; + static const bluetooth = FeatherIcons.bluetooth; } diff --git a/lib/components/connect/local_devices.dart b/lib/components/connect/local_devices.dart new file mode 100644 index 00000000..dd7db971 --- /dev/null +++ b/lib/components/connect/local_devices.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; + +class ConnectPageLocalDevices extends HookWidget { + const ConnectPageLocalDevices({super.key}); + + @override + Widget build(BuildContext context) { + final ThemeData(:textTheme) = Theme.of(context); + final devicesFuture = useFuture(audioPlayer.devices); + final devicesStream = useStream(audioPlayer.devicesStream); + final selectedDeviceFuture = useFuture(audioPlayer.selectedDevice); + final selectedDeviceStream = useStream(audioPlayer.selectedDeviceStream); + + final devices = devicesStream.data ?? devicesFuture.data; + final selectedDevice = + selectedDeviceStream.data ?? selectedDeviceFuture.data; + + if (devices == null) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + return SliverMainAxisGroup( + slivers: [ + const SliverGap(10), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + sliver: SliverToBoxAdapter( + child: Text( + context.l10n.this_device, + style: textTheme.titleMedium, + ), + ), + ), + const SliverGap(10), + SliverList.separated( + itemCount: devices.length, + separatorBuilder: (context, index) => const Gap(10), + itemBuilder: (context, index) { + final device = devices[index]; + + return Card( + child: ListTile( + leading: const Icon(SpotubeIcons.speaker), + title: Text(device.description), + subtitle: Text(device.name), + selected: selectedDevice == device, + onTap: () => audioPlayer.setAudioDevice(device), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 311e7d5c..832862c0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -318,5 +318,7 @@ "enable_connect_description": "Control Spotube from other devices", "devices": "Devices", "select": "Select", - "connect_client_alert": "You're being controlled by {client}" + "connect_client_alert": "You're being controlled by {client}", + "this_device": "This Device", + "remote": "Remote" } \ No newline at end of file diff --git a/lib/pages/connect/connect.dart b/lib/pages/connect/connect.dart index 6847b2c0..170a0c72 100644 --- a/lib/pages/connect/connect.dart +++ b/lib/pages/connect/connect.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/connect/local_devices.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/connect/clients.dart'; @@ -12,7 +13,7 @@ class ConnectPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final ThemeData(:colorScheme) = Theme.of(context); + final ThemeData(:colorScheme, :textTheme) = Theme.of(context); final connectClients = ref.watch(connectClientsProvider); final connectClientsNotifier = ref.read(connectClientsProvider.notifier); @@ -23,49 +24,69 @@ class ConnectPage extends HookConsumerWidget { automaticallyImplyLeading: true, title: Text(context.l10n.devices), ), - body: ListView.separated( - padding: const EdgeInsets.all(10), - itemCount: discoveredDevices?.length ?? 0, - separatorBuilder: (context, index) => const Gap(10), - itemBuilder: (context, index) { - final device = discoveredDevices![index]; - final selected = - connectClients.asData?.value.resolvedService?.name == device.name; - return Card( - child: ListTile( - leading: const Icon(SpotubeIcons.monitor), - title: Text(device.name), - subtitle: selected - ? Text( - "${connectClients.asData?.value.resolvedService?.host}" - ":${connectClients.asData?.value.resolvedService?.port}", - ) - : null, - selected: selected, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + body: ListTileTheme( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + selectedTileColor: colorScheme.secondary.withOpacity(0.1), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + sliver: SliverToBoxAdapter( + child: Text( + context.l10n.remote, + style: textTheme.titleMedium, + ), + ), ), - selectedTileColor: colorScheme.secondary.withOpacity(0.1), - onTap: () { - if (selected) { - ServiceUtils.push( - context, - "/connect/control", + const SliverGap(10), + SliverList.separated( + itemCount: discoveredDevices?.length ?? 0, + separatorBuilder: (context, index) => const Gap(10), + itemBuilder: (context, index) { + final device = discoveredDevices![index]; + final selected = + connectClients.asData?.value.resolvedService?.name == + device.name; + return Card( + child: ListTile( + leading: const Icon(SpotubeIcons.monitor), + title: Text(device.name), + subtitle: selected + ? Text( + "${connectClients.asData?.value.resolvedService?.host}" + ":${connectClients.asData?.value.resolvedService?.port}", + ) + : null, + selected: selected, + onTap: () { + if (selected) { + ServiceUtils.push( + context, + "/connect/control", + ); + } else { + connectClientsNotifier.resolveService(device); + } + }, + trailing: selected + ? IconButton( + icon: const Icon(SpotubeIcons.power), + onPressed: () => + connectClientsNotifier.clearResolvedService(), + ) + : null, + ), ); - } else { - connectClientsNotifier.resolveService(device); - } - }, - trailing: selected - ? IconButton( - icon: const Icon(SpotubeIcons.power), - onPressed: () => - connectClientsNotifier.clearResolvedService(), - ) - : null, - ), - ); - }, + }, + ), + const ConnectPageLocalDevices(), + ], + ), + ), ), ); } diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index b3957964..0a22bec1 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,4 +1,5 @@ import 'package:catcher_2/catcher_2.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:spotube/services/audio_player/mk_state_player.dart'; // import 'package:just_audio/just_audio.dart' as ja; import 'dart:async'; @@ -14,7 +15,7 @@ part 'audio_player_impl.dart'; abstract class AudioPlayerInterface { final MkPlayerWithState _mkPlayer; - // final ja.AudioPlayer? _justAudio; + // final ja.AudioPlayer? _justAudxio; AudioPlayerInterface() : _mkPlayer = MkPlayerWithState( @@ -60,6 +61,14 @@ abstract class AudioPlayerInterface { } } + Future get selectedDevice async { + return _mkPlayer.state.audioDevice; + } + + Future> get devices async { + return _mkPlayer.state.audioDevices; + } + bool get hasSource { return _mkPlayer.playlist.medias.isNotEmpty; // if (mkSupportedPlatform) { diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index 2af94dd7..bfa13220 100644 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -83,6 +83,10 @@ class SpotubeAudioPlayer extends AudioPlayerInterface // await _justAudio?.setSpeed(speed); } + Future setAudioDevice(AudioDevice device) async { + await _mkPlayer.setAudioDevice(device); + } + Future dispose() async { await _mkPlayer.dispose(); // await _justAudio?.dispose(); diff --git a/lib/services/audio_player/audio_players_streams_mixin.dart b/lib/services/audio_player/audio_players_streams_mixin.dart index a736dc1c..f05ba5ef 100644 --- a/lib/services/audio_player/audio_players_streams_mixin.dart +++ b/lib/services/audio_player/audio_players_streams_mixin.dart @@ -140,4 +140,10 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface { // .cast(); // } } + + Stream> get devicesStream => + _mkPlayer.stream.audioDevices.asBroadcastStream(); + + Stream get selectedDeviceStream => + _mkPlayer.stream.audioDevice.asBroadcastStream(); } diff --git a/untranslated_messages.json b/untranslated_messages.json index ed3cd980..be7d38f1 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -4,7 +4,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "bn": [ @@ -12,7 +14,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "ca": [ @@ -20,7 +24,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "de": [ @@ -28,7 +34,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "es": [ @@ -36,7 +44,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "fa": [ @@ -44,7 +54,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "fr": [ @@ -52,7 +64,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "hi": [ @@ -60,7 +74,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "it": [ @@ -68,7 +84,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "ja": [ @@ -76,7 +94,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "ko": [ @@ -84,7 +104,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "ne": [ @@ -92,7 +114,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "nl": [ @@ -100,7 +124,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "pl": [ @@ -108,7 +134,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "pt": [ @@ -116,7 +144,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "ru": [ @@ -124,7 +154,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "tr": [ @@ -132,7 +164,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "uk": [ @@ -140,7 +174,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "vi": [ @@ -150,7 +186,9 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ], "zh": [ @@ -158,6 +196,8 @@ "enable_connect_description", "devices", "select", - "connect_client_alert" + "connect_client_alert", + "this_device", + "remote" ] }