From dc2521da655cd59e780532aa56bed4750af368d8 Mon Sep 17 00:00:00 2001 From: rzuasti Date: Tue, 3 Mar 2026 12:20:57 -0500 Subject: [PATCH] Settings working on front-end and with it the notifications list --- backend/src/web_server.rs | 6 ++ frontend/lib/main.dart | 2 +- frontend/lib/settings/settings.dart | 125 +++++++++++++++++++++++---- frontend/lib/utils/oott_api.dart | 41 +++++++-- frontend/lib/utils/ui_snackbars.dart | 31 +++++++ 5 files changed, 181 insertions(+), 24 deletions(-) create mode 100644 frontend/lib/utils/ui_snackbars.dart diff --git a/backend/src/web_server.rs b/backend/src/web_server.rs index c8760eb..6543243 100644 --- a/backend/src/web_server.rs +++ b/backend/src/web_server.rs @@ -11,6 +11,7 @@ use log::{debug, error, info}; use tower::ServiceBuilder; use tower_http::cors::{Any, CorsLayer}; use tower_http::services::ServeDir; +use axum::Json; pub mod devices; pub mod notifications; @@ -26,6 +27,7 @@ pub async fn serve() -> Result<(), Box> { .allow_headers([http::header::AUTHORIZATION, http::header::CONTENT_TYPE]); let router = Router::new() + .route("/api/test", get(test_api)) .route("/api/devices", get(devices::list)) .route("/api/devices", put(devices::register)) .route("/api/devices/{mac_address}", delete(devices::unregister)) @@ -60,6 +62,10 @@ pub async fn serve() -> Result<(), Box> { Ok(()) } +async fn test_api() -> Result, StatusCode> { + Ok(Json("OOTT_API_OK".to_string())) +} + async fn auth(request: Request, next: Next) -> Result { let auth_header = request .headers() diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 880e668..f2fa223 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -19,7 +19,7 @@ final class MainApp extends StatelessWidget { title: 'OOTT', theme: ThemeData( colorScheme: ColorScheme.fromSeed( - seedColor: Colors.deepOrange, + seedColor: Color.fromARGB(255, 214, 93, 14), brightness: Brightness.dark, ), ), diff --git a/frontend/lib/settings/settings.dart b/frontend/lib/settings/settings.dart index 2172bd6..afe760b 100644 --- a/frontend/lib/settings/settings.dart +++ b/frontend/lib/settings/settings.dart @@ -1,7 +1,10 @@ +import 'dart:io'; + import 'package:encrypter/encrypter/xor.dart'; import 'package:flutter/material.dart'; import '../utils/oott_api.dart'; import '../utils/pref_utils.dart'; +import '../utils/ui_snackbars.dart'; class Settings extends StatefulWidget { @override @@ -10,9 +13,12 @@ class Settings extends StatefulWidget { class _SettingsState extends State { bool _isLoading = true; + bool _isSaving = false; final _baseUrlController = TextEditingController(); final _apiKeyController = TextEditingController(); bool _apiKeyVisible = false; + bool _testOk = false; + final _formKey = GlobalKey(); @override @@ -37,6 +43,11 @@ class _SettingsState extends State { setState(() {}); } + void _saveData() { + PrefUtil.setValue("base_url", _baseUrlController.text); + PrefUtil.setValue("api_key", XOR().xorEncode(_apiKeyController.text)); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -48,8 +59,20 @@ class _SettingsState extends State { child: Column( children: [ SizedBox(height: 16), + // Base URL TextFormField( controller: _baseUrlController, + onChanged: (text) { + setState(() { + _testOk = false; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return "The URL cannot be empty"; + } + return null; + }, decoration: const InputDecoration( border: UnderlineInputBorder(), labelText: 'Base URL of your OOTT server\'s API', @@ -57,12 +80,25 @@ class _SettingsState extends State { ), ), SizedBox(height: 16), + // API Key TextFormField( controller: _apiKeyController, + onChanged: (text) { + setState(() { + _testOk = false; + }); + }, + + validator: (value) { + if (value == null || value.isEmpty) { + return "The API key cannot be empty"; + } + return null; + }, obscureText: !_apiKeyVisible, decoration: InputDecoration( border: UnderlineInputBorder(), - labelText: 'API Key', + labelText: 'API key', suffixIcon: IconButton( icon: Icon( _apiKeyVisible @@ -78,23 +114,78 @@ class _SettingsState extends State { ), ), SizedBox(height: 16), - ElevatedButton.icon( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: Text( - _baseUrlController.text + - ' - ' + - _apiKeyController.text, - ), - ); + // Button row + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Test button + ElevatedButton.icon( + onPressed: () async { + if (_formKey.currentState!.validate()) { + String? testResult = await BackendAPI.test( + _baseUrlController.text, + _apiKeyController.text, + ); + + setState(() { + _testOk = testResult == null; + }); + + if (testResult == null) { + UISnackbars.showSuccess(context, 'It works!'); + } else { + UISnackbars.showError(context, testResult); + } + } }, - ); - }, - label: Text('Test'), - icon: const Icon(Icons.check), + label: Text('Test'), + icon: _testOk + ? Icon(Icons.check) + : Icon(Icons.play_arrow), + style: ElevatedButton.styleFrom( + backgroundColor: _testOk + ? Colors.lightGreen + : Theme.of(context).colorScheme.secondary, + foregroundColor: _testOk + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSecondary, + ), + ), + SizedBox(width: 8), + + // Save button + ElevatedButton.icon( + onPressed: ((!_testOk) || _isSaving) + ? null + : () async { + if (_formKey.currentState!.validate()) { + setState(() { + _isSaving = true; + }); + _saveData(); + + setState(() { + _isSaving = false; + }); + UISnackbars.showSuccess( + context, + 'Settings saved successfully', + ); + } + }, + label: Text(_isSaving ? 'Saving...' : 'Save'), + + icon: _isSaving ? null : Icon(Icons.save), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.primary, + foregroundColor: Theme.of( + context, + ).colorScheme.onPrimary, + ), + ), + ], ), ], ), diff --git a/frontend/lib/utils/oott_api.dart b/frontend/lib/utils/oott_api.dart index d4640d5..0b40a76 100644 --- a/frontend/lib/utils/oott_api.dart +++ b/frontend/lib/utils/oott_api.dart @@ -11,12 +11,9 @@ class BackendAPI { static BackendAPI get instance => _instance; BackendAPI._internal() { - // _baseUrl = - // PrefUtil.getValue("base_url", "http://localhost:3000/api") as String; - // _apiKey = XOR().xorDecode(PrefUtil.getValue("api_key", "") as String); - - _baseUrl = "http://localhost:3000/api"; - _apiKey = "super_secret"; + _baseUrl = + PrefUtil.getValue("base_url", "http://localhost:3000/api") as String; + _apiKey = XOR().xorDecode(PrefUtil.getValue("api_key", "") as String); print('Base URL: $_baseUrl'); print('API KEY $_apiKey'); @@ -32,6 +29,38 @@ class BackendAPI { ); } + // Returns null if the test was successful, and a String with a message about the issue if not + static Future test(String baseUrl, String apiKey) async { + print('About to test API with baseUrl=$baseUrl and apiKey=$apiKey'); + Dio dio = Dio( + BaseOptions( + baseUrl: baseUrl, + headers: { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: 'Bearer $apiKey', + }, + ), + ); + + try { + Response response = await dio.get('/test'); + return response.toString().contains('OOTT_API_OK') + ? null + : "URL successfully called but didn't return the expected value. Check your URL and make sure it points to your OOTT backend base URL."; + } on DioException catch (e) { + if (e.response != null) { + if (e.response!.statusCode == 401) { + return "Authorization failed, check your API Key."; + } else { + return "Error querying the given URL (${e.response!.statusCode} - ${e.message ?? 'N/A'})"; + } + } else { + // Response was null, something happened while sending the message + return "Error sending message to provided URL (${e.message ?? 'no message'})"; + } + } + } + late String _baseUrl; late String _apiKey; late Dio _dio; diff --git a/frontend/lib/utils/ui_snackbars.dart b/frontend/lib/utils/ui_snackbars.dart new file mode 100644 index 0000000..22e0cb8 --- /dev/null +++ b/frontend/lib/utils/ui_snackbars.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class UISnackbars { + static void showError(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + style: TextStyle(color: Theme.of(context).colorScheme.onError), + ), + behavior: SnackBarBehavior.floating, + showCloseIcon: true, + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + + static void showSuccess(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + style: TextStyle(color: Theme.of(context).colorScheme.onPrimary), + ), + behavior: SnackBarBehavior.floating, + showCloseIcon: true, + backgroundColor: Colors.lightGreen, + ), + ); + } +}