Settings working on front-end and with it the notifications list

This commit is contained in:
rzuasti
2026-03-03 12:20:57 -05:00
parent 1d029c8c5f
commit dc2521da65
5 changed files with 181 additions and 24 deletions
+6
View File
@@ -11,6 +11,7 @@ use log::{debug, error, info};
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use axum::Json;
pub mod devices; pub mod devices;
pub mod notifications; pub mod notifications;
@@ -26,6 +27,7 @@ pub async fn serve() -> Result<(), Box<dyn Error>> {
.allow_headers([http::header::AUTHORIZATION, http::header::CONTENT_TYPE]); .allow_headers([http::header::AUTHORIZATION, http::header::CONTENT_TYPE]);
let router = Router::new() let router = Router::new()
.route("/api/test", get(test_api))
.route("/api/devices", get(devices::list)) .route("/api/devices", get(devices::list))
.route("/api/devices", put(devices::register)) .route("/api/devices", put(devices::register))
.route("/api/devices/{mac_address}", delete(devices::unregister)) .route("/api/devices/{mac_address}", delete(devices::unregister))
@@ -60,6 +62,10 @@ pub async fn serve() -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
async fn test_api() -> Result<Json<String>, StatusCode> {
Ok(Json("OOTT_API_OK".to_string()))
}
async fn auth(request: Request, next: Next) -> Result<Response, StatusCode> { async fn auth(request: Request, next: Next) -> Result<Response, StatusCode> {
let auth_header = request let auth_header = request
.headers() .headers()
+1 -1
View File
@@ -19,7 +19,7 @@ final class MainApp extends StatelessWidget {
title: 'OOTT', title: 'OOTT',
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepOrange, seedColor: Color.fromARGB(255, 214, 93, 14),
brightness: Brightness.dark, brightness: Brightness.dark,
), ),
), ),
+108 -17
View File
@@ -1,7 +1,10 @@
import 'dart:io';
import 'package:encrypter/encrypter/xor.dart'; import 'package:encrypter/encrypter/xor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../utils/oott_api.dart'; import '../utils/oott_api.dart';
import '../utils/pref_utils.dart'; import '../utils/pref_utils.dart';
import '../utils/ui_snackbars.dart';
class Settings extends StatefulWidget { class Settings extends StatefulWidget {
@override @override
@@ -10,9 +13,12 @@ class Settings extends StatefulWidget {
class _SettingsState extends State<Settings> { class _SettingsState extends State<Settings> {
bool _isLoading = true; bool _isLoading = true;
bool _isSaving = false;
final _baseUrlController = TextEditingController(); final _baseUrlController = TextEditingController();
final _apiKeyController = TextEditingController(); final _apiKeyController = TextEditingController();
bool _apiKeyVisible = false; bool _apiKeyVisible = false;
bool _testOk = false;
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
@override @override
@@ -37,6 +43,11 @@ class _SettingsState extends State<Settings> {
setState(() {}); setState(() {});
} }
void _saveData() {
PrefUtil.setValue("base_url", _baseUrlController.text);
PrefUtil.setValue("api_key", XOR().xorEncode(_apiKeyController.text));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -48,8 +59,20 @@ class _SettingsState extends State<Settings> {
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
SizedBox(height: 16), SizedBox(height: 16),
// Base URL
TextFormField( TextFormField(
controller: _baseUrlController, 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( decoration: const InputDecoration(
border: UnderlineInputBorder(), border: UnderlineInputBorder(),
labelText: 'Base URL of your OOTT server\'s API', labelText: 'Base URL of your OOTT server\'s API',
@@ -57,12 +80,25 @@ class _SettingsState extends State<Settings> {
), ),
), ),
SizedBox(height: 16), SizedBox(height: 16),
// API Key
TextFormField( TextFormField(
controller: _apiKeyController, 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, obscureText: !_apiKeyVisible,
decoration: InputDecoration( decoration: InputDecoration(
border: UnderlineInputBorder(), border: UnderlineInputBorder(),
labelText: 'API Key', labelText: 'API key',
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(
_apiKeyVisible _apiKeyVisible
@@ -78,23 +114,78 @@ class _SettingsState extends State<Settings> {
), ),
), ),
SizedBox(height: 16), SizedBox(height: 16),
ElevatedButton.icon( // Button row
onPressed: () { Row(
showDialog( mainAxisAlignment: MainAxisAlignment.end,
context: context, children: [
builder: (context) { // Test button
return AlertDialog( ElevatedButton.icon(
content: Text( onPressed: () async {
_baseUrlController.text + if (_formKey.currentState!.validate()) {
' - ' + String? testResult = await BackendAPI.test(
_apiKeyController.text, _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: _testOk
label: Text('Test'), ? Icon(Icons.check)
icon: const 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,
),
),
],
), ),
], ],
), ),
+35 -6
View File
@@ -11,12 +11,9 @@ class BackendAPI {
static BackendAPI get instance => _instance; static BackendAPI get instance => _instance;
BackendAPI._internal() { BackendAPI._internal() {
// _baseUrl = _baseUrl =
// PrefUtil.getValue("base_url", "http://localhost:3000/api") as String; PrefUtil.getValue("base_url", "http://localhost:3000/api") as String;
// _apiKey = XOR().xorDecode(PrefUtil.getValue("api_key", "") as String); _apiKey = XOR().xorDecode(PrefUtil.getValue("api_key", "") as String);
_baseUrl = "http://localhost:3000/api";
_apiKey = "super_secret";
print('Base URL: $_baseUrl'); print('Base URL: $_baseUrl');
print('API KEY $_apiKey'); 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<String?> 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 _baseUrl;
late String _apiKey; late String _apiKey;
late Dio _dio; late Dio _dio;
+31
View File
@@ -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,
),
);
}
}