feat: add connect confirmation dialog

This commit is contained in:
Kingkor Roy Tirtho 2025-04-27 22:39:16 +06:00
parent 7c26d29d06
commit a06614bc5c
36 changed files with 471 additions and 42 deletions

View File

@ -426,5 +426,7 @@
"custom": "Custom",
"add_custom_url": "Add custom URL",
"edit_port": "Edit port",
"port_helper_msg": "Default is -1 which indicates random number. If you've firewall configured, setting this is recommended."
"port_helper_msg": "Default is -1 which indicates random number. If you've firewall configured, setting this is recommended.",
"connect_request": "Allow {client} to connect?",
"connection_request_denied": "Connection denied. User denied access."
}

View File

@ -2716,6 +2716,18 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.'**
String get port_helper_msg;
/// No description provided for @connect_request.
///
/// In en, this message translates to:
/// **'Allow {client} to connect?'**
String connect_request(Object client);
/// No description provided for @connection_request_denied.
///
/// In en, this message translates to:
/// **'Connection denied. User denied access.'**
String get connection_request_denied;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsBn extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsCa extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsCs extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsEu extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsKa extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsNe extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsTa extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsTl extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -1377,4 +1377,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get port_helper_msg => 'Default is -1 which indicates random number. If you\'ve firewall configured, setting this is recommended.';
@override
String connect_request(Object client) {
return 'Allow $client to connect?';
}
@override
String get connection_request_denied => 'Connection denied. User denied access.';
}

View File

@ -38,7 +38,23 @@ class SettingsPlaybackEditConnectPortDialog extends HookConsumerWidget {
validator: FormBuilderValidators.integer(radix: 10),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
// Allow only signed integers
TextInputFormatter.withFunction(
(oldValue, newValue) {
if (newValue.text.isEmpty) {
return const TextEditingValue();
}
if (newValue.text.length == 1 && newValue.text == "-") {
return newValue;
}
final intValue = int.tryParse(newValue.text);
if (intValue == null) {
return oldValue;
}
return newValue;
},
),
],
),
const Gap(5),

View File

