diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index 4b1a0f6..de55658 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -205,7 +205,7 @@ class APIClient { fn: User.fromJson, authToken: auth.getToken(), query: { - 'confirm': ['true'] + 'confirm': ['true'], }, ); } @@ -230,7 +230,7 @@ class APIClient { static Future updateClient(TokenSource auth, String clientID, {String? fcmToken, String? agentModel, String? name, String? agentVersion}) async { return await _request( name: 'updateClient', - method: 'PUT', + method: 'PATCH', relURL: 'users/${auth.getUserID()}/clients/$clientID', jsonBody: { if (fcmToken != null) 'fcm_token': fcmToken, @@ -259,7 +259,7 @@ class APIClient { method: 'GET', relURL: 'users/${auth.getUserID()}/channels', query: { - 'selector': [sel.apiKey] + 'selector': [sel.apiKey], }, fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels'] as List), authToken: auth.getToken(), diff --git a/flutter/lib/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart b/flutter/lib/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart index c7eba3a..473db4d 100644 --- a/flutter/lib/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart +++ b/flutter/lib/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart @@ -12,6 +12,7 @@ import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; +import 'package:simplecloudnotifier/utils/dialogs.dart'; import 'package:simplecloudnotifier/utils/ui.dart'; class ChannelScannerResultChannelSubscribe extends StatefulWidget { @@ -172,6 +173,38 @@ class _ChannelScannerResultChannelSubscribeState extends State(context, listen: false); + + // Check if username is set + try { + final user = await auth.loadUser(); + if (user.username == null || user.username!.isEmpty) { + // Show modal to set username + var newusername = await UIDialogs.showUsernameRequiredDialog(context); + + if (newusername == null) return; // User cancelled + + newusername = newusername.trim(); + if (newusername.isEmpty) { + Toaster.error("Error", 'Username cannot be empty'); + return; + } + + // Update username via API + try { + await APIClient.updateUser(auth, auth.userID!, username: newusername); + await auth.loadUser(force: true); // Refresh cached user + Toaster.success("Success", 'Username set'); + } catch (e) { + Toaster.error("Error", 'Failed to set username: ${e.toString()}'); + return; + } + } + } catch (e) { + Toaster.error("Error", 'Failed to load user data: ${e.toString()}'); + return; + } + + // Proceed with subscription try { var sub = await APIClient.subscribeToChannelbyID(auth, widget.value.channelID, subscribeKey: widget.value.subscribeKey); if (sub.confirmed) { diff --git a/flutter/lib/pages/debug/debug_actions.dart b/flutter/lib/pages/debug/debug_actions.dart index 4bad575..63014e8 100644 --- a/flutter/lib/pages/debug/debug_actions.dart +++ b/flutter/lib/pages/debug/debug_actions.dart @@ -5,6 +5,7 @@ import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/state/app_settings.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/utils/notifier.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/ui.dart'; @@ -65,22 +66,31 @@ class _DebugActionsPageState extends State { onPressed: _sendTokenToServer, text: 'Send FCM Token to Server', ), + SizedBox(height: 4), + UI.button( + big: false, + onPressed: _updateClient, + text: 'Update Client on Server', + ), SizedBox(height: 20), UI.button( big: false, onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, null), text: 'Show local notification (generic)', ), + SizedBox(height: 4), UI.button( big: false, onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 0), text: 'Show local notification (Prio = 0)', ), + SizedBox(height: 4), UI.button( big: false, onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 1), text: 'Show local notification (Prio = 1)', ), + SizedBox(height: 4), UI.button( big: false, onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 2), @@ -128,6 +138,26 @@ class _DebugActionsPageState extends State { } } + void _updateClient() async { + try { + final auth = AppAuth(); + + final clientID = auth.getClientID(); + if (clientID == null) { + Toaster.error("Error", "No Client set"); + return; + } + + final newClient = await APIClient.updateClient(auth, clientID, agentModel: Globals().deviceModel, name: Globals().nameForClient(), agentVersion: Globals().version); + auth.setClientAndClientID(newClient); + + Toaster.success("Success", "Client updated"); + } catch (exc, trace) { + Toaster.error("Error", "An error occurred while updating the client: ${exc.toString()}"); + ApplicationLog.error("An error occurred while updating the client: ${exc.toString()}", trace: trace); + } + } + void _copyToken() async { try { final fcmToken = await FirebaseMessaging.instance.getToken(); diff --git a/flutter/lib/pages/debug/debug_request_view.dart b/flutter/lib/pages/debug/debug_request_view.dart index 3e7909c..e70fceb 100644 --- a/flutter/lib/pages/debug/debug_request_view.dart +++ b/flutter/lib/pages/debug/debug_request_view.dart @@ -135,7 +135,7 @@ class _DebugRequestViewPageState extends State { void _copyCurl() { final method = '-X ${widget.request.method}'; final header = widget.request.requestHeaders.entries.map((v) => '-H "${v.key}: ${v.value}"').join(' '); - final body = widget.request.requestBody.isNotEmpty ? '-d "${widget.request.requestBody}"' : ''; + final body = widget.request.requestBody.isNotEmpty ? '-d \'${widget.request.requestBody}\'' : ''; final curlParts = ['curl', method, header, '"${widget.request.url}"', body]; diff --git a/flutter/lib/utils/dialogs.dart b/flutter/lib/utils/dialogs.dart index 83db8d4..f4cb36c 100644 --- a/flutter/lib/utils/dialogs.dart +++ b/flutter/lib/utils/dialogs.dart @@ -28,6 +28,40 @@ class UIDialogs { ); } + static Future showUsernameRequiredDialog(BuildContext context) { + var _textFieldController = TextEditingController(); + + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Username Required'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Please set a public username to subscribe to channels from other users.'), + SizedBox(height: 16), + TextField( + autofocus: true, + controller: _textFieldController, + decoration: InputDecoration(hintText: 'Enter username'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(_textFieldController.text), + child: Text('OK'), + ), + ], + ), + ); + } + static Future showConfirmDialog(BuildContext context, String title, {String? text, String? okText, String? cancelText}) { return showDialog( context: context, diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0069b6b..6bcf1a0 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: simplecloudnotifier description: "Receive push messages" publish_to: 'none' -version: 2.1.1+509 +version: 2.1.1+541 environment: sdk: '>=3.9.0 <4.0.0' diff --git a/scnserver/.gitignore b/scnserver/.gitignore index af90937..2028390 100644 --- a/scnserver/.gitignore +++ b/scnserver/.gitignore @@ -17,8 +17,8 @@ simple_cloud_notifier-*.sql identifier.sqlite .idea/dataSources.xml - .idea/copilot* +.idea/go.imports.xml .swaggobin