mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Settings working on front-end and with it the notifications list
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user