@ -1,9 +1,13 @@
import 'dart:convert';
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/modules/player/volume_slider.dart';
import 'package:spotube/components/image/universal_image.dart';
@ -57,6 +61,7 @@ class ConnectControlPage extends HookConsumerWidget {
final resolvedService =
ref.watch(connectClientsProvider).asData?.value.resolvedService;
final connect = ref.watch(connectProvider);
final connectNotifier = ref.read(connectProvider.notifier);
final playlist = ref.watch(queueProvider);
final playing = ref.watch(playingProvider);
@ -69,12 +74,32 @@ class ConnectControlPage extends HookConsumerWidget {
}
});
useEffect(() {
if (connect.asData?.value == null) return null;
final subscription = connect.asData?.value?.stream.listen((message) {
final event = WebSocketEvent.fromJson(
jsonDecode(message),
(data) => data,
);
event.onError((event) {
if (event.data != "Connection denied") return;
if (!context.mounted) return;
context.back();
});
});
return () {
subscription?.cancel();
};
}, [connect.asData?.value]);
return SafeArea(
bottom: false,
child: Scaffold(
headers: [
TitleBar(
title: Text(resolvedService!.name),
title: Text(resolvedService?.name ?? ""),
)
],
child: LayoutBuilder(builder: (context, constrains) {
@ -247,7 +272,8 @@ class ConnectControlPage extends HookConsumerWidget {
),
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.next_track)).call,
child: Text(context.l10n.next_track))
.call,
child: IconButton.ghost(
icon: const Icon(SpotubeIcons.skipForward),
onPressed: playlist.activeTrack == null

View File

@ -1,6 +1,10 @@
import 'dart:convert';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
@ -46,15 +50,17 @@ final volumeProvider = StateProvider<double>(
(ref) => 1.0,
);
class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
typedef ConnectState = ({WebSocketChannel channel, Stream stream});
class ConnectNotifier extends AsyncNotifier<ConnectState?> {
@override
build() async {
try {
final connectClients = ref.watch(connectClientsProvider);
final connectClients = await ref.watch(connectClientsProvider.future);
if (connectClients.asData?.value.resolvedService == null) return null;
if (connectClients.resolvedService == null) return null;
final service = connectClients.asData!.value.resolvedService!;
final service = connectClients.resolvedService!;
AppLogger.log.t(
'♾️ Connecting to ${service.name}: ws://${service.host}:${service.port}/ws',
@ -70,7 +76,9 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
'✅ Connected to ${service.name}: ws://${service.host}:${service.port}/ws',
);
final subscription = channel.stream.listen(
final stream = channel.stream.asBroadcastStream();
final subscription = stream.listen(
(message) {
final event =
WebSocketEvent.fromJson(jsonDecode(message), (data) => data);
@ -102,6 +110,38 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
event.onVolume((event) {
ref.read(volumeProvider.notifier).state = event.data;
});
event.onError((event) {
if (event.data == "Connection denied") {
ref.read(connectClientsProvider.notifier).clearResolvedService();
if (rootNavigatorKey.currentContext?.mounted == true) {
final theme = Theme.of(rootNavigatorKey.currentContext!);
showToast(
context: rootNavigatorKey.currentContext!,
location: ToastLocation.topRight,
dismissible: true,
builder: (context, overlay) {
return SurfaceCard(
fillColor: theme.colorScheme.destructive,
filled: true,
child: Basic(
leading: const Icon(SpotubeIcons.error),
title: Text(
context.l10n.connection_request_denied,
style: theme.typography.normal.copyWith(
color: theme.colorScheme.destructiveForeground,
),
),
leadingAlignment: Alignment.center,
),
);
},
);
}
}
});
},
onError: (error) {
AppLogger.reportError(error, StackTrace.current);
@ -113,7 +153,7 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
channel.sink.close(status.goingAway);
});
return channel;
return (channel: channel, stream: stream);
} catch (e, stack) {
AppLogger.reportError(e, stack);
rethrow;
@ -122,7 +162,7 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
Future<void> emit(Object message) async {
if (state.value == null) return;
state.value?.sink.add(
state.value?.channel.sink.add(
message is String ? message : (message as dynamic).toJson(),
);
}
@ -184,7 +224,6 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
}
}
final connectProvider =
AsyncNotifierProvider<ConnectNotifier, WebSocketChannel?>(
final connectProvider = AsyncNotifierProvider<ConnectNotifier, ConnectState?>(
() => ConnectNotifier(),
);

View File

@ -3,9 +3,12 @@ import 'dart:convert';
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/provider/history/history.dart';
@ -43,6 +46,8 @@ class ServerConnectRoutes {
Stream<String> get connectClientStream =>
_connectClientStreamController.stream;
final List<String> _allowedConnections = [];
FutureOr<Response> websocket(Request req) {
return webSocketHandler(
(
@ -54,6 +59,47 @@ class ServerConnectRoutes {
final origin = "${context?.remoteAddress.host}:${context?.remotePort}";
_connectClientStreamController.add(origin);
// Confirm whether user allows to connect
if (rootNavigatorKey.currentContext?.mounted == true &&
_allowedConnections.contains(origin) == false) {
final confirmed = await showDialog<bool>(
context: rootNavigatorKey.currentContext!,
builder: (context) {
return AlertDialog(
title: Text(context.l10n.connect),
content: Text(
context.l10n.connect_request(origin),
),
actions: [
Button.secondary(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(context.l10n.decline),
),
Button.primary(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text(context.l10n.accept),
),
],
);
},
) ??
false;
if (confirmed) {
_allowedConnections.add(origin);
} else {
channel.sink.addEvent(
WebSocketErrorEvent("Connection denied"),
);
await channel.sink.close();
return;
}
}
ref.listen(
audioPlayerProvider,
(previous, next) {
@ -106,7 +152,7 @@ class ServerConnectRoutes {
},
),
channel.stream.listen(
(message) {
(message) async {
try {
final event = WebSocketEvent.fromJson(
jsonDecode(message),

View File

@ -1,141 +1,197 @@
{
"ar": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"bn": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"ca": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"cs": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"de": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"es": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"eu": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"fa": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"fi": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"fr": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"hi": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"id": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"it": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"ja": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"ka": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"ko": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"ne": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"nl": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"pl": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"pt": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"ru": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"ta": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"th": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"tl": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"tr": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"uk": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"vi": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
],
"zh": [
"edit_port",
"port_helper_msg"
"port_helper_msg",
"connect_request",
"connection_request_denied"
]
}