Initial
This commit is contained in:
152
lib/BlindMasterScreens/Startup/create_user_screen.dart
Normal file
152
lib/BlindMasterScreens/Startup/create_user_screen.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
|
||||
import 'package:blind_master/BlindMasterResources/title_text.dart';
|
||||
|
||||
class CreateUserScreen extends StatefulWidget {
|
||||
const CreateUserScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CreateUserScreen> createState() => _CreateUserScreenState();
|
||||
}
|
||||
|
||||
class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
|
||||
final emailController = TextEditingController();
|
||||
final nameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
final _passFormKey = GlobalKey<FormState>();
|
||||
final _emailFormKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> attemptCreate() async{
|
||||
try {
|
||||
if (!_emailFormKey.currentState!.validate() || !_passFormKey.currentState!.validate()) {
|
||||
throw Exception('Invalid information entered!');
|
||||
}
|
||||
|
||||
final payload = {
|
||||
'name': nameController.text.trim(),
|
||||
'email': emailController.text.trim(),
|
||||
'password': passwordController.text,
|
||||
};
|
||||
|
||||
final response = await regularPost(payload, 'create_user');
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
if(mounted) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.green[800],
|
||||
content: Text(
|
||||
"Account Successfully Created!",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
)
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} else {
|
||||
if (response.statusCode == 409) throw Exception('Email Already In Use!');
|
||||
throw Exception('Create failed: ${response.statusCode}');
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
errorSnackbar(e)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
String? confirmPasswordValidator(String? input) {
|
||||
if (input == passwordController.text) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return "Passwords do not match!";
|
||||
}
|
||||
}
|
||||
|
||||
String? emailValidator(String? input) {
|
||||
if (input == null || input.isEmpty) {
|
||||
return 'Email is required';
|
||||
}
|
||||
|
||||
final emailPattern = r'^[^@]+@[^@]+\.[^@]+$';
|
||||
|
||||
if (!RegExp(emailPattern).hasMatch(input)) {
|
||||
return 'Enter a valid email address';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TitleText("Create Account", txtClr: Colors.white),
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(40, 10, 40, 10),
|
||||
child: Column(
|
||||
children: [
|
||||
BlindMasterMainInput("Preferred Name (optional)", controller: nameController),
|
||||
SizedBox(height: 20),
|
||||
Form(
|
||||
key: _emailFormKey,
|
||||
autovalidateMode: AutovalidateMode.onUnfocus,
|
||||
child: BlindMasterMainInput(
|
||||
"Email",
|
||||
controller: emailController,
|
||||
validator: emailValidator,
|
||||
),
|
||||
),
|
||||
Form(
|
||||
key: _passFormKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
BlindMasterMainInput(
|
||||
"Password",
|
||||
password: true,
|
||||
controller: passwordController
|
||||
),
|
||||
BlindMasterMainInput(
|
||||
"Confirm Password",
|
||||
validator: confirmPasswordValidator,
|
||||
password: true,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: attemptCreate,
|
||||
child: Text(
|
||||
"Create"
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/BlindMasterScreens/Startup/login_screen.dart
Normal file
126
lib/BlindMasterScreens/Startup/login_screen.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/Startup/create_user_screen.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/home_screen.dart';
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterResources/fade_transition.dart';
|
||||
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
|
||||
import 'package:blind_master/BlindMasterResources/title_text.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final emailController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
bool inputWrong = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up the controller when the widget is removed from the
|
||||
// widget tree.
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> attemptSignIn() async {
|
||||
final email = emailController.text.trim();
|
||||
final password = passwordController.text;
|
||||
|
||||
final payload = {
|
||||
'email': email,
|
||||
'password': password,
|
||||
}; // query parameters
|
||||
|
||||
try {
|
||||
final response = await regularPost(payload, 'login');
|
||||
if (response.statusCode != 200) {
|
||||
if (response.statusCode == 400) {throw Exception('Email and Password Necessary');}
|
||||
else if (response.statusCode == 500) {throw Exception('Server Error');}
|
||||
else {throw Exception('Incorrect email or password');}
|
||||
}
|
||||
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
final token = body['token'] as String;
|
||||
|
||||
if (token.isEmpty) throw Exception('Token Not Received');
|
||||
final storage = FlutterSecureStorage();
|
||||
await storage.write(key: 'token', value: token);
|
||||
|
||||
} catch(e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
errorSnackbar(e)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
fadeTransition(HomeScreen()),
|
||||
);
|
||||
}
|
||||
|
||||
void switchToCreate() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => CreateUserScreen()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TitleText("BlindMaster"),
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(40, 10, 40, 10),
|
||||
child: Column(
|
||||
children: [
|
||||
BlindMasterMainInput("Email", controller: emailController),
|
||||
BlindMasterMainInput("Password", controller: passwordController, password: true,),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.only(bottom:10),
|
||||
child: ElevatedButton(
|
||||
onPressed: attemptSignIn,
|
||||
child: Text(
|
||||
"Log In"
|
||||
),
|
||||
)
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: switchToCreate,
|
||||
child: Text(
|
||||
"Create Account"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/BlindMasterScreens/Startup/splash_screen.dart
Normal file
89
lib/BlindMasterScreens/Startup/splash_screen.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/home_screen.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/Startup/login_screen.dart';
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterResources/fade_transition.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
Widget nextScreen = LoginScreen();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_routeNext();
|
||||
}
|
||||
|
||||
Future<void> _routeNext() async {
|
||||
await verifyToken();
|
||||
_animateBackgroundBasedOnTime();
|
||||
}
|
||||
|
||||
Future<void> verifyToken() async{
|
||||
final storage = FlutterSecureStorage();
|
||||
|
||||
try {
|
||||
http.Response? response = await secureGet('verify');
|
||||
if (response == null) {
|
||||
nextScreen = LoginScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
final newToken = body['token'];
|
||||
|
||||
if (newToken != null) {
|
||||
await storage.write(key: 'token', value: newToken); // ✅ Rotate
|
||||
}
|
||||
|
||||
nextScreen = HomeScreen();
|
||||
} else {
|
||||
nextScreen = LoginScreen();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
errorSnackbar(e)
|
||||
);
|
||||
nextScreen = LoginScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void _animateBackgroundBasedOnTime() {
|
||||
|
||||
// Optionally navigate to your main app after a short delay
|
||||
Timer(const Duration(seconds: 3), () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
// MaterialPageRoute(builder: (context) => const MainAppScreen())
|
||||
fadeTransition(nextScreen),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
child: Center(
|
||||
child: Image.asset('assets/images/2xwhite.png', height: 250)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
191
lib/BlindMasterScreens/addingDevices/add_device.dart
Normal file
191
lib/BlindMasterScreens/addingDevices/add_device.dart
Normal file
@@ -0,0 +1,191 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/addingDevices/device_setup.dart';
|
||||
import 'package:blind_master/utils_from_FBPExample/extra.dart';
|
||||
import 'package:blind_master/utils_from_FBPExample/scan_result_tile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class AddDevice extends StatefulWidget {
|
||||
const AddDevice({super.key});
|
||||
|
||||
@override
|
||||
State<AddDevice> createState() => _AddDeviceState();
|
||||
}
|
||||
|
||||
class _AddDeviceState extends State<AddDevice> {
|
||||
List<ScanResult> _scanResults = [];
|
||||
bool _isScanning = false;
|
||||
|
||||
late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
|
||||
late StreamSubscription<bool> _isScanningSubscription;
|
||||
|
||||
BluetoothAdapterState _adapterState = BluetoothAdapterState.unknown;
|
||||
late StreamSubscription<BluetoothAdapterState> _adapterStateSubscription;
|
||||
bool _isConnecting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initBluetoothandStartScan();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scanResultsSubscription.cancel();
|
||||
_isScanningSubscription.cancel();
|
||||
_adapterStateSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _startScan() async {
|
||||
try {
|
||||
await FlutterBluePlus.startScan(
|
||||
timeout: const Duration(seconds: 15),
|
||||
withServices: [
|
||||
Guid("181C"),
|
||||
],
|
||||
webOptionalServices: [
|
||||
Guid("181C"), // user input
|
||||
],
|
||||
);
|
||||
} catch (e, backtrace) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
print("backtrace: $backtrace");
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
|
||||
Future<void> initBluetoothandStartScan() async {
|
||||
|
||||
_adapterStateSubscription = FlutterBluePlus.adapterState.listen((state) {
|
||||
if (mounted) {
|
||||
setState(() => _adapterState = state);
|
||||
if (state == BluetoothAdapterState.on) {
|
||||
_startScan();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_scanResultsSubscription = FlutterBluePlus.scanResults.listen((results) {
|
||||
if (mounted) {
|
||||
setState(() => _scanResults = results);
|
||||
// setState(() => _scanResults = results.where((r) => r.device.platformName == "BlindMaster").toList());
|
||||
}
|
||||
}, onError: (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
errorSnackbar(e)
|
||||
);
|
||||
});
|
||||
|
||||
_isScanningSubscription = FlutterBluePlus.isScanning.listen((state) {
|
||||
if (mounted) {
|
||||
setState(() => _isScanning = state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future onConnectPressed(BluetoothDevice device) async {
|
||||
if (_isConnecting) return;
|
||||
_isConnecting = true;
|
||||
|
||||
await device.connectAndUpdateStream().catchError((e) {
|
||||
if(!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
});
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => DeviceSetup(device: device))
|
||||
).then((_) {
|
||||
device.disconnectAndUpdateStream().catchError((e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
});
|
||||
_isConnecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future onRefresh() async {
|
||||
if (_isScanning == false) {
|
||||
await _startScan();
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
return Future.delayed(Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
Widget _buildScanResultTiles() {
|
||||
final res = _scanResults.where((r) => r.advertisementData.advName == "BlindMaster Device" && r.rssi > -55);
|
||||
return (res.isNotEmpty)
|
||||
? ListView(
|
||||
children: [
|
||||
...res.map((r) => ScanResultTile(result: r, onTap: () => onConnectPressed(r.device)))
|
||||
],
|
||||
)
|
||||
: _isScanning ? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
"Nothing Yet...",
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"No BlindMaster devices found nearby",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"Add a Device",
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
backgroundColor: Theme.of(context).primaryColorLight,
|
||||
),
|
||||
body: _adapterState != BluetoothAdapterState.on
|
||||
? SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"Bluetooth is off.\nPlease turn it on to scan for devices.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
)
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
child: _buildScanResultTiles()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
329
lib/BlindMasterScreens/addingDevices/device_setup.dart
Normal file
329
lib/BlindMasterScreens/addingDevices/device_setup.dart
Normal file
@@ -0,0 +1,329 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/addingDevices/set_device_name.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class DeviceSetup extends StatefulWidget {
|
||||
final BluetoothDevice device;
|
||||
|
||||
const DeviceSetup({super.key, required this.device});
|
||||
|
||||
@override
|
||||
State<DeviceSetup> createState() => _DeviceSetupState();
|
||||
}
|
||||
|
||||
class _DeviceSetupState extends State<DeviceSetup> {
|
||||
List<BluetoothService> _services = [];
|
||||
|
||||
List<String> openNetworks = [];
|
||||
List<String> pskNetworks = [];
|
||||
|
||||
late StreamSubscription<List<int>> _ssidSub;
|
||||
StreamSubscription<List<int>>? _confirmSub;
|
||||
|
||||
Widget? wifiList;
|
||||
String? message;
|
||||
String? password;
|
||||
|
||||
final passControl = TextEditingController();
|
||||
|
||||
@override void initState() {
|
||||
super.initState();
|
||||
initSetup();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ssidSub.cancel();
|
||||
_confirmSub?.cancel();
|
||||
passControl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future setWifiListListener(BluetoothCharacteristic ssidListChar) async {
|
||||
setState(() {
|
||||
wifiList = null;
|
||||
});
|
||||
await ssidListChar.setNotifyValue(true);
|
||||
|
||||
_ssidSub = ssidListChar.onValueReceived.listen((List<int> value) {
|
||||
List<String> ssidList = [];
|
||||
bool noNetworks = false;
|
||||
|
||||
try {
|
||||
final val = utf8.decode(value);
|
||||
if (val == ';') noNetworks = true;
|
||||
ssidList = val
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
openNetworks = ssidList
|
||||
.where((s) => s.split(',')[1] == "OPEN")
|
||||
.map((s) => s.split(',')[0])
|
||||
.toList();
|
||||
pskNetworks = ssidList
|
||||
.where((s) => s.split(',')[1] == "SECURED")
|
||||
.map((s) => s.split(',')[0])
|
||||
.toList();
|
||||
} catch (e) {
|
||||
if(!mounted)return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
|
||||
setState(() {
|
||||
wifiList = noNetworks
|
||||
? SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"No networks found...",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ssidList.isNotEmpty
|
||||
? ListView(
|
||||
children: [
|
||||
...buildSSIDs()
|
||||
],
|
||||
)
|
||||
: null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> buildSSIDs() {
|
||||
List<Widget> open = openNetworks.map((s) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.wifi),
|
||||
title: Text(s),
|
||||
trailing: const Icon(Icons.arrow_forward_ios_rounded),
|
||||
onTap: () {
|
||||
openConnect(s);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
List<Widget> secure = pskNetworks.map((s) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.wifi_password),
|
||||
title: Text(s),
|
||||
trailing: const Icon(Icons.arrow_forward_ios_rounded),
|
||||
onTap: () {
|
||||
setPassword(s);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
return open + secure;
|
||||
}
|
||||
|
||||
Future openConnect(String ssid) async {
|
||||
await transmitWiFiDetails(ssid, "");
|
||||
}
|
||||
|
||||
Future discoverServices() async{
|
||||
try {
|
||||
_services = await widget.device.discoverServices();
|
||||
} catch (e) {
|
||||
if(!mounted)return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
errorSnackbar(e)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_services = _services.where((s) => s.uuid.str.toUpperCase() == "181C").toList();
|
||||
if (_services.length != 1) throw Exception("Invalid Bluetooth Broadcast");
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future initSetup() async {
|
||||
await discoverServices();
|
||||
final ssidListChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0000");
|
||||
await setWifiListListener(ssidListChar);
|
||||
refreshWifiList();
|
||||
}
|
||||
|
||||
Future setPassword(String ssid) async {
|
||||
String? password = await showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
ssid,
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
content: Form(
|
||||
autovalidateMode: AutovalidateMode.onUnfocus,
|
||||
child: TextFormField(
|
||||
controller: passControl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(hintText: "Enter password"),
|
||||
validator: (value) {
|
||||
if (value == null) return "null input";
|
||||
if (value.length < 8) return "not long enough!";
|
||||
return null;
|
||||
},
|
||||
textInputAction: TextInputAction.send,
|
||||
onFieldSubmitted: (value) => Navigator.pop(dialogContext, passControl.text),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
passControl.clear();
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext, passControl.text);
|
||||
passControl.clear();
|
||||
},
|
||||
child: const Text("Connect"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await transmitWiFiDetails(ssid, password);
|
||||
}
|
||||
|
||||
Future transmitWiFiDetails(String ssid, String? password) async {
|
||||
if (password == null) return;
|
||||
|
||||
setState(() {
|
||||
wifiList = null;
|
||||
message = "Attempting Connection...";
|
||||
});
|
||||
|
||||
final ssidEntryChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0001");
|
||||
try {
|
||||
try {
|
||||
await ssidEntryChar.write(utf8.encode(ssid), withoutResponse: ssidEntryChar.properties.writeWithoutResponse);
|
||||
} catch (e) {
|
||||
throw Exception("SSID Write Error");
|
||||
}
|
||||
} catch (e){
|
||||
if(!mounted)return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
refreshWifiList();
|
||||
return;
|
||||
}
|
||||
|
||||
final passEntryChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0002");
|
||||
try {
|
||||
try {
|
||||
await passEntryChar.write(utf8.encode(password), withoutResponse: passEntryChar.properties.writeWithoutResponse);
|
||||
} catch (e) {
|
||||
throw Exception("Password Write Error");
|
||||
}
|
||||
} catch (e){
|
||||
if(!mounted)return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
refreshWifiList();
|
||||
return;
|
||||
}
|
||||
|
||||
final connectConfirmChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0005");
|
||||
final tokenEntryChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0003");
|
||||
await connectConfirmChar.setNotifyValue(true);
|
||||
_confirmSub = connectConfirmChar.onValueReceived.listen((List<int> connectVal) {
|
||||
try {
|
||||
final connectResponse = utf8.decode(connectVal);
|
||||
if (connectResponse == "Connected") {
|
||||
if (!mounted) return;
|
||||
_confirmSub?.cancel();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SetDeviceName(tokenEntryChar: tokenEntryChar, device: widget.device),
|
||||
)
|
||||
).then((_) {
|
||||
if (widget.device.isConnected) {
|
||||
refreshWifiList();
|
||||
}
|
||||
});
|
||||
} else if (connectResponse == "Error") {
|
||||
_confirmSub?.cancel();
|
||||
throw Exception("SSID/Password Incorrect");
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
refreshWifiList();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future refreshWifiList() async{
|
||||
final ssidRefreshChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0004");
|
||||
setState(() {
|
||||
message = null;
|
||||
});
|
||||
|
||||
try {
|
||||
try {
|
||||
await ssidRefreshChar.write(utf8.encode("refresh"), withoutResponse: ssidRefreshChar.properties.writeWithoutResponse);
|
||||
} catch (e) {
|
||||
throw Exception ("Refresh Error");
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted)return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"Select WiFi Network",
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
backgroundColor: Theme.of(context).primaryColorLight,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: refreshWifiList,
|
||||
child: wifiList ?? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
message ?? "Fetching Networks...",
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/BlindMasterScreens/addingDevices/set_device_name.dart
Normal file
128
lib/BlindMasterScreens/addingDevices/set_device_name.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
|
||||
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/home_screen.dart';
|
||||
import 'package:blind_master/utils_from_FBPExample/extra.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class SetDeviceName extends StatefulWidget {
|
||||
const SetDeviceName({super.key, required this.tokenEntryChar, required this.device});
|
||||
final BluetoothCharacteristic tokenEntryChar;
|
||||
final BluetoothDevice device;
|
||||
|
||||
@override
|
||||
State<SetDeviceName> createState() => _SetDeviceNameState();
|
||||
}
|
||||
|
||||
class _SetDeviceNameState extends State<SetDeviceName> {
|
||||
final deviceNameController = TextEditingController();
|
||||
Widget? screen;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initScreen();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
deviceNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void initScreen() {
|
||||
screen = Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
BlindMasterMainInput(
|
||||
"Device Name (Different from others)",
|
||||
controller: deviceNameController,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(
|
||||
"Add to Account"
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future addDevice(String name) async {
|
||||
final payload = {'deviceName': name};
|
||||
String? token;
|
||||
try {
|
||||
final tokenResponse = await securePost(payload, 'add_device');
|
||||
if (tokenResponse == null) return;
|
||||
if (tokenResponse.statusCode != 201) {
|
||||
if (tokenResponse.statusCode == 404) {throw Exception("Somehow the id of your device wasn't found??");}
|
||||
else if (tokenResponse.statusCode == 409) {throw Exception('Device Name in Use');}
|
||||
else {throw Exception('Server Error');}
|
||||
}
|
||||
final jsonResponse = json.decode(tokenResponse.body) as Map<String, dynamic>;
|
||||
final fetchedToken = jsonResponse['token'];
|
||||
if (fetchedToken == null || fetchedToken is! String) {
|
||||
throw Exception('Invalid token in response');
|
||||
}
|
||||
token = fetchedToken;
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
await widget.tokenEntryChar.write(utf8.encode(token), withoutResponse: widget.tokenEntryChar.properties.writeWithoutResponse);
|
||||
} catch (e) {
|
||||
throw Exception("Token Write Error");
|
||||
}
|
||||
} catch (e){
|
||||
if(!mounted)return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
return;
|
||||
}
|
||||
|
||||
await widget.device.disconnectAndUpdateStream().catchError((e) {
|
||||
if(!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
});
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => HomeScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
|
||||
Future onPressed() async {
|
||||
setState(() {
|
||||
screen = null;
|
||||
});
|
||||
await(addDevice(deviceNameController.text));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"Name Your Device",
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
backgroundColor: Theme.of(context).primaryColorLight,
|
||||
),
|
||||
body: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: Center(
|
||||
child: screen ?? CircularProgressIndicator(color: Theme.of(context).primaryColorLight),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
15
lib/BlindMasterScreens/groupControl/groups_menu.dart
Normal file
15
lib/BlindMasterScreens/groupControl/groups_menu.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GroupsMenu extends StatefulWidget {
|
||||
const GroupsMenu({super.key});
|
||||
|
||||
@override
|
||||
State<GroupsMenu> createState() => _GroupsMenuState();
|
||||
}
|
||||
|
||||
class _GroupsMenuState extends State<GroupsMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
75
lib/BlindMasterScreens/home_screen.dart
Normal file
75
lib/BlindMasterScreens/home_screen.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:blind_master/BlindMasterScreens/groupControl/groups_menu.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/individualControl/devices_menu.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
int currentPageIndex = 0;
|
||||
String greeting = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getGreeting();
|
||||
}
|
||||
|
||||
void getGreeting() {
|
||||
final hour = DateTime.now().hour;
|
||||
|
||||
if (hour >= 5 && hour < 12) {
|
||||
greeting = "Good Morning!";
|
||||
} else if (hour >= 12 && hour < 18) {
|
||||
greeting = "Good Afternoon!";
|
||||
} else if (hour >= 18 && hour < 22) {
|
||||
greeting = "Good Evening!";
|
||||
} else {greeting = "😴";}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).primaryColorLight,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
greeting,
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: (int index) {
|
||||
setState(() {
|
||||
currentPageIndex = index;
|
||||
});
|
||||
},
|
||||
indicatorColor: Theme.of(context).primaryColorDark,
|
||||
selectedIndex: currentPageIndex,
|
||||
destinations: const <Widget>[
|
||||
NavigationDestination(
|
||||
selectedIcon: Icon(Icons.blinds_rounded),
|
||||
icon: Icon(Icons.blinds_closed_rounded),
|
||||
label: 'Devices',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.window_outlined),
|
||||
selectedIcon: Icon(Icons.window_rounded),
|
||||
label: 'Groups',
|
||||
),
|
||||
],
|
||||
),
|
||||
body:
|
||||
<Widget>[
|
||||
DevicesMenu(),
|
||||
GroupsMenu(),
|
||||
][currentPageIndex],
|
||||
);
|
||||
}
|
||||
}
|
||||
406
lib/BlindMasterScreens/individualControl/device_screen.dart
Normal file
406
lib/BlindMasterScreens/individualControl/device_screen.dart
Normal file
@@ -0,0 +1,406 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
|
||||
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/individualControl/peripheral_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class DeviceScreen extends StatefulWidget {
|
||||
final int deviceId;
|
||||
const DeviceScreen({super.key, required this.deviceId});
|
||||
|
||||
|
||||
@override
|
||||
State<DeviceScreen> createState() => _DeviceScreenState();
|
||||
}
|
||||
|
||||
class _DeviceScreenState extends State<DeviceScreen> {
|
||||
bool enabled = false;
|
||||
final _newPeripheralNameController = TextEditingController();
|
||||
final _hubRenameController = TextEditingController();
|
||||
List<Map<String, dynamic>> peripherals = [];
|
||||
List occports = [];
|
||||
Widget? peripheralList;
|
||||
String deviceName = "...";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initAll();
|
||||
}
|
||||
|
||||
Future initAll() async {
|
||||
await getDeviceName();
|
||||
await populatePeripherals();
|
||||
}
|
||||
|
||||
Future getDeviceName() async {
|
||||
try {
|
||||
final payload = {
|
||||
"deviceId": widget.deviceId
|
||||
};
|
||||
final response = await secureGet('device_name', queryParameters: payload);
|
||||
if (response == null) throw Exception("no response!");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
setState(() {
|
||||
deviceName = body['device_name'];
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future populatePeripherals() async {
|
||||
setState(() {
|
||||
peripheralList = null;
|
||||
});
|
||||
try {
|
||||
final payload = {
|
||||
"deviceId": widget.deviceId
|
||||
};
|
||||
final response = await secureGet('peripheral_list', queryParameters: payload);
|
||||
if (response == null) throw Exception("no response!");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
final names = body['peripheral_names'] as List;
|
||||
final ids = body['peripheral_ids'] as List;
|
||||
occports = body['port_nums'] as List;
|
||||
peripherals = List.generate(names.length, (i) => {
|
||||
'id': ids[i],
|
||||
'name': names[i],
|
||||
'port': occports[i]
|
||||
});
|
||||
peripherals.sort((a, b) => (a['port'] as int).compareTo(b['port'] as int));
|
||||
enabled = peripherals.length < 4;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
peripheralList = RefreshIndicator(
|
||||
onRefresh: populatePeripherals,
|
||||
child: peripherals.isEmpty ? SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"No peripherals found...\nAdd one using the '+' button",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
) : ListView.builder(
|
||||
itemCount: peripherals.length,
|
||||
itemBuilder: (context, i) {
|
||||
final peripheral = peripherals[i];
|
||||
return Dismissible(
|
||||
key: Key(peripheral['id'].toString()),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
confirmDismiss: (direction) async {
|
||||
// Ask for confirmation (optional)
|
||||
return await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete Peripheral'),
|
||||
content: const Text('Are you sure you want to delete this peripheral?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
onDismissed: (direction) => deletePeripheral(peripheral['id'], i),
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.blinds),
|
||||
title: Text(peripheral['name']),
|
||||
subtitle: Text("Port #${peripheral['port']}"),
|
||||
trailing: const Icon(Icons.arrow_forward_ios_rounded),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PeripheralScreen(peripheralId: peripheral['id'],
|
||||
peripheralNum: peripheral['port'], deviceId: widget.deviceId,),
|
||||
),
|
||||
).then((_) { populatePeripherals(); });
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
return Future.delayed(Duration(milliseconds: 500));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future deletePeripheral(int id, int i) async {
|
||||
setState(() {
|
||||
peripherals.removeAt(i);
|
||||
peripheralList = null;
|
||||
});
|
||||
final payload = {
|
||||
'periphId': id,
|
||||
};
|
||||
try {
|
||||
final response = await securePost(payload, 'delete_peripheral');
|
||||
if (response == null) return;
|
||||
if (response.statusCode != 204) {
|
||||
if (response.statusCode == 404) {throw Exception('Device Not Found');}
|
||||
else if (response.statusCode == 500) {throw Exception('Server Error');}
|
||||
}
|
||||
if (mounted){
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text(
|
||||
'Deleted',
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
|
||||
populatePeripherals();
|
||||
}
|
||||
|
||||
void addPeripheral() {
|
||||
var freePorts = <int>{};
|
||||
for (int i = 1; i < 5; i++) {
|
||||
freePorts.add(i);
|
||||
}
|
||||
freePorts = freePorts.difference(occports.toSet());
|
||||
int? port = freePorts.firstOrNull;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) { // Use dialogContext for navigation within the dialog
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'New Peripheral',
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min, // Keep column compact
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
controller: _newPeripheralNameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Peripheral Name',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
DropdownButtonFormField<int>(
|
||||
value: port,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Hub Port',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: freePorts.map((int number) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: number,
|
||||
child: Text('$number'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (int? newValue) {
|
||||
setState(() {
|
||||
port = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
color: Colors.red
|
||||
),
|
||||
)
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
uploadPeriphData(_newPeripheralNameController.text, port);
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text("Add"),
|
||||
),
|
||||
]
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Future uploadPeriphData(String name, int? port) async {
|
||||
try {
|
||||
if (name.isEmpty || port == null) {
|
||||
throw Exception("Name and Port Required");
|
||||
}
|
||||
final payload = {
|
||||
'device_id': widget.deviceId,
|
||||
'port_num': port,
|
||||
'peripheral_name': name
|
||||
};
|
||||
|
||||
final response = await securePost(payload, 'add_peripheral');
|
||||
if (response == null) throw Exception("Auth Error");
|
||||
if (response.statusCode != 201) {
|
||||
if (response.statusCode == 409) throw Exception("Choose a unique name!");
|
||||
throw Exception("Server Error");
|
||||
}
|
||||
|
||||
populatePeripherals();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
void rename() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
"Rename Hub",
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
content: BlindMasterMainInput("New Hub Name", controller: _hubRenameController,),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
color: Colors.red
|
||||
),
|
||||
)
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
updateHubName(_hubRenameController.text, widget.deviceId);
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text("Confirm")
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Future updateHubName(String name, int id) async {
|
||||
try {
|
||||
if (name.isEmpty) throw Exception("New name cannot be empty!");
|
||||
final payload = {
|
||||
'deviceId': id,
|
||||
'newName': name,
|
||||
};
|
||||
final response = await securePost(payload, 'rename_device');
|
||||
if (response == null) throw Exception("Auth Error");
|
||||
if (response.statusCode != 204) {
|
||||
if (response.statusCode == 409) throw Exception("Choose a unique name!");
|
||||
throw Exception("Server Error");
|
||||
}
|
||||
getDeviceName();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
deviceName,
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
backgroundColor: Theme.of(context).primaryColorLight,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: peripheralList ?? SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
),
|
||||
)
|
||||
),
|
||||
floatingActionButton: Container(
|
||||
padding: EdgeInsets.all(25),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
backgroundColor: Theme.of(context).primaryColorDark,
|
||||
foregroundColor: Theme.of(context).highlightColor,
|
||||
heroTag: "rename",
|
||||
onPressed: rename,
|
||||
tooltip: "Rename Hub",
|
||||
child: Icon(Icons.drive_file_rename_outline_sharp),
|
||||
),
|
||||
FloatingActionButton(
|
||||
backgroundColor: enabled
|
||||
? Theme.of(context).primaryColorDark
|
||||
: Theme.of(context).disabledColor,
|
||||
foregroundColor: Theme.of(context).highlightColor,
|
||||
heroTag: "add",
|
||||
onPressed: enabled ? addPeripheral : null,
|
||||
tooltip: "Add Peripheral",
|
||||
child: Icon(Icons.add),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
);
|
||||
}
|
||||
}
|
||||
175
lib/BlindMasterScreens/individualControl/devices_menu.dart
Normal file
175
lib/BlindMasterScreens/individualControl/devices_menu.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/addingDevices/add_device.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/individualControl/device_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DevicesMenu extends StatefulWidget {
|
||||
const DevicesMenu({super.key});
|
||||
|
||||
@override
|
||||
State<DevicesMenu> createState() => _DevicesMenuState();
|
||||
}
|
||||
|
||||
class _DevicesMenuState extends State<DevicesMenu> {
|
||||
List<Map<String, dynamic>> devices = [];
|
||||
Widget? deviceList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getDevices();
|
||||
}
|
||||
|
||||
Future getDevices() async {
|
||||
try{
|
||||
final response = await secureGet('device_list');
|
||||
if (response == null) throw Exception("no response!");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
final names = body['devices'] as List;
|
||||
final ids = body['device_ids'] as List;
|
||||
devices = List.generate(names.length, (i) => {
|
||||
'id': ids[i],
|
||||
'name': names[i],
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
errorSnackbar(e)
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
deviceList = RefreshIndicator(
|
||||
onRefresh: getDevices,
|
||||
child: devices.isEmpty
|
||||
? SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"No hubs found...\nAdd one using the '+' button",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: devices.length,
|
||||
itemBuilder: (context, i) {
|
||||
final device = devices[i];
|
||||
return Dismissible(
|
||||
key: Key(device['id'].toString()),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
confirmDismiss: (direction) async {
|
||||
// Ask for confirmation (optional)
|
||||
return await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete Hub'),
|
||||
content: const Text('Are you sure you want to delete this hub?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
onDismissed: (direction) {
|
||||
// Actually delete the device
|
||||
deleteDevice(device['id'], i);
|
||||
},
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.blinds),
|
||||
title: Text(device['name']),
|
||||
trailing: const Icon(Icons.arrow_forward_ios_rounded),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DeviceScreen(deviceId: device['id']),
|
||||
),
|
||||
).then((_) { getDevices(); });
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
return Future.delayed(Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
Future deleteDevice(int id, int i) async {
|
||||
setState(() {
|
||||
devices.removeAt(i);
|
||||
deviceList = null;
|
||||
});
|
||||
print("deleting");
|
||||
final payload = {
|
||||
'deviceId': id,
|
||||
};
|
||||
try {
|
||||
final response = await securePost(payload, 'delete_device');
|
||||
if (response == null) return;
|
||||
if (response.statusCode != 204) {
|
||||
if (response.statusCode == 404) {throw Exception('Device Not Found');}
|
||||
else if (response.statusCode == 500) {throw Exception('Server Error');}
|
||||
}
|
||||
if (mounted){
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Deleted',
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
|
||||
getDevices();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: deviceList ?? const Center(child: CircularProgressIndicator()),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => AddDevice()),
|
||||
);
|
||||
},
|
||||
foregroundColor: Theme.of(context).highlightColor,
|
||||
backgroundColor: Theme.of(context).primaryColorDark,
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
524
lib/BlindMasterScreens/individualControl/peripheral_screen.dart
Normal file
524
lib/BlindMasterScreens/individualControl/peripheral_screen.dart
Normal file
@@ -0,0 +1,524 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:blind_master/BlindMasterResources/blindmaster_progress_indicator.dart';
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
|
||||
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/schedules_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart' as IO;
|
||||
|
||||
class PeripheralScreen extends StatefulWidget {
|
||||
const PeripheralScreen({super.key, required this.peripheralId, required this.deviceId, required this.peripheralNum});
|
||||
final int peripheralId;
|
||||
final int peripheralNum;
|
||||
final int deviceId;
|
||||
@override
|
||||
State<PeripheralScreen> createState() => _PeripheralScreenState();
|
||||
}
|
||||
|
||||
class _PeripheralScreenState extends State<PeripheralScreen> {
|
||||
IO.Socket? socket;
|
||||
String imagePath = "";
|
||||
String peripheralName = "...";
|
||||
bool loaded = false;
|
||||
bool calibrated = false;
|
||||
bool calibrating = false;
|
||||
double _blindPosition = 5.0;
|
||||
DateTime? lastSet;
|
||||
String lastSetMessage = "";
|
||||
|
||||
final _peripheralRenameController = TextEditingController();
|
||||
|
||||
void getImage() {
|
||||
final hour = DateTime.now().hour;
|
||||
|
||||
if (hour >= 5 && hour < 10) {
|
||||
imagePath = 'assets/images/MorningSill.png';
|
||||
} else if (hour >= 10 && hour < 18) {
|
||||
imagePath = 'assets/images/NoonSill.png';
|
||||
} else if (hour >= 18 && hour < 22) {
|
||||
imagePath = 'assets/images/EveningSill.png';
|
||||
} else {
|
||||
imagePath = 'assets/images/NightSill.png';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initAll();
|
||||
initSocket();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
socket?.disconnect();
|
||||
socket?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> initSocket() async {
|
||||
try {
|
||||
socket = await connectSocket();
|
||||
if (socket == null) throw Exception("Unsuccessful socket connection");
|
||||
socket?.on("success", (_) {
|
||||
socket?.on("posUpdates", (list) {
|
||||
for (var update in list) {
|
||||
if (update is Map<String, dynamic>) {
|
||||
if (update['periphID'] == widget.peripheralId) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_blindPosition = (update['pos'] as int).toDouble();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
socket?.on("calib", (periphData) {
|
||||
if (periphData is Map<String, dynamic>) {
|
||||
if (periphData['periphID'] == widget.peripheralId) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
calibrating = true;
|
||||
calibrated = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
socket?.on("calib_done", (periphData) {
|
||||
if (periphData is Map<String, dynamic>) {
|
||||
if (periphData['periphID'] == widget.peripheralId) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
calibrating = false;
|
||||
calibrated = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> calibrate() async {
|
||||
try {
|
||||
final payload = {
|
||||
'periphId': widget.peripheralId
|
||||
};
|
||||
|
||||
final response = await securePost(payload, 'calib');
|
||||
|
||||
if (response == null) throw Exception("auth error");
|
||||
if (response.statusCode != 202) throw Exception("Server Error");
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cancelCalib() async {
|
||||
try {
|
||||
final payload = {
|
||||
'periphId': widget.peripheralId
|
||||
};
|
||||
|
||||
final response = await securePost(payload, 'cancel_calib');
|
||||
|
||||
if (response == null) throw Exception("auth error");
|
||||
if (response.statusCode != 202) throw Exception("Server Error");
|
||||
setState(() {
|
||||
calibrated = false;
|
||||
calibrating = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getName() async {
|
||||
try {
|
||||
final payload = {
|
||||
'periphId': widget.peripheralId
|
||||
};
|
||||
final response = await secureGet('peripheral_name', queryParameters: payload);
|
||||
if (response == null) throw Exception("auth error");
|
||||
if (response.statusCode != 200) throw Exception("Server Error");
|
||||
final body = json.decode(response.body);
|
||||
setState(() => peripheralName = body['name']);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future loop() async{
|
||||
try {
|
||||
final payload = {
|
||||
'periphId': widget.peripheralId
|
||||
};
|
||||
|
||||
final response = await secureGet('peripheral_status', queryParameters: payload);
|
||||
if (response == null) throw Exception("auth error");
|
||||
if (response.statusCode != 200) {
|
||||
if (response.statusCode == 404) throw Exception("Device Not Found");
|
||||
throw Exception("Server Error");
|
||||
}
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
if (!body['await_calib']){
|
||||
if (!body['calibrated']) {
|
||||
calibrated = false;
|
||||
calibrating = false;
|
||||
}
|
||||
else {
|
||||
getImage();
|
||||
final nowUtc = DateTime.now().toUtc();
|
||||
final lastSetUtc = DateTime.parse(body['last_set']);
|
||||
final Duration difference = nowUtc.difference(lastSetUtc);
|
||||
if (!lastSetUtc.isUtc) throw Exception("Why isn't the server giving UTC?");
|
||||
final diffDays = difference.inDays > 0;
|
||||
final diffHours = difference.inHours > 0;
|
||||
final diffMins = difference.inMinutes > 0;
|
||||
lastSetMessage = "Last set ${diffDays ? '${difference.inDays.toString()} days' : diffHours ? '${difference.inHours.toString()} hours' : diffMins ? '${difference.inMinutes.toString()} minutes' : '${difference.inSeconds.toString()} seconds'} ago";
|
||||
_blindPosition = (body['last_pos'] as int).toDouble();
|
||||
|
||||
calibrated = true;
|
||||
calibrating = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
calibrating = true;
|
||||
}
|
||||
|
||||
setState(() {loaded = true;});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future initAll() async{
|
||||
getName();
|
||||
loop();
|
||||
}
|
||||
|
||||
void rename() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
"Rename Peripheral",
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
content: BlindMasterMainInput("New Peripheral Name", controller: _peripheralRenameController,),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
color: Colors.red
|
||||
),
|
||||
)
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
updatePeriphName(_peripheralRenameController.text, widget.peripheralId);
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text("Confirm")
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Future updatePeriphName(String name, int id) async {
|
||||
try {
|
||||
if (name.isEmpty) throw Exception("New name cannot be empty!");
|
||||
final payload = {
|
||||
'periphId': id,
|
||||
'newName': name,
|
||||
};
|
||||
final response = await securePost(payload, 'rename_peripheral');
|
||||
if (response == null) throw Exception("Auth Error");
|
||||
if (response.statusCode != 204) {
|
||||
if (response.statusCode == 409) throw Exception("Choose a unique name!");
|
||||
throw Exception("Server Error");
|
||||
}
|
||||
getName();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
void recalibrate() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
"Recalibrate Peripheral",
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
content: const Text(
|
||||
"This will take under a minute",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
color: Colors.red
|
||||
),
|
||||
)
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
calibrate();
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text("Confirm")
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Future updateBlindPosition() async {
|
||||
try {
|
||||
final payload = {
|
||||
'periphId': widget.peripheralId,
|
||||
'periphNum': widget.peripheralNum,
|
||||
'deviceId': widget.deviceId,
|
||||
'newPos': _blindPosition.toInt(),
|
||||
};
|
||||
|
||||
final response = await securePost(payload, 'manual_position_update');
|
||||
if (response == null) throw Exception("Auth Error");
|
||||
if (response.statusCode != 202) {
|
||||
throw Exception("Server Error");
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
peripheralName,
|
||||
style: GoogleFonts.aBeeZee(),
|
||||
),
|
||||
backgroundColor: Theme.of(context).primaryColorLight,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
|
||||
body: loaded
|
||||
? (calibrating
|
||||
? RefreshIndicator(
|
||||
onRefresh: initAll,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
"Calibrating... Check again soon."
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: cancelCalib,
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
color: Colors.red
|
||||
),
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: (calibrated
|
||||
? Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(0, 20, 0, 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.15,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
// Background image
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
imagePath,
|
||||
// fit: BoxFit.cover,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
),
|
||||
),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: MediaQuery.of(context).size.width * 0.05),
|
||||
height: MediaQuery.of(context).size.width * 0.68,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(10, (index) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: _blindPosition < 5 ?
|
||||
5.4 * (5 - _blindPosition)
|
||||
: 5.4 * (_blindPosition - 5),
|
||||
width: MediaQuery.of(context).size.width * 0.65, // example
|
||||
color: const Color.fromARGB(255, 121, 85, 72),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
// Slider on the side
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: RotatedBox(
|
||||
quarterTurns: -1,
|
||||
child: Slider(
|
||||
value: _blindPosition,
|
||||
activeColor: Theme.of(context).primaryColorDark,
|
||||
thumbColor: Theme.of(context).primaryColorLight,
|
||||
inactiveColor: Theme.of(context).primaryColorDark,
|
||||
min: 0,
|
||||
max: 10,
|
||||
divisions: 10,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_blindPosition = value;
|
||||
updateBlindPosition();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.all(25),
|
||||
child: Text(
|
||||
lastSetMessage
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SchedulesScreen()
|
||||
)
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Set Schedules"
|
||||
)
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
"Peripheral Not Calibrated"
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: calibrate,
|
||||
child: const Text("Calibrate")
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
)))
|
||||
: BlindmasterProgressIndicator(),
|
||||
floatingActionButton: Container(
|
||||
padding: EdgeInsets.all(25),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
heroTag: "rename",
|
||||
tooltip: "Rename Peripheral",
|
||||
onPressed: rename,
|
||||
foregroundColor: Theme.of(context).highlightColor,
|
||||
backgroundColor: Theme.of(context).primaryColorDark,
|
||||
child: Icon(Icons.drive_file_rename_outline_sharp),
|
||||
),
|
||||
FloatingActionButton(
|
||||
heroTag: "recalibrate",
|
||||
tooltip: "Recalibrate Peripheral",
|
||||
onPressed: recalibrate,
|
||||
foregroundColor: Theme.of(context).highlightColor,
|
||||
backgroundColor: Theme.of(context).primaryColorDark,
|
||||
child: Icon(Icons.swap_vert),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
);
|
||||
}
|
||||
}
|
||||
15
lib/BlindMasterScreens/schedules_screen.dart
Normal file
15
lib/BlindMasterScreens/schedules_screen.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SchedulesScreen extends StatefulWidget {
|
||||
const SchedulesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SchedulesScreen> createState() => _SchedulesScreenState();
|
||||
}
|
||||
|
||||
class _SchedulesScreenState extends State<SchedulesScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user