-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: LAN connect a.k.a control remote Spotube playback and local out…
…put device selection (#1355) * feat: add connect server support * feat: add ability discover and connect to same network Spotube(s) and sync queue * feat(connect): add player controls, shuffle, loop, progress bar and queue support * feat: make control page adaptive * feat: add volume control support * cd: upgrade macos runner version * chore: upgrade inappwebview version to 6 * feat: customized devices button * feat: add user icon next to devices button * feat: add play in remote device support * feat: show alert when new client connects * fix: ignore the device itself from broadcast list * fix: volume control not working * feat: add ability to select current device's output speaker
- Loading branch information
Showing
63 changed files
with
3,089 additions
and
406 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -284,7 +284,7 @@ jobs: | |
|
||
macos: | ||
|
||
runs-on: macos-12 | ||
runs-on: macos-14 | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: subosito/[email protected] | ||
|
@@ -349,7 +349,7 @@ jobs: | |
limit-access-to-actor: true | ||
|
||
iOS: | ||
runs-on: macos-latest | ||
runs-on: macos-14 | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: subosito/[email protected] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
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/extensions/context.dart'; | ||
import 'package:spotube/provider/connect/clients.dart'; | ||
import 'package:spotube/utils/service_utils.dart'; | ||
|
||
class ConnectDeviceButton extends HookConsumerWidget { | ||
const ConnectDeviceButton({super.key}); | ||
|
||
@override | ||
Widget build(BuildContext context, ref) { | ||
final ThemeData(:colorScheme) = Theme.of(context); | ||
final pixelRatio = MediaQuery.of(context).devicePixelRatio; | ||
final connectClients = ref.watch(connectClientsProvider); | ||
|
||
return SizedBox( | ||
height: 40 * pixelRatio, | ||
child: Stack( | ||
alignment: Alignment.centerRight, | ||
fit: StackFit.loose, | ||
children: [ | ||
Center( | ||
child: InkWell( | ||
onTap: () { | ||
ServiceUtils.push(context, "/connect"); | ||
}, | ||
borderRadius: BorderRadius.circular(50), | ||
child: Ink( | ||
decoration: BoxDecoration( | ||
borderRadius: BorderRadius.circular(50), | ||
color: colorScheme.primaryContainer, | ||
), | ||
padding: | ||
const EdgeInsets.symmetric(horizontal: 10, vertical: 5), | ||
child: Row( | ||
mainAxisSize: MainAxisSize.min, | ||
children: [ | ||
if (connectClients.asData?.value.resolvedService != | ||
null) ...[ | ||
Container( | ||
width: 7, | ||
height: 7, | ||
decoration: BoxDecoration( | ||
color: Colors.greenAccent, | ||
borderRadius: BorderRadius.circular(50), | ||
), | ||
), | ||
const Gap(5), | ||
], | ||
Text(context.l10n.devices), | ||
if (connectClients.asData?.value.services.isNotEmpty == | ||
true) | ||
Text( | ||
" (${connectClients.asData?.value.services.length})", | ||
style: TextStyle( | ||
color: | ||
colorScheme.onPrimaryContainer.withOpacity(0.5), | ||
), | ||
), | ||
const Gap(35), | ||
], | ||
), | ||
), | ||
), | ||
), | ||
Positioned( | ||
right: 0, | ||
child: IconButton.filled( | ||
icon: const Icon(SpotubeIcons.speaker), | ||
style: IconButton.styleFrom( | ||
visualDensity: VisualDensity.standard, | ||
foregroundColor: colorScheme.onPrimary, | ||
), | ||
onPressed: () { | ||
ServiceUtils.push(context, "/connect"); | ||
}, | ||
), | ||
), | ||
], | ||
), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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), | ||
), | ||
); | ||
}, | ||
), | ||
], | ||
); | ||
} | ||
} |
Oops, something went wrong.