mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
feat: add ability discover and connect to same network Spotube(s) and sync queue
This commit is contained in:
parent
c399baa5ab
commit
351e833088
@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart' hide Search;
|
import 'package:spotify/spotify.dart' hide Search;
|
||||||
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
||||||
import 'package:spotube/pages/album/album.dart';
|
import 'package:spotube/pages/album/album.dart';
|
||||||
|
import 'package:spotube/pages/connect/connect.dart';
|
||||||
import 'package:spotube/pages/getting_started/getting_started.dart';
|
import 'package:spotube/pages/getting_started/getting_started.dart';
|
||||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
||||||
import 'package:spotube/pages/home/genres/genres.dart';
|
import 'package:spotube/pages/home/genres/genres.dart';
|
||||||
@ -173,6 +174,12 @@ final routerProvider = Provider((ref) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/connect",
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: ConnectPage(),
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
@ -117,4 +117,6 @@ abstract class SpotubeIcons {
|
|||||||
static const anonymous = FeatherIcons.user;
|
static const anonymous = FeatherIcons.user;
|
||||||
static const history = FeatherIcons.clock;
|
static const history = FeatherIcons.clock;
|
||||||
static const connect = FeatherIcons.link;
|
static const connect = FeatherIcons.link;
|
||||||
|
static const speaker = FeatherIcons.speaker;
|
||||||
|
static const monitor = FeatherIcons.monitor;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ 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/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';
|
||||||
@ -34,6 +35,7 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final layoutMode =
|
final layoutMode =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
final remoteControl = ref.watch(connectProvider);
|
||||||
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,8 @@ import 'package:spotube/l10n/l10n.dart';
|
|||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/models/skip_segment.dart';
|
import 'package:spotube/models/skip_segment.dart';
|
||||||
import 'package:spotube/models/source_match.dart';
|
import 'package:spotube/models/source_match.dart';
|
||||||
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/connect/server.dart';
|
import 'package:spotube/provider/connect/server.dart';
|
||||||
import 'package:spotube/provider/palette_provider.dart';
|
import 'package:spotube/provider/palette_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
@ -182,6 +184,7 @@ class SpotubeState extends ConsumerState<Spotube> {
|
|||||||
final router = ref.watch(routerProvider);
|
final router = ref.watch(routerProvider);
|
||||||
|
|
||||||
ref.read(connectServerProvider);
|
ref.read(connectServerProvider);
|
||||||
|
ref.read(connectClientsProvider);
|
||||||
|
|
||||||
useDisableBatteryOptimizations();
|
useDisableBatteryOptimizations();
|
||||||
useInitSysTray(ref);
|
useInitSysTray(ref);
|
||||||
|
|||||||
@ -47,8 +47,7 @@ class WebSocketEvent<T> {
|
|||||||
EventCallback<WebSocketPositionEvent> callback,
|
EventCallback<WebSocketPositionEvent> callback,
|
||||||
) async {
|
) async {
|
||||||
if (type == WsEvent.position) {
|
if (type == WsEvent.position) {
|
||||||
await callback(
|
await callback(WebSocketPositionEvent.fromJson({"data": data}));
|
||||||
WebSocketPositionEvent.fromJson(data as Map<String, dynamic>));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +128,8 @@ class WebSocketEvent<T> {
|
|||||||
) async {
|
) async {
|
||||||
if (type == WsEvent.queue) {
|
if (type == WsEvent.queue) {
|
||||||
await callback(
|
await callback(
|
||||||
WebSocketQueueEvent.fromJson(data as Map<String, dynamic>));
|
WebSocketQueueEvent.fromJson(data as Map<String, dynamic>),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,6 +186,6 @@ class WebSocketQueueEvent extends WebSocketEvent<ProxyPlaylist> {
|
|||||||
|
|
||||||
factory WebSocketQueueEvent.fromJson(Map<String, dynamic> json) =>
|
factory WebSocketQueueEvent.fromJson(Map<String, dynamic> json) =>
|
||||||
WebSocketQueueEvent(
|
WebSocketQueueEvent(
|
||||||
ProxyPlaylist.fromJsonRaw(json["data"] as Map<String, dynamic>),
|
ProxyPlaylist.fromJsonRaw(json),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
61
lib/pages/connect/connect.dart
Normal file
61
lib/pages/connect/connect.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
|
|
||||||
|
class ConnectPage extends HookConsumerWidget {
|
||||||
|
const ConnectPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final ThemeData(:colorScheme) = Theme.of(context);
|
||||||
|
|
||||||
|
final connectClients = ref.watch(connectClientsProvider);
|
||||||
|
final connectClientsNotifier = ref.read(connectClientsProvider.notifier);
|
||||||
|
final discoveredDevices = connectClients.asData?.value.services;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const PageWindowTitleBar(
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
title: Text("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),
|
||||||
|
),
|
||||||
|
selectedTileColor: colorScheme.secondary.withOpacity(0.1),
|
||||||
|
onTap: () {
|
||||||
|
if (selected) {
|
||||||
|
connectClientsNotifier.clearResolvedService();
|
||||||
|
} else {
|
||||||
|
connectClientsNotifier.resolveService(device);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailing: selected ? const Icon(SpotubeIcons.done) : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,18 +3,22 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.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/components/home/sections/featured.dart';
|
import 'package:spotube/components/home/sections/featured.dart';
|
||||||
import 'package:spotube/components/home/sections/friends.dart';
|
import 'package:spotube/components/home/sections/friends.dart';
|
||||||
import 'package:spotube/components/home/sections/genres.dart';
|
import 'package:spotube/components/home/sections/genres.dart';
|
||||||
import 'package:spotube/components/home/sections/made_for_user.dart';
|
import 'package:spotube/components/home/sections/made_for_user.dart';
|
||||||
import 'package:spotube/components/home/sections/new_releases.dart';
|
import 'package:spotube/components/home/sections/new_releases.dart';
|
||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class HomePage extends HookConsumerWidget {
|
class HomePage extends HookConsumerWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
final ThemeData(:colorScheme) = Theme.of(context);
|
||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
@ -29,6 +33,29 @@ class HomePage extends HookConsumerWidget {
|
|||||||
slivers: [
|
slivers: [
|
||||||
if (DesktopTools.platform.isMacOS || DesktopTools.platform.isWeb)
|
if (DesktopTools.platform.isMacOS || DesktopTools.platform.isWeb)
|
||||||
const SliverGap(20),
|
const SliverGap(20),
|
||||||
|
SliverAppBar(
|
||||||
|
actions: [
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final connectClients = ref.watch(connectClientsProvider);
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(SpotubeIcons.speaker),
|
||||||
|
style: connectClients.asData?.value.resolvedService !=
|
||||||
|
null
|
||||||
|
? IconButton.styleFrom(
|
||||||
|
backgroundColor: colorScheme.primaryContainer,
|
||||||
|
foregroundColor: colorScheme.primary,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onPressed: () {
|
||||||
|
ServiceUtils.push(context, "/connect");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
const HomeGenresSection(),
|
const HomeGenresSection(),
|
||||||
const SliverToBoxAdapter(child: HomeFeaturedSection()),
|
const SliverToBoxAdapter(child: HomeFeaturedSection()),
|
||||||
const HomePageFriendsSection(),
|
const HomePageFriendsSection(),
|
||||||
|
|||||||
99
lib/provider/connect/clients.dart
Normal file
99
lib/provider/connect/clients.dart
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import 'package:bonsoir/bonsoir.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class ConnectClientsState {
|
||||||
|
final List<BonsoirService> services;
|
||||||
|
final ResolvedBonsoirService? resolvedService;
|
||||||
|
final BonsoirDiscovery discovery;
|
||||||
|
|
||||||
|
ConnectClientsState({
|
||||||
|
required this.services,
|
||||||
|
required this.discovery,
|
||||||
|
this.resolvedService,
|
||||||
|
});
|
||||||
|
|
||||||
|
ConnectClientsState copyWith({
|
||||||
|
List<BonsoirService>? services,
|
||||||
|
BonsoirDiscovery? discovery,
|
||||||
|
ResolvedBonsoirService? resolvedService,
|
||||||
|
}) {
|
||||||
|
return ConnectClientsState(
|
||||||
|
services: services ?? this.services,
|
||||||
|
discovery: discovery ?? this.discovery,
|
||||||
|
resolvedService: resolvedService ?? this.resolvedService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
|
||||||
|
ConnectClientsNotifier();
|
||||||
|
|
||||||
|
@override
|
||||||
|
build() async {
|
||||||
|
final discovery = BonsoirDiscovery(type: '_spotube._tcp');
|
||||||
|
await discovery.ready;
|
||||||
|
|
||||||
|
final subscription = discovery.eventStream?.listen((event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case BonsoirDiscoveryEventType.discoveryServiceFound:
|
||||||
|
state = AsyncData(state.value!.copyWith(
|
||||||
|
services: [
|
||||||
|
...?state.value?.services,
|
||||||
|
event.service!,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case BonsoirDiscoveryEventType.discoveryServiceResolved:
|
||||||
|
state = AsyncData(
|
||||||
|
state.value!.copyWith(
|
||||||
|
resolvedService: event.service as ResolvedBonsoirService,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case BonsoirDiscoveryEventType.discoveryServiceLost:
|
||||||
|
state = AsyncData(
|
||||||
|
state.value!.copyWith(
|
||||||
|
services: state.value!.services
|
||||||
|
.where((service) => service.name != event.service!.name)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
subscription?.cancel();
|
||||||
|
discovery.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
await discovery.start();
|
||||||
|
|
||||||
|
return ConnectClientsState(
|
||||||
|
services: [],
|
||||||
|
discovery: discovery,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resolveService(BonsoirService service) async {
|
||||||
|
if (state.value == null) return;
|
||||||
|
await service.resolve(state.value!.discovery.serviceResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearResolvedService() async {
|
||||||
|
if (state.value == null) return;
|
||||||
|
state = AsyncData(
|
||||||
|
ConnectClientsState(
|
||||||
|
services: state.value!.services,
|
||||||
|
discovery: state.value!.discovery,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final connectClientsProvider =
|
||||||
|
AsyncNotifierProvider<ConnectClientsNotifier, ConnectClientsState>(
|
||||||
|
() => ConnectClientsNotifier(),
|
||||||
|
);
|
||||||
121
lib/provider/connect/connect.dart
Normal file
121
lib/provider/connect/connect.dart
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
import 'package:web_socket_channel/status.dart' as status;
|
||||||
|
|
||||||
|
final playingStreamController = StreamController<bool>.broadcast();
|
||||||
|
final playingProvider = StreamProvider.autoDispose<bool>(
|
||||||
|
(ref) => playingStreamController.stream,
|
||||||
|
);
|
||||||
|
|
||||||
|
final positionStreamController = StreamController<Duration>.broadcast();
|
||||||
|
final positionProvider = StreamProvider.autoDispose<Duration>(
|
||||||
|
(ref) => positionStreamController.stream,
|
||||||
|
);
|
||||||
|
|
||||||
|
class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
|
||||||
|
@override
|
||||||
|
build() async {
|
||||||
|
try {
|
||||||
|
final connectClients = ref.watch(connectClientsProvider);
|
||||||
|
print('Building ConnectNotifier');
|
||||||
|
|
||||||
|
if (connectClients.asData?.value.resolvedService == null) return null;
|
||||||
|
|
||||||
|
final service = connectClients.asData!.value.resolvedService!;
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Connecting to ${service.name}: ws://${service.host}:${service.port}/ws');
|
||||||
|
|
||||||
|
final channel = WebSocketChannel.connect(
|
||||||
|
Uri.parse('ws://${service.host}:${service.port}/ws'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await channel.ready;
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Connected to ${service.name}: ws://${service.host}:${service.port}/ws');
|
||||||
|
|
||||||
|
final subscription = channel.stream.listen(
|
||||||
|
(message) {
|
||||||
|
final event =
|
||||||
|
WebSocketEvent.fromJson(jsonDecode(message), (data) => data);
|
||||||
|
|
||||||
|
event.onQueue((event) {
|
||||||
|
ref.read(ProxyPlaylistNotifier.notifier).state = event.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onPlaying((event) {
|
||||||
|
playingStreamController.add(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onPosition((event) {
|
||||||
|
positionStreamController.add(event.data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
Catcher2.reportCheckedError(
|
||||||
|
error,
|
||||||
|
StackTrace.current,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
subscription.cancel();
|
||||||
|
channel.sink.close(status.goingAway);
|
||||||
|
});
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
} catch (e, stack) {
|
||||||
|
Catcher2.reportCheckedError(e, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void emit(Object message) {
|
||||||
|
if (state.value == null) return;
|
||||||
|
state.value?.sink.add(
|
||||||
|
message is String ? message : (message as dynamic).toJson(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resume() {
|
||||||
|
emit(WebSocketResumeEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
void pause() {
|
||||||
|
emit(WebSocketPauseEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
emit(WebSocketStopEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
void jumpTo(int position) {
|
||||||
|
emit(WebSocketJumpEvent(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
void load(WebSocketLoadEventData data) {
|
||||||
|
emit(WebSocketLoadEvent(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void next() {
|
||||||
|
emit(WebSocketNextEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
void previous() {
|
||||||
|
emit(WebSocketPreviousEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final connectProvider =
|
||||||
|
AsyncNotifierProvider<ConnectNotifier, WebSocketChannel?>(
|
||||||
|
() => ConnectNotifier(),
|
||||||
|
);
|
||||||
@ -4,6 +4,7 @@ import 'dart:io';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:catcher_2/catcher_2.dart';
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf/shelf_io.dart';
|
import 'package:shelf/shelf_io.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
@ -14,6 +15,8 @@ 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/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:bonsoir/bonsoir.dart';
|
import 'package:bonsoir/bonsoir.dart';
|
||||||
|
import 'package:spotube/services/device_info/device_info.dart';
|
||||||
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
final logger = getLogger('ConnectServer');
|
final logger = getLogger('ConnectServer');
|
||||||
@ -29,6 +32,13 @@ final connectServerProvider = FutureProvider((ref) async {
|
|||||||
|
|
||||||
final app = Router();
|
final app = Router();
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
"/ping",
|
||||||
|
(Request req) {
|
||||||
|
return Response.ok("pong");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final subscriptions = <StreamSubscription>[];
|
final subscriptions = <StreamSubscription>[];
|
||||||
|
|
||||||
final websocket = webSocketHandler(
|
final websocket = webSocketHandler(
|
||||||
@ -40,6 +50,7 @@ final connectServerProvider = FutureProvider((ref) async {
|
|||||||
WebSocketQueueEvent(next).toJson(),
|
WebSocketQueueEvent(next).toJson(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
fireImmediately: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.addAll([
|
subscriptions.addAll([
|
||||||
@ -122,16 +133,19 @@ final connectServerProvider = FutureProvider((ref) async {
|
|||||||
}
|
}
|
||||||
return app(request);
|
return app(request);
|
||||||
},
|
},
|
||||||
InternetAddress.loopbackIPv4,
|
InternetAddress.anyIPv4,
|
||||||
port,
|
port,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.i('Server running on http://${server.address.host}:${server.port}');
|
logger.i('Server running on http://${server.address.host}:${server.port}');
|
||||||
|
|
||||||
final service = BonsoirService(
|
final service = BonsoirService(
|
||||||
name: 'Spotube',
|
name: await DeviceInfoService.instance.computerName(),
|
||||||
type: '_spotube._tcp',
|
type: '_spotube._tcp',
|
||||||
port: port,
|
port: port,
|
||||||
|
attributes: {
|
||||||
|
"id": PrimitiveUtils.uuid.v4(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final broadcast = BonsoirBroadcast(service: service);
|
final broadcast = BonsoirBroadcast(service: service);
|
||||||
|
|||||||
21
lib/services/device_info/device_info.dart
Normal file
21
lib/services/device_info/device_info.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
|
||||||
|
class DeviceInfoService {
|
||||||
|
final DeviceInfoPlugin deviceInfo;
|
||||||
|
DeviceInfoService._() : deviceInfo = DeviceInfoPlugin();
|
||||||
|
|
||||||
|
static final instance = DeviceInfoService._();
|
||||||
|
|
||||||
|
Future<String> computerName() async {
|
||||||
|
final info = await deviceInfo.deviceInfo;
|
||||||
|
|
||||||
|
return switch (info) {
|
||||||
|
AndroidDeviceInfo() => info.model,
|
||||||
|
IosDeviceInfo() => info.localizedModel,
|
||||||
|
MacOsDeviceInfo() => info.computerName,
|
||||||
|
WindowsDeviceInfo() => info.computerName,
|
||||||
|
LinuxDeviceInfo() => info.name,
|
||||||
|
_ => 'Unknown',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -542,10 +542,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659"
|
sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.3"
|
version: "9.1.2"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -25,7 +25,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.5
|
cupertino_icons: ^1.0.5
|
||||||
curved_navigation_bar: ^1.0.3
|
curved_navigation_bar: ^1.0.3
|
||||||
dbus: ^0.7.8
|
dbus: ^0.7.8
|
||||||
device_info_plus: ^9.0.3
|
device_info_plus: ^9.1.2
|
||||||
device_preview: ^1.1.0
|
device_preview: ^1.1.0
|
||||||
dio: ^5.4.1
|
dio: ^5.4.1
|
||||||
disable_battery_optimization: ^1.1.0+1
|
disable_battery_optimization: ^1.1.0+1
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user