From 0ed358eeb8a2e49f27b8d29c48183def258a8ff0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 28 Mar 2024 21:38:12 +0600 Subject: [PATCH] feat: show alert when new client connects --- lib/l10n/app_en.arb | 3 +- lib/pages/root/root_app.dart | 88 ++++++--- lib/provider/connect/server.dart | 323 ++++++++++++++++--------------- untranslated_messages.json | 60 ++++-- 4 files changed, 265 insertions(+), 209 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9e3b2938..311e7d5c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -317,5 +317,6 @@ "enable_connect": "Enable Connect", "enable_connect_description": "Control Spotube from other devices", "devices": "Devices", - "select": "Select" + "select": "Select", + "connect_client_alert": "You're being controlled by {client}" } \ No newline at end of file diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 729ad88d..2e079200 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -16,6 +16,7 @@ import 'package:spotube/components/root/spotube_navigation_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart'; import 'package:spotube/hooks/configurators/use_update_checker.dart'; +import 'package:spotube/provider/connect/server.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/connectivity_adapter.dart'; @@ -54,50 +55,75 @@ class RootApp extends HookConsumerWidget { } }); - final subscription = ConnectionCheckerService - .instance.onConnectivityChanged - .listen((status) { - if (status) { + final subscriptions = [ + ConnectionCheckerService.instance.onConnectivityChanged + .listen((status) { + if (status) { + scaffoldMessenger.showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + SpotubeIcons.wifi, + color: theme.colorScheme.onPrimary, + ), + const SizedBox(width: 10), + Text(context.l10n.connection_restored), + ], + ), + backgroundColor: theme.colorScheme.primary, + showCloseIcon: true, + width: 350, + ), + ); + } else { + scaffoldMessenger.showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + SpotubeIcons.noWifi, + color: theme.colorScheme.onError, + ), + const SizedBox(width: 10), + Text(context.l10n.you_are_offline), + ], + ), + backgroundColor: theme.colorScheme.error, + showCloseIcon: true, + width: 300, + ), + ); + } + }), + connectClientStream.listen((clientOrigin) { scaffoldMessenger.showSnackBar( SnackBar( + backgroundColor: Colors.yellow[600], + behavior: SnackBarBehavior.floating, content: Row( + mainAxisSize: MainAxisSize.min, children: [ - Icon( - SpotubeIcons.wifi, - color: theme.colorScheme.onPrimary, + const Icon( + SpotubeIcons.error, + color: Colors.black, ), const SizedBox(width: 10), - Text(context.l10n.connection_restored), - ], - ), - backgroundColor: theme.colorScheme.primary, - showCloseIcon: true, - width: 350, - ), - ); - } else { - scaffoldMessenger.showSnackBar( - SnackBar( - content: Row( - children: [ - Icon( - SpotubeIcons.noWifi, - color: theme.colorScheme.onError, + Text( + context.l10n.connect_client_alert(clientOrigin), + style: const TextStyle(color: Colors.black), ), - const SizedBox(width: 10), - Text(context.l10n.you_are_offline), ], ), - backgroundColor: theme.colorScheme.error, - showCloseIcon: true, - width: 300, ), ); - } - }); + }) + ]; return () { - subscription.cancel(); + for (final subscription in subscriptions) { + subscription.cancel(); + } }; }, []); diff --git a/lib/provider/connect/server.dart b/lib/provider/connect/server.dart index 7288711c..895cd37e 100644 --- a/lib/provider/connect/server.dart +++ b/lib/provider/connect/server.dart @@ -22,6 +22,9 @@ import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:spotube/provider/volume_provider.dart'; final logger = getLogger('ConnectServer'); +final _connectClientStreamController = StreamController.broadcast(); + +Stream get connectClientStream => _connectClientStreamController.stream; final connectServerProvider = FutureProvider((ref) async { final enabled = @@ -45,169 +48,175 @@ final connectServerProvider = FutureProvider((ref) async { final subscriptions = []; - final websocket = webSocketHandler( - (WebSocketChannel channel, String? protocol) async { - ref.listen( - ProxyPlaylistNotifier.provider, - (previous, next) { - channel.sink.add( - WebSocketQueueEvent(next).toJson(), - ); - }, - fireImmediately: true, - ); + FutureOr websocket(Request req) => webSocketHandler( + (WebSocketChannel channel, String? protocol) async { + final context = + (req.context["shelf.io.connection_info"] as HttpConnectionInfo?); + final origin = + "${context?.remoteAddress.host}:${context?.remotePort}"; + _connectClientStreamController.add(origin); - // because audioPlayer events doesn't fireImmediately - channel.sink.add( - WebSocketPlayingEvent(audioPlayer.isPlaying).toJson(), - ); - channel.sink.add( - WebSocketPositionEvent(await audioPlayer.position ?? Duration.zero) - .toJson(), - ); - channel.sink.add( - WebSocketDurationEvent(await audioPlayer.duration ?? Duration.zero) - .toJson(), - ); - channel.sink.add( - WebSocketShuffleEvent(await audioPlayer.isShuffled).toJson(), - ); - channel.sink.add( - WebSocketLoopEvent(audioPlayer.loopMode).toJson(), - ); - channel.sink.add( - WebSocketVolumeEvent(audioPlayer.volume).toJson(), - ); - - subscriptions.addAll([ - audioPlayer.positionStream.listen( - (position) { - channel.sink.add( - WebSocketPositionEvent(position).toJson(), - ); - }, - ), - audioPlayer.playingStream.listen( - (playing) { - channel.sink.add( - WebSocketPlayingEvent(playing).toJson(), - ); - }, - ), - audioPlayer.durationStream.listen( - (duration) { - channel.sink.add( - WebSocketDurationEvent(duration).toJson(), - ); - }, - ), - audioPlayer.shuffledStream.listen( - (shuffled) { - channel.sink.add( - WebSocketShuffleEvent(shuffled).toJson(), - ); - }, - ), - audioPlayer.loopModeStream.listen( - (loopMode) { - channel.sink.add( - WebSocketLoopEvent(loopMode).toJson(), - ); - }, - ), - audioPlayer.volumeStream.listen( - (volume) { - channel.sink.add( - WebSocketVolumeEvent(volume).toJson(), - ); - }, - ), - channel.stream.listen( - (message) { - try { - final event = WebSocketEvent.fromJson( - jsonDecode(message), - (data) => data, + ref.listen( + ProxyPlaylistNotifier.provider, + (previous, next) { + channel.sink.add( + WebSocketQueueEvent(next).toJson(), ); + }, + fireImmediately: true, + ); - event.onLoad((event) async { - await playbackNotifier.load( - event.data.tracks, - autoPlay: true, - initialIndex: event.data.initialIndex ?? 0, + // because audioPlayer events doesn't fireImmediately + channel.sink.add( + WebSocketPlayingEvent(audioPlayer.isPlaying).toJson(), + ); + channel.sink.add( + WebSocketPositionEvent(await audioPlayer.position ?? Duration.zero) + .toJson(), + ); + channel.sink.add( + WebSocketDurationEvent(await audioPlayer.duration ?? Duration.zero) + .toJson(), + ); + channel.sink.add( + WebSocketShuffleEvent(await audioPlayer.isShuffled).toJson(), + ); + channel.sink.add( + WebSocketLoopEvent(audioPlayer.loopMode).toJson(), + ); + channel.sink.add( + WebSocketVolumeEvent(audioPlayer.volume).toJson(), + ); + + subscriptions.addAll([ + audioPlayer.positionStream.listen( + (position) { + channel.sink.add( + WebSocketPositionEvent(position).toJson(), ); + }, + ), + audioPlayer.playingStream.listen( + (playing) { + channel.sink.add( + WebSocketPlayingEvent(playing).toJson(), + ); + }, + ), + audioPlayer.durationStream.listen( + (duration) { + channel.sink.add( + WebSocketDurationEvent(duration).toJson(), + ); + }, + ), + audioPlayer.shuffledStream.listen( + (shuffled) { + channel.sink.add( + WebSocketShuffleEvent(shuffled).toJson(), + ); + }, + ), + audioPlayer.loopModeStream.listen( + (loopMode) { + channel.sink.add( + WebSocketLoopEvent(loopMode).toJson(), + ); + }, + ), + audioPlayer.volumeStream.listen( + (volume) { + channel.sink.add( + WebSocketVolumeEvent(volume).toJson(), + ); + }, + ), + channel.stream.listen( + (message) { + try { + final event = WebSocketEvent.fromJson( + jsonDecode(message), + (data) => data, + ); - if (event.data.collectionId != null) { - playbackNotifier.addCollection(event.data.collectionId!); + event.onLoad((event) async { + await playbackNotifier.load( + event.data.tracks, + autoPlay: true, + initialIndex: event.data.initialIndex ?? 0, + ); + + if (event.data.collectionId != null) { + playbackNotifier.addCollection(event.data.collectionId!); + } + }); + + event.onPause((event) async { + await audioPlayer.pause(); + }); + + event.onResume((event) async { + await audioPlayer.resume(); + }); + + event.onStop((event) async { + await audioPlayer.stop(); + }); + + event.onNext((event) async { + await playbackNotifier.next(); + }); + + event.onPrevious((event) async { + await playbackNotifier.previous(); + }); + + event.onJump((event) async { + await playbackNotifier.jumpTo(event.data); + }); + + event.onSeek((event) async { + await audioPlayer.seek(event.data); + }); + + event.onShuffle((event) async { + await audioPlayer.setShuffle(event.data); + }); + + event.onLoop((event) async { + await audioPlayer.setLoopMode(event.data); + }); + + event.onAddTrack((event) async { + await playbackNotifier.addTrack(event.data); + }); + + event.onRemoveTrack((event) async { + await playbackNotifier.removeTrack(event.data); + }); + + event.onReorder((event) async { + await playbackNotifier.moveTrack( + event.data.oldIndex, + event.data.newIndex, + ); + }); + + event.onVolume((event) async { + ref.read(volumeProvider.notifier).setVolume(event.data); + }); + } catch (e, stackTrace) { + Catcher2.reportCheckedError(e, stackTrace); + channel.sink.add(WebSocketErrorEvent(e.toString()).toJson()); } - }); - - event.onPause((event) async { - await audioPlayer.pause(); - }); - - event.onResume((event) async { - await audioPlayer.resume(); - }); - - event.onStop((event) async { - await audioPlayer.stop(); - }); - - event.onNext((event) async { - await playbackNotifier.next(); - }); - - event.onPrevious((event) async { - await playbackNotifier.previous(); - }); - - event.onJump((event) async { - await playbackNotifier.jumpTo(event.data); - }); - - event.onSeek((event) async { - await audioPlayer.seek(event.data); - }); - - event.onShuffle((event) async { - await audioPlayer.setShuffle(event.data); - }); - - event.onLoop((event) async { - await audioPlayer.setLoopMode(event.data); - }); - - event.onAddTrack((event) async { - await playbackNotifier.addTrack(event.data); - }); - - event.onRemoveTrack((event) async { - await playbackNotifier.removeTrack(event.data); - }); - - event.onReorder((event) async { - await playbackNotifier.moveTrack( - event.data.oldIndex, - event.data.newIndex, - ); - }); - - event.onVolume((event) async { - ref.read(volumeProvider.notifier).setVolume(event.data); - }); - } catch (e, stackTrace) { - Catcher2.reportCheckedError(e, stackTrace); - channel.sink.add(WebSocketErrorEvent(e.toString()).toJson()); - } - }, - onDone: () { - logger.i('Connection closed'); - }, - ), - ]); - }, - ); + }, + onDone: () { + logger.i('Connection closed'); + }, + ), + ]); + }, + )(req); final port = Random().nextInt(17000) + 3000; diff --git a/untranslated_messages.json b/untranslated_messages.json index dd59724a..ed3cd980 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -3,126 +3,144 @@ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "bn": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "ca": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "de": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "es": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "fa": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "fr": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "hi": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "it": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "ja": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "ko": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "ne": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "nl": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "pl": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "pt": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "ru": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "tr": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "uk": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "vi": [ @@ -131,13 +149,15 @@ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ], "zh": [ "enable_connect", "enable_connect_description", "devices", - "select" + "select", + "connect_client_alert" ] }