Initial
This commit is contained in:
17
lib/BlindMasterResources/blindmaster_progress_indicator.dart
Normal file
17
lib/BlindMasterResources/blindmaster_progress_indicator.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BlindmasterProgressIndicator extends StatelessWidget {
|
||||
const BlindmasterProgressIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).primaryColorDark,
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
19
lib/BlindMasterResources/error_snackbar.dart
Normal file
19
lib/BlindMasterResources/error_snackbar.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
SnackBar errorSnackbar(
|
||||
Object e, {
|
||||
Color backgroundColor = const Color.fromARGB(255, 196, 26, 14),
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
return SnackBar(
|
||||
backgroundColor: Color.fromARGB(255, 196, 26, 14),
|
||||
content: Text(
|
||||
e.toString().replaceFirst(RegExp(r'^[^:]+:\s*'), ''),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.white
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
16
lib/BlindMasterResources/fade_transition.dart
Normal file
16
lib/BlindMasterResources/fade_transition.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
PageRouteBuilder<dynamic> fadeTransition(Widget nextScreen) {
|
||||
return PageRouteBuilder(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => nextScreen,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
transitionDuration: Duration(milliseconds: 500)
|
||||
);
|
||||
}
|
||||
|
||||
113
lib/BlindMasterResources/secure_transmissions.dart
Normal file
113
lib/BlindMasterResources/secure_transmissions.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:socket_io_client/socket_io_client.dart' as IO;
|
||||
|
||||
String local = Platform.isAndroid ? '10.0.2.2' : 'localhost';
|
||||
String fromDevice = '192.168.1.190';
|
||||
|
||||
String host = local;
|
||||
int port = 3000;
|
||||
String socketString = "$scheme://$host:$port";
|
||||
String scheme = 'http';
|
||||
|
||||
Future<http.Response?> secureGet(String path, {Map<String, dynamic>? queryParameters}) async{
|
||||
final storage = FlutterSecureStorage();
|
||||
final token = await storage.read(key: 'token');
|
||||
if (token == null) return null;
|
||||
|
||||
final uri = Uri(
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port, // your host
|
||||
path: path, // your path
|
||||
queryParameters: queryParameters?.map((key, value) => MapEntry(key, value.toString())),
|
||||
);
|
||||
|
||||
return await http
|
||||
.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
)
|
||||
.timeout(const Duration(seconds: 10)); // 🚀 Timeout added
|
||||
}
|
||||
|
||||
Future<http.Response?> securePost(Map<String, dynamic> payload, String path) async{
|
||||
final storage = FlutterSecureStorage();
|
||||
final token = await storage.read(key: 'token');
|
||||
if (token == null) return null;
|
||||
|
||||
final uri = Uri(
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port, // your host
|
||||
path: path, // your path
|
||||
);
|
||||
|
||||
return await http.post(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: json.encode(payload),
|
||||
)
|
||||
.timeout(const Duration(seconds: 10)); // 🚀 Timeout added
|
||||
}
|
||||
|
||||
Future<http.Response> regularGet(String path) async {
|
||||
final uri = Uri(
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port, // your host
|
||||
path: path, // your path
|
||||
);
|
||||
|
||||
return await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
)
|
||||
.timeout(const Duration(seconds: 10)); // 🚀 Timeout added
|
||||
}
|
||||
|
||||
Future<http.Response> regularPost(Map<String, dynamic> payload, String path) async{
|
||||
final uri = Uri(
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port, // your host
|
||||
path: path, // your path
|
||||
);
|
||||
|
||||
return await http.post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: json.encode(payload)
|
||||
).timeout(const Duration(seconds: 10)); // 🚀 Timeout added
|
||||
}
|
||||
|
||||
Future<IO.Socket?> connectSocket() async {
|
||||
final storage = FlutterSecureStorage();
|
||||
final token = await storage.read(key: 'token');
|
||||
if (token == null) return null;
|
||||
|
||||
final socket = IO.io(
|
||||
socketString,
|
||||
IO.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.setAuth({'token': token})
|
||||
.disableAutoConnect().build(),
|
||||
);
|
||||
|
||||
socket.connect();
|
||||
|
||||
return socket;
|
||||
}
|
||||
39
lib/BlindMasterResources/text_inputs.dart
Normal file
39
lib/BlindMasterResources/text_inputs.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
|
||||
class BlindMasterMainInput extends StatelessWidget {
|
||||
const BlindMasterMainInput(this.label, {super.key, this.controller, this.validator, this.color, this.password = false});
|
||||
|
||||
final String label;
|
||||
final TextEditingController? controller;
|
||||
final Color? color;
|
||||
final bool password;
|
||||
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
child:TextFormField(
|
||||
validator: validator,
|
||||
obscureText: password,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
controller: controller,
|
||||
style: TextStyle(
|
||||
color: color
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
labelText: label,
|
||||
labelStyle: TextStyle(color: color),
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
21
lib/BlindMasterResources/title_text.dart
Normal file
21
lib/BlindMasterResources/title_text.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class TitleText extends StatelessWidget {
|
||||
const TitleText(this.text, {super.key, this.txtClr});
|
||||
|
||||
final String text;
|
||||
final Color? txtClr;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
style: GoogleFonts.aBeeZee(
|
||||
color: txtClr,
|
||||
fontSize: 50
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
65
lib/main.dart
Normal file
65
lib/main.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:blind_master/BlindMasterScreens/Startup/splash_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
Map<String, Color> getBackgroundBasedOnTime() {
|
||||
final hour = DateTime.now().hour;
|
||||
|
||||
Color secondaryLight;
|
||||
Color primary;
|
||||
Color secondaryDark;
|
||||
if (hour >= 5 && hour < 10) {
|
||||
// Morning
|
||||
primary = Colors.orange;
|
||||
secondaryLight = const Color.fromARGB(255, 255, 204, 128);
|
||||
secondaryDark = const Color.fromARGB(255, 174, 104, 0);
|
||||
} else if (hour >= 10 && hour < 18) {
|
||||
// Afternoon
|
||||
primary = Colors.blue;
|
||||
secondaryLight = const Color.fromARGB(255, 144, 202, 249);
|
||||
secondaryDark = const Color.fromARGB(255, 0, 92, 168);
|
||||
} else {
|
||||
// Evening/Night
|
||||
primary = const Color.fromARGB(255, 71, 17, 137);
|
||||
secondaryLight = const Color.fromARGB(255, 186, 130, 255);
|
||||
secondaryDark = const Color.fromARGB(255, 40, 0, 89);
|
||||
}
|
||||
|
||||
return {
|
||||
'primary': primary,
|
||||
'secondaryLight': secondaryLight,
|
||||
'secondaryDark': secondaryDark,
|
||||
};
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = getBackgroundBasedOnTime();
|
||||
return MaterialApp(
|
||||
home: SplashScreen(),
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
primaryColorLight: colors['primary'],
|
||||
highlightColor: Colors.black,
|
||||
disabledColor: Colors.grey,
|
||||
primaryColorDark: colors['secondaryLight'],
|
||||
brightness: Brightness.light
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
highlightColor: Colors.white,
|
||||
primaryColorLight: colors['primary'],
|
||||
disabledColor: Colors.grey[800],
|
||||
primaryColorDark: colors['secondaryDark'],
|
||||
brightness: Brightness.dark
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
51
lib/utils_from_FBPExample/extra.dart
Normal file
51
lib/utils_from_FBPExample/extra.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'utils.dart';
|
||||
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
|
||||
final Map<DeviceIdentifier, StreamControllerReemit<bool>> _cglobal = {};
|
||||
final Map<DeviceIdentifier, StreamControllerReemit<bool>> _dglobal = {};
|
||||
|
||||
/// connect & disconnect + update stream
|
||||
extension Extra on BluetoothDevice {
|
||||
// convenience
|
||||
StreamControllerReemit<bool> get _cstream {
|
||||
_cglobal[remoteId] ??= StreamControllerReemit(initialValue: false);
|
||||
return _cglobal[remoteId]!;
|
||||
}
|
||||
|
||||
// convenience
|
||||
StreamControllerReemit<bool> get _dstream {
|
||||
_dglobal[remoteId] ??= StreamControllerReemit(initialValue: false);
|
||||
return _dglobal[remoteId]!;
|
||||
}
|
||||
|
||||
// get stream
|
||||
Stream<bool> get isConnecting {
|
||||
return _cstream.stream;
|
||||
}
|
||||
|
||||
// get stream
|
||||
Stream<bool> get isDisconnecting {
|
||||
return _dstream.stream;
|
||||
}
|
||||
|
||||
// connect & update stream
|
||||
Future<void> connectAndUpdateStream() async {
|
||||
_cstream.add(true);
|
||||
try {
|
||||
await connect(mtu: null);
|
||||
} finally {
|
||||
_cstream.add(false);
|
||||
}
|
||||
}
|
||||
|
||||
// disconnect & update stream
|
||||
Future<void> disconnectAndUpdateStream({bool queue = true}) async {
|
||||
_dstream.add(true);
|
||||
try {
|
||||
await disconnect(queue: queue);
|
||||
} finally {
|
||||
_dstream.add(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
lib/utils_from_FBPExample/scan_result_tile.dart
Normal file
64
lib/utils_from_FBPExample/scan_result_tile.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
|
||||
class ScanResultTile extends StatefulWidget {
|
||||
const ScanResultTile({super.key, required this.result, this.onTap});
|
||||
|
||||
final ScanResult result;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
State<ScanResultTile> createState() => _ScanResultTileState();
|
||||
}
|
||||
|
||||
class _ScanResultTileState extends State<ScanResultTile> {
|
||||
BluetoothConnectionState _connectionState = BluetoothConnectionState.disconnected;
|
||||
|
||||
late StreamSubscription<BluetoothConnectionState> _connectionStateSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_connectionStateSubscription = widget.result.device.connectionState.listen((state) {
|
||||
_connectionState = state;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connectionStateSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get isConnected {
|
||||
return _connectionState == BluetoothConnectionState.connected;
|
||||
}
|
||||
|
||||
Widget _buildTitle(BuildContext context) {
|
||||
return Text(
|
||||
widget.result.advertisementData.advName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: _buildTitle(context),
|
||||
subtitle: Text(
|
||||
"Signal Strength (Less = farther): ${widget.result.rssi}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Icon(Icons.arrow_forward_ios_rounded),
|
||||
onTap: widget.result.advertisementData.connectable ? widget.onTap : null,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
153
lib/utils_from_FBPExample/utils.dart
Normal file
153
lib/utils_from_FBPExample/utils.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
import 'dart:async';
|
||||
|
||||
// It is essentially a stream but:
|
||||
// 1. we cache the latestValue of the stream
|
||||
// 2. the "latestValue" is re-emitted whenever the stream is listened to
|
||||
class StreamControllerReemit<T> {
|
||||
T? _latestValue;
|
||||
|
||||
final StreamController<T> _controller = StreamController<T>.broadcast();
|
||||
|
||||
StreamControllerReemit({T? initialValue}) : _latestValue = initialValue;
|
||||
|
||||
Stream<T> get stream {
|
||||
return _latestValue != null ? _controller.stream.newStreamWithInitialValue(_latestValue as T) : _controller.stream;
|
||||
}
|
||||
|
||||
T? get value => _latestValue;
|
||||
|
||||
void add(T newValue) {
|
||||
_latestValue = newValue;
|
||||
_controller.add(newValue);
|
||||
}
|
||||
|
||||
Future<void> close() {
|
||||
return _controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
// return a new stream that immediately emits an initial value
|
||||
extension _StreamNewStreamWithInitialValue<T> on Stream<T> {
|
||||
Stream<T> newStreamWithInitialValue(T initialValue) {
|
||||
return transform(_NewStreamWithInitialValueTransformer(initialValue));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for 'newStreamWithInitialValue' method for streams.
|
||||
class _NewStreamWithInitialValueTransformer<T> extends StreamTransformerBase<T, T> {
|
||||
/// the initial value to push to the new stream
|
||||
final T initialValue;
|
||||
|
||||
/// controller for the new stream
|
||||
late StreamController<T> controller;
|
||||
|
||||
/// subscription to the original stream
|
||||
late StreamSubscription<T> subscription;
|
||||
|
||||
/// new stream listener count
|
||||
var listenerCount = 0;
|
||||
|
||||
_NewStreamWithInitialValueTransformer(this.initialValue);
|
||||
|
||||
@override
|
||||
Stream<T> bind(Stream<T> stream) {
|
||||
if (stream.isBroadcast) {
|
||||
return _bind(stream, broadcast: true);
|
||||
} else {
|
||||
return _bind(stream);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<T> _bind(Stream<T> stream, {bool broadcast = false}) {
|
||||
|
||||
/////////////////////////////////////////
|
||||
/// Original Stream Subscription Callbacks
|
||||
///
|
||||
|
||||
/// When the original stream emits data, forward it to our new stream
|
||||
void onData(T data) {
|
||||
controller.add(data);
|
||||
}
|
||||
|
||||
/// When the original stream is done, close our new stream
|
||||
void onDone() {
|
||||
controller.close();
|
||||
}
|
||||
|
||||
/// When the original stream has an error, forward it to our new stream
|
||||
void onError(Object error) {
|
||||
controller.addError(error);
|
||||
}
|
||||
|
||||
/// When a client listens to our new stream, emit the
|
||||
/// initial value and subscribe to original stream if needed
|
||||
void onListen() {
|
||||
// Emit the initial value to our new stream
|
||||
controller.add(initialValue);
|
||||
|
||||
// listen to the original stream, if needed
|
||||
if (listenerCount == 0) {
|
||||
subscription = stream.listen(
|
||||
onData,
|
||||
onError: onError,
|
||||
onDone: onDone,
|
||||
);
|
||||
}
|
||||
|
||||
// count listeners of the new stream
|
||||
listenerCount++;
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
/// New Stream Controller Callbacks
|
||||
///
|
||||
|
||||
/// (Single Subscription Only) When a client pauses
|
||||
/// the new stream, pause the original stream
|
||||
void onPause() {
|
||||
subscription.pause();
|
||||
}
|
||||
|
||||
/// (Single Subscription Only) When a client resumes
|
||||
/// the new stream, resume the original stream
|
||||
void onResume() {
|
||||
subscription.resume();
|
||||
}
|
||||
|
||||
/// Called when a client cancels their
|
||||
/// subscription to the new stream,
|
||||
void onCancel() {
|
||||
// count listeners of the new stream
|
||||
listenerCount--;
|
||||
|
||||
// when there are no more listeners of the new stream,
|
||||
// cancel the subscription to the original stream,
|
||||
// and close the new stream controller
|
||||
if (listenerCount == 0) {
|
||||
subscription.cancel();
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
/// Return New Stream
|
||||
///
|
||||
|
||||
// create a new stream controller
|
||||
if (broadcast) {
|
||||
controller = StreamController<T>.broadcast(
|
||||
onListen: onListen,
|
||||
onCancel: onCancel,
|
||||
);
|
||||
} else {
|
||||
controller = StreamController<T>(
|
||||
onListen: onListen,
|
||||
onPause: onPause,
|
||||
onResume: onResume,
|
||||
onCancel: onCancel,
|
||||
);
|
||||
}
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user