This commit is contained in:
Aditya Pulipaka
2025-07-10 18:52:04 -05:00
commit e0a41761ec
166 changed files with 8444 additions and 0 deletions

View 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"
),
)
],
),
),
);
}
}

View 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"
),
),
],
),
),
);
}
}

View 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)
),
),
);
}
}

View 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()
),
);
}
}

View 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,
)
]
),
)
),
);
}
}

View 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),
)
)
);
}
}

View 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();
}
}

View 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],
);
}
}

View 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,
);
}
}

View 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),
),
);
}
}

View 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,
);
}
}

View 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();
}
}