feat: show alert when new client connects

This commit is contained in:
Kingkor Roy Tirtho 2024-03-28 21:38:12 +06:00
parent 2f3a2e671d
commit 0ed358eeb8
4 changed files with 265 additions and 209 deletions

View File

@ -317,5 +317,6 @@
"enable_connect": "Enable Connect", "enable_connect": "Enable Connect",
"enable_connect_description": "Control Spotube from other devices", "enable_connect_description": "Control Spotube from other devices",
"devices": "Devices", "devices": "Devices",
"select": "Select" "select": "Select",
"connect_client_alert": "You're being controlled by {client}"
} }

View File

@ -16,6 +16,7 @@ import 'package:spotube/components/root/spotube_navigation_bar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/configurators/use_endless_playback.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart';
import 'package:spotube/hooks/configurators/use_update_checker.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/download_manager_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/services/connectivity_adapter.dart'; import 'package:spotube/services/connectivity_adapter.dart';
@ -54,50 +55,75 @@ class RootApp extends HookConsumerWidget {
} }
}); });
final subscription = ConnectionCheckerService final subscriptions = [
.instance.onConnectivityChanged ConnectionCheckerService.instance.onConnectivityChanged
.listen((status) { .listen((status) {
if (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( scaffoldMessenger.showSnackBar(
SnackBar( SnackBar(
backgroundColor: Colors.yellow[600],
behavior: SnackBarBehavior.floating,
content: Row( content: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( const Icon(
SpotubeIcons.wifi, SpotubeIcons.error,
color: theme.colorScheme.onPrimary, color: Colors.black,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(context.l10n.connection_restored), Text(
], context.l10n.connect_client_alert(clientOrigin),
), style: const TextStyle(color: Colors.black),
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,
), ),
); );
} })
}); ];
return () { return () {
subscription.cancel(); for (final subscription in subscriptions) {
subscription.cancel();
}
}; };
}, []); }, []);

View File

@ -22,6 +22,9 @@ import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/provider/volume_provider.dart';
final logger = getLogger('ConnectServer'); final logger = getLogger('ConnectServer');
final _connectClientStreamController = StreamController<String>.broadcast();
Stream<String> get connectClientStream => _connectClientStreamController.stream;
final connectServerProvider = FutureProvider((ref) async { final connectServerProvider = FutureProvider((ref) async {
final enabled = final enabled =
@ -45,169 +48,175 @@ final connectServerProvider = FutureProvider((ref) async {
final subscriptions = <StreamSubscription>[]; final subscriptions = <StreamSubscription>[];
final websocket = webSocketHandler( FutureOr<Response> websocket(Request req) => webSocketHandler(
(WebSocketChannel channel, String? protocol) async { (WebSocketChannel channel, String? protocol) async {
ref.listen( final context =
ProxyPlaylistNotifier.provider, (req.context["shelf.io.connection_info"] as HttpConnectionInfo?);
(previous, next) { final origin =
channel.sink.add( "${context?.remoteAddress.host}:${context?.remotePort}";
WebSocketQueueEvent(next).toJson(), _connectClientStreamController.add(origin);
);
},
fireImmediately: true,
);
// because audioPlayer events doesn't fireImmediately ref.listen(
channel.sink.add( ProxyPlaylistNotifier.provider,
WebSocketPlayingEvent(audioPlayer.isPlaying).toJson(), (previous, next) {
); channel.sink.add(
channel.sink.add( WebSocketQueueEvent(next).toJson(),
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,
); );
},
fireImmediately: true,
);
event.onLoad((event) async { // because audioPlayer events doesn't fireImmediately
await playbackNotifier.load( channel.sink.add(
event.data.tracks, WebSocketPlayingEvent(audioPlayer.isPlaying).toJson(),
autoPlay: true, );
initialIndex: event.data.initialIndex ?? 0, 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) { event.onLoad((event) async {
playbackNotifier.addCollection(event.data.collectionId!); 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());
} }
}); },
onDone: () {
event.onPause((event) async { logger.i('Connection closed');
await audioPlayer.pause(); },
}); ),
]);
event.onResume((event) async { },
await audioPlayer.resume(); )(req);
});
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');
},
),
]);
},
);
final port = Random().nextInt(17000) + 3000; final port = Random().nextInt(17000) + 3000;

View File

@ -3,126 +3,144 @@
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"bn": [ "bn": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"ca": [ "ca": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"de": [ "de": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"es": [ "es": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"fa": [ "fa": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"fr": [ "fr": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"hi": [ "hi": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"it": [ "it": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"ja": [ "ja": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"ko": [ "ko": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"ne": [ "ne": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"nl": [ "nl": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"pl": [ "pl": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"pt": [ "pt": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"ru": [ "ru": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"tr": [ "tr": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"uk": [ "uk": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"vi": [ "vi": [
@ -131,13 +149,15 @@
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
], ],
"zh": [ "zh": [
"enable_connect", "enable_connect",
"enable_connect_description", "enable_connect_description",
"devices", "devices",
"select" "select",
"connect_client_alert"
] ]
} }