From d33a88dfe03d9bbdf59f6b75dfa93127a0c775a0 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:04:14 +0200 Subject: [PATCH 1/6] Add dependencies and connectivity service --- lib/services/connectivity_service.dart | 13 ++ pubspec.lock | 252 ++++++++++++++++++++++++- pubspec.yaml | 11 +- 3 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 lib/services/connectivity_service.dart diff --git a/lib/services/connectivity_service.dart b/lib/services/connectivity_service.dart new file mode 100644 index 0000000..8299526 --- /dev/null +++ b/lib/services/connectivity_service.dart @@ -0,0 +1,13 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; + +class ConnectivityService { + Future hasConnection() async { + final List results = + await Connectivity().checkConnectivity(); + + if (results.isEmpty || results.contains(ConnectivityResult.none)) { + return false; + } + return true; + } +} diff --git a/pubspec.lock b/pubspec.lock index 3939540..4157023 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -33,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -41,6 +57,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -49,6 +89,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + event_bus: + dependency: transitive + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.dev" + source: hosted + version: "2.0.1" fake_async: dependency: transitive description: @@ -86,6 +142,54 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -96,6 +200,30 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" leak_tracker: dependency: transitive description: @@ -160,6 +288,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mqtt_client: + dependency: "direct main" + description: + name: mqtt_client + sha256: fd22ea00a4c7b5623e01000a91a256d62a8bacba38e9812170458070c52affed + url: "https://pub.dev" + source: hosted + version: "10.11.9" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" path: dependency: transitive description: @@ -168,6 +336,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -192,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -208,6 +408,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" sensors_plus: dependency: "direct main" description: @@ -341,6 +557,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.9" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -365,6 +589,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -373,6 +605,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: - dart: ">=3.11.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 2487a60..b67e1ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ^3.11.0 + sdk: ">=3.0.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -36,9 +36,14 @@ dependencies: cupertino_icons: ^1.0.8 - shake: ^3.0.0 # Оновлена версія - sensors_plus: ^6.1.1 # Сумісна версія + shake: ^3.0.0 + sensors_plus: ^6.1.1 shared_preferences: ^2.2.2 + + mqtt_client: ^10.0.0 + connectivity_plus: ^6.0.0 + flutter_secure_storage: ^9.0.0 + provider: ^6.1.1 dev_dependencies: flutter_test: From 08e7aba1a58ce5c2016cdb65d0db7066c4731eaa Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:06:55 +0200 Subject: [PATCH 2/6] Implement secure auth and provider --- lib/main.dart | 81 +++++++++++++++++++++++---- lib/providers/auth_provider.dart | 41 ++++++++++++++ lib/screens/auth/login_screen.dart | 27 ++++++--- lib/screens/auth/register_screen.dart | 33 +++++++---- 4 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 lib/providers/auth_provider.dart diff --git a/lib/main.dart b/lib/main.dart index c0cdfcc..47cce83 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,23 +1,32 @@ import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/providers/auth_provider.dart'; +import 'package:mobile_flutter_iot/providers/mqtt_provider.dart'; import 'package:mobile_flutter_iot/screens/auth/login_screen.dart'; import 'package:mobile_flutter_iot/screens/auth/register_screen.dart'; import 'package:mobile_flutter_iot/screens/home/details_screen.dart'; import 'package:mobile_flutter_iot/screens/main/main_wrapper.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:mobile_flutter_iot/services/connectivity_service.dart'; +import 'package:provider/provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - final prefs = await SharedPreferences.getInstance(); + final authProvider = AuthProvider(); + await authProvider.checkAuth(); - final bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false; - - runApp(SmartWorkspaceApp(isLoggedIn: isLoggedIn)); + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: authProvider), + ChangeNotifierProvider(create: (_) => MqttProvider()), + ], + child: const SmartWorkspaceApp(), + ), + ); } class SmartWorkspaceApp extends StatelessWidget { - final bool isLoggedIn; - const SmartWorkspaceApp({required this.isLoggedIn, super.key}); + const SmartWorkspaceApp({super.key}); @override Widget build(BuildContext context) { @@ -34,9 +43,7 @@ class SmartWorkspaceApp extends StatelessWidget { error: Color(0xFFF87171), ), ), - - home: isLoggedIn ? const MainWrapper() : const LoginScreen(), - + home: const RootHandler(), routes: { '/login': (context) => const LoginScreen(), '/register': (context) => const RegisterScreen(), @@ -46,3 +53,57 @@ class SmartWorkspaceApp extends StatelessWidget { ); } } + +class RootHandler extends StatefulWidget { + const RootHandler({super.key}); + + @override + State createState() => _RootHandlerState(); +} + +class _RootHandlerState extends State { + @override + void initState() { + super.initState(); + _checkInitialConnectivity(); + } + + Future _checkInitialConnectivity() async { + final isOnline = await ConnectivityService().hasConnection(); + + if (!mounted) return; + + final auth = Provider.of(context, listen: false); + + if (!auth.isLoggedIn) return; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + + if (!isOnline) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Offline Mode: Session loaded from cache.'), + backgroundColor: Colors.orange, + behavior: SnackBarBehavior.floating, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('System Online. Syncing data...'), + backgroundColor: Color(0xFF4ADE80), + behavior: SnackBarBehavior.floating, + duration: Duration(seconds: 2), + ), + ); + } + }); + } + + @override + Widget build(BuildContext context) { + final auth = context.watch(); + return auth.isLoggedIn ? const MainWrapper() : const LoginScreen(); + } +} diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart new file mode 100644 index 0000000..df059b5 --- /dev/null +++ b/lib/providers/auth_provider.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AuthProvider with ChangeNotifier { + final _storage = const FlutterSecureStorage(); + bool _isLoggedIn = false; + String? _userEmail; + + bool get isLoggedIn => _isLoggedIn; + String? get userEmail => _userEmail; + + Future checkAuth() async { + final prefs = await SharedPreferences.getInstance(); + _isLoggedIn = prefs.getBool('isLoggedIn') ?? false; + _userEmail = await _storage.read(key: 'user_email'); + notifyListeners(); + } + + Future login(String email, String password) async { + final prefs = await SharedPreferences.getInstance(); + + await prefs.setBool('isLoggedIn', true); + await _storage.write(key: 'user_email', value: email); + await _storage.write(key: 'user_password', value: password); + + _isLoggedIn = true; + _userEmail = email; + notifyListeners(); + } + + Future logout() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('isLoggedIn', false); + await _storage.deleteAll(); + + _isLoggedIn = false; + _userEmail = null; + notifyListeners(); + } +} diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart index db90a91..55b9255 100644 --- a/lib/screens/auth/login_screen.dart +++ b/lib/screens/auth/login_screen.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/providers/auth_provider.dart'; import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/connectivity_service.dart'; import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; import 'package:mobile_flutter_iot/widgets/glass_card.dart'; import 'package:mobile_flutter_iot/widgets/glass_input.dart'; import 'package:mobile_flutter_iot/widgets/primary_button.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:provider/provider.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -16,7 +18,7 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); - + final _connectivity = ConnectivityService(); final _userRepository = LocalUserRepository(); void _handleLogin() async { @@ -28,13 +30,23 @@ class _LoginScreenState extends State { return; } + final bool isOnline = await _connectivity.hasConnection(); + if (!mounted) return; + + if (!isOnline) { + _showStatusMessage('No Internet! Access denied.', isError: true); + return; + } + final isValid = await _userRepository.validateCredentials(email, password); + if (!mounted) return; - if (isValid) { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('isLoggedIn', true); + if (isValid) { if (mounted) { + await context.read().login(email, password); + if (!mounted) return; + _showStatusMessage('Access Granted. Welcome back!'); Navigator.pushReplacementNamed(context, '/main'); } @@ -49,8 +61,9 @@ class _LoginScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), - backgroundColor: isError ? Colors.redAccent : Colors.green, + backgroundColor: isError ? Colors.redAccent : const Color(0xFF4ADE80), duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, ), ); } @@ -96,7 +109,7 @@ class _LoginScreenState extends State { ), ), const Text( - 'ELEVATOR MONITOR v1.0', + 'IOT NODE TERMINAL v2.0', style: TextStyle(color: Colors.white30, fontSize: 12), ), const SizedBox(height: 40), diff --git a/lib/screens/auth/register_screen.dart b/lib/screens/auth/register_screen.dart index 5f25ead..390a0b7 100644 --- a/lib/screens/auth/register_screen.dart +++ b/lib/screens/auth/register_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile_flutter_iot/models/user_model.dart'; import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/connectivity_service.dart'; import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; import 'package:mobile_flutter_iot/widgets/glass_card.dart'; import 'package:mobile_flutter_iot/widgets/glass_input.dart'; @@ -18,6 +19,7 @@ class _RegisterScreenState extends State { final _emailController = TextEditingController(); final _deptController = TextEditingController(); final _passwordController = TextEditingController(); + final _connectivity = ConnectivityService(); final _userRepository = LocalUserRepository(); @@ -27,6 +29,15 @@ class _RegisterScreenState extends State { String? _passwordError; void _handleRegister() async { + final bool isOnline = await _connectivity.hasConnection(); + if (!isOnline) { + _showStatusMessage( + 'No Internet connection to sync account', + isError: true, + ); + return; + } + setState(() { _nameError = null; _emailError = null; @@ -45,7 +56,6 @@ class _RegisterScreenState extends State { setState(() => _nameError = 'Please enter your full name'); hasError = true; } - if (email.isEmpty) { setState(() => _emailError = 'Email address is required'); hasError = true; @@ -53,12 +63,10 @@ class _RegisterScreenState extends State { setState(() => _emailError = 'Invalid email format (missing @ or .)'); hasError = true; } - if (dept.isEmpty) { - setState(() => _deptError = 'Specify your university department'); + setState(() => _deptError = 'Specify your department'); hasError = true; } - if (password.isEmpty) { setState(() => _passwordError = 'Security password is required'); hasError = true; @@ -79,16 +87,21 @@ class _RegisterScreenState extends State { await _userRepository.saveUser(newUser); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Access Key Created! You can now log in.'), - backgroundColor: Color(0xFF4ADE80), - ), - ); + _showStatusMessage('Access Key Created! You can now log in.'); Navigator.pop(context); } } + void _showStatusMessage(String message, {bool isError = false}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: isError ? Colors.redAccent : const Color(0xFF4ADE80), + duration: const Duration(seconds: 2), + ), + ); + } + @override void dispose() { _nameController.dispose(); From 7b99598f7a820cb97f4250e8e9c4550b91f49fb9 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:38:31 +0200 Subject: [PATCH 3/6] Add mqtt provider and iot dashboard logic --- lib/providers/mqtt_provider.dart | 103 ++++++++ lib/screens/home/dashboard_screen.dart | 349 ++++++++++++++++--------- lib/screens/home/details_screen.dart | 113 ++++++-- lib/widgets/indicator.dart | 22 +- test/widget_test.dart | 18 +- 5 files changed, 463 insertions(+), 142 deletions(-) create mode 100644 lib/providers/mqtt_provider.dart diff --git a/lib/providers/mqtt_provider.dart b/lib/providers/mqtt_provider.dart new file mode 100644 index 0000000..158ac9d --- /dev/null +++ b/lib/providers/mqtt_provider.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:mqtt_client/mqtt_client.dart'; +import 'package:mqtt_client/mqtt_server_client.dart'; + +enum MqttStatus { disconnected, connecting, connected, error } + +class MqttProvider with ChangeNotifier { + MqttServerClient? client; + MqttStatus _status = MqttStatus.disconnected; + String _airQuality = '0'; + bool _isLedOn = false; + + MqttStatus get status => _status; + String get airQuality => _airQuality; + bool get isLedOn => _isLedOn; + + void initMqtt(String broker, String clientId) { + client = MqttServerClient(broker, clientId); + client!.port = 1883; + client!.logging(on: false); + client!.keepAlivePeriod = 20; + client!.autoReconnect = true; + + client!.onDisconnected = () { + _status = MqttStatus.disconnected; + notifyListeners(); + }; + + client!.onConnected = () { + _status = MqttStatus.connected; + _subscribeToTopics(); + notifyListeners(); + }; + + client!.onAutoReconnect = () => debugPrint('MQTT: Auto reconnecting...'); + client!.onAutoReconnected = () => debugPrint('MQTT: Auto reconnected'); + } + + Future connect() async { + if (client == null) return; + if (_status == MqttStatus.connected || _status == MqttStatus.connecting) { + return; + } + + _status = MqttStatus.connecting; + notifyListeners(); + + try { + await client!.connect(); + } catch (e) { + debugPrint('MQTT ERROR: $e'); + _status = MqttStatus.error; + if (client?.connectionStatus?.state != MqttConnectionState.disconnected) { + client?.disconnect(); + } + notifyListeners(); + } + } + + void _subscribeToTopics() { + if (client == null || _status != MqttStatus.connected) return; + + client!.subscribe('sensors/air', MqttQos.atMostOnce); + + client!.updates!.listen( + (List> messages) { + final recMess = messages[0].payload as MqttPublishMessage; + final payload = + MqttPublishPayload.bytesToStringAsString(recMess.payload.message); + _airQuality = payload; + notifyListeners(); + }, + onError: (Object error) { + debugPrint('MQTT Stream Error: $error'); + }, + cancelOnError: false, + ); + } + + void toggleLed() { + if (client == null || _status != MqttStatus.connected) return; + try { + _isLedOn = !_isLedOn; + final builder = MqttClientPayloadBuilder(); + builder.addString(_isLedOn ? 'ON' : 'OFF'); + client! + .publishMessage('commands/led', MqttQos.atMostOnce, builder.payload!); + notifyListeners(); + } catch (e) { + debugPrint('MQTT Publish Error: $e'); + } + } + + void disconnect() { + try { + client?.disconnect(); + } catch (e) { + debugPrint('MQTT Disconnect Error: $e'); + } + _status = MqttStatus.disconnected; + notifyListeners(); + } +} diff --git a/lib/screens/home/dashboard_screen.dart b/lib/screens/home/dashboard_screen.dart index 7a95267..1a92b35 100644 --- a/lib/screens/home/dashboard_screen.dart +++ b/lib/screens/home/dashboard_screen.dart @@ -1,13 +1,17 @@ +import 'dart:async'; import 'dart:math'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:mobile_flutter_iot/models/device_model.dart'; +import 'package:mobile_flutter_iot/providers/mqtt_provider.dart'; import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; import 'package:mobile_flutter_iot/screens/home/add_device_screen.dart'; import 'package:mobile_flutter_iot/screens/home/details_screen.dart'; import 'package:mobile_flutter_iot/widgets/glass_card.dart'; import 'package:mobile_flutter_iot/widgets/indicator.dart'; import 'package:mobile_flutter_iot/widgets/workspace_card.dart'; +import 'package:provider/provider.dart'; import 'package:shake/shake.dart'; class DashboardScreen extends StatefulWidget { @@ -23,139 +27,148 @@ class _DashboardScreenState extends State { bool _isLoading = true; late ShakeDetector _shakeDetector; + StreamSubscription>? _connectivitySubscription; + @override void initState() { super.initState(); _loadDevices(); + _initConnectivityMonitoring(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _safelyConnectMQTT(); + }); _shakeDetector = ShakeDetector.autoStart( - onPhoneShake: (_) { - _handleShake(); - }, + onPhoneShake: (_) => _handleShake(), shakeThresholdGravity: 1.5, ); } - @override - void dispose() { - _shakeDetector.stopListening(); - super.dispose(); - } + bool _isFirstCheck = true; - void _handleShake() { - () async { - if (!mounted || _devices.isEmpty) return; + void _initConnectivityMonitoring() { + _connectivitySubscription = Connectivity().onConnectivityChanged.listen( + (List results) { + if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Row( - children: [ - Icon(Icons.auto_fix_high, color: Colors.white, size: 20), - SizedBox(width: 12), - Text('SHAKE DETECTED: Simulating live data...'), - ], - ), - backgroundColor: const Color(0xFF38BDF8).withValues(alpha: 0.8), - behavior: SnackBarBehavior.floating, - duration: const Duration(seconds: 1), - ), - ); + final bool hasNet = !results.contains(ConnectivityResult.none); + final mqtt = Provider.of(context, listen: false); - final random = Random(); - for (var device in _devices) { - if (device.title.toLowerCase().contains('temp')) { - device.value = '${20 + random.nextInt(10)}°C'; - } else if (device.title.toLowerCase().contains('humidity')) { - device.value = '${40 + random.nextInt(20)}%'; + if (!hasNet) { + _isFirstCheck = false; + _showStatusBanner( + 'OFFLINE: Check your Wi-Fi connection', + Colors.redAccent, + ); + mqtt.disconnect(); } else { - device.value = '${random.nextInt(100)} units'; + if (!_isFirstCheck) { + _showStatusBanner('ONLINE: Connection restored!', Colors.green); + } + _isFirstCheck = false; + + debugPrint('Network is back! Attempting MQTT Sync...'); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) _safelyConnectMQTT(); + }); } - } + }, + ); + } - await _syncData(); - _loadDevices(); - }(); + void _showStatusBanner(String message, Color color) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + color == Colors.green ? Icons.wifi : Icons.wifi_off, + color: Colors.white, + ), + const SizedBox(width: 12), + Text(message), + ], + ), + backgroundColor: color.withValues(alpha: 0.9), + behavior: SnackBarBehavior.floating, + ), + ); } - Future _loadDevices() async { - final savedDevices = await _repository.getDevices(); - if (!mounted) return; - setState(() { - _devices = savedDevices; - _isLoading = false; - }); + void _safelyConnectMQTT() async { + final mqtt = Provider.of(context, listen: false); + if (mqtt.client == null) { + mqtt.initMqtt('192.168.1.XXX', 'flutter_client_${Random().nextInt(100)}'); + } + + final connectivity = await Connectivity().checkConnectivity(); + if (!connectivity.contains(ConnectivityResult.none)) { + mqtt.connect(); + } } - Future _syncData() async { - await _repository.saveDevices(_devices); + @override + void dispose() { + _shakeDetector.stopListening(); + _connectivitySubscription?.cancel(); + super.dispose(); } - void _onAddPressed() async { - final DeviceModel? result = await Navigator.push( - context, - MaterialPageRoute(builder: (context) => const AddDeviceScreen()), + void _handleShake() async { + if (!mounted || _devices.isEmpty) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('SHAKE: Local nodes refreshed'), + duration: Duration(seconds: 1), + ), ); - if (!mounted) return; - if (result != null) { - setState(() => _devices.add(result)); - await _syncData(); + final random = Random(); + for (var device in _devices) { + device.value = device.title.toLowerCase().contains('temp') + ? '${20 + random.nextInt(10)}°C' + : '${random.nextInt(100)} units'; } + await _syncData(); + _loadDevices(); } - void _onEditDevice(DeviceModel device, int index) async { - final DeviceModel? result = await Navigator.push( - context, - MaterialPageRoute(builder: (context) => AddDeviceScreen(device: device)), - ); + Future _loadDevices() async { + final savedDevices = await _repository.getDevices(); if (!mounted) return; - if (result != null) { - setState(() => _devices[index] = result); - await _syncData(); - } + setState(() { + _devices = savedDevices; + _isLoading = false; + }); } - void _onDeleteDevice(int index) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: const Color(0xFF1E293B), - title: const Text('Delete Sensor?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('CANCEL'), - ), - TextButton( - onPressed: () async { - setState(() => _devices.removeAt(index)); - await _syncData(); - if (context.mounted) Navigator.pop(context); - }, - child: const Text( - 'DELETE', - style: TextStyle(color: Colors.redAccent), - ), - ), - ], - ), - ); + Future _syncData() async { + await _repository.saveDevices(_devices); } @override Widget build(BuildContext context) { + final mqtt = context.watch(); + final bool isMqttLive = mqtt.status == MqttStatus.connected; + return Scaffold( backgroundColor: Colors.transparent, body: SafeArea( child: Column( children: [ _buildAppBar(), - _buildStatusCard(), + _buildStatusCard(mqtt), + if (isMqttLive) ...[ + _buildMqttLiveNode(mqtt), + _buildMqttControlNode(mqtt), + ], Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) - : _devices.isEmpty - ? _buildEmptyState() - : _buildDeviceList(), + : (_devices.isEmpty && !isMqttLive) + ? _buildEmptyState() + : _buildDeviceList(mqtt), ), ], ), @@ -180,23 +193,40 @@ class _DashboardScreenState extends State { ); } - Widget _buildStatusCard() { - return const Padding( - padding: EdgeInsets.all(16), + Widget _buildStatusCard(MqttProvider mqtt) { + final bool isConnected = mqtt.status == MqttStatus.connected; + + final Color statusColor = mqtt.status == MqttStatus.connected + ? const Color(0xFF4ADE80) + : const Color(0xFFF87171); + return Padding( + padding: const EdgeInsets.all(16), child: GlassCard( - padding: EdgeInsets.symmetric(vertical: 12, horizontal: 20), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), child: Row( children: [ - SystemPulseIndicator(), - SizedBox(width: 12), - Text( - 'SYSTEM: ONLINE', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + SystemPulseIndicator(color: statusColor), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'SYSTEM ENGINE', + style: TextStyle(fontSize: 10, color: Colors.white38), + ), + Text( + isConnected ? 'ONLINE' : 'MQTT DISCONNECTED', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ], ), - Spacer(), + const Spacer(), Text( - 'MQTT ACTIVE', - style: TextStyle(color: Color(0xFF4ADE80), fontSize: 10), + mqtt.client?.server ?? 'No IP', + style: const TextStyle(fontSize: 10, color: Colors.white24), ), ], ), @@ -204,30 +234,95 @@ class _DashboardScreenState extends State { ); } + Widget _buildMqttLiveNode(MqttProvider mqtt) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: GestureDetector( + onTap: () { + Navigator.pushNamed( + context, + '/details', + arguments: SensorArguments( + id: 'ESP_AIR_01', + title: 'Air Quality', + value: '${mqtt.airQuality} AQI', + icon: Icons.air_rounded, + color: const Color(0xFF38BDF8), + ipAddress: mqtt.client?.server ?? '192.168.1.XXX', + ), + ); + }, + child: WorkspaceCard( + id: 'ESP_AIR_01', + title: 'Air Quality (ESP8266)', + value: '${mqtt.airQuality} AQI', + status: 'LIVE', + subtitle: 'Real-time MQTT data', + icon: Icons.air_rounded, + accentColor: const Color(0xFF38BDF8), + ), + ), + ); + } + + Widget _buildMqttControlNode(MqttProvider mqtt) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: GlassCard( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: ListTile( + contentPadding: EdgeInsets.zero, + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: mqtt.isLedOn + ? Colors.yellow.withValues(alpha: 0.1) + : Colors.white.withValues(alpha: 0.05), + shape: BoxShape.circle, + ), + child: Icon( + mqtt.isLedOn ? Icons.lightbulb : Icons.lightbulb_outline, + color: mqtt.isLedOn ? Colors.yellow : Colors.white24, + ), + ), + title: const Text( + 'Smart LED System', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + subtitle: Text( + mqtt.isLedOn ? 'Active (ON)' : 'Inactive (OFF)', + style: const TextStyle(fontSize: 11, color: Colors.white38), + ), + trailing: Switch( + value: mqtt.isLedOn, + activeThumbColor: Colors.yellow, + onChanged: (_) => mqtt.toggleLed(), + ), + ), + ), + ); + } + Widget _buildEmptyState() { return const Center( child: Text( - 'No devices found.\nTap + to add.', + 'No active nodes found.\nConnect ESP8266 or add manually.', textAlign: TextAlign.center, style: TextStyle(color: Colors.white30), ), ); } - Widget _buildDeviceList() { + Widget _buildDeviceList(MqttProvider mqtt) { return ListView.separated( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), itemCount: _devices.length, separatorBuilder: (context, index) => const SizedBox(height: 16), itemBuilder: (context, index) { - if (index >= _devices.length) return const SizedBox.shrink(); final device = _devices[index]; - final String shortId = device.id.length > 8 - ? device.id.substring(0, 8) - : device.id; return GestureDetector( onTap: () async { - final result = await Navigator.pushNamed( + await Navigator.pushNamed( context, '/details', arguments: SensorArguments( @@ -236,14 +331,10 @@ class _DashboardScreenState extends State { value: device.value, icon: device.icon, color: device.color, + ipAddress: mqtt.client?.server ?? '192.168.1.XXX', ), ); - if (!mounted) return; - if (result is Map && result.containsKey('deleteId')) { - _onDeleteDevice(index); - } else { - _loadDevices(); - } + if (mounted) _loadDevices(); }, onLongPress: () => _onEditDevice(device, index), child: WorkspaceCard( @@ -251,7 +342,7 @@ class _DashboardScreenState extends State { title: device.title, value: device.value, status: device.status, - subtitle: 'ID: $shortId', + subtitle: 'Local simulation', icon: device.icon, accentColor: device.color, ), @@ -259,4 +350,28 @@ class _DashboardScreenState extends State { }, ); } + + void _onAddPressed() async { + final result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => const AddDeviceScreen()), + ); + if (mounted && result != null) { + setState(() => _devices.add(result)); + _syncData(); + } + } + + void _onEditDevice(DeviceModel device, int index) async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddDeviceScreen(device: device), + ), + ); + if (mounted && result != null) { + setState(() => _devices[index] = result); + _syncData(); + } + } } diff --git a/lib/screens/home/details_screen.dart b/lib/screens/home/details_screen.dart index 46ca3cf..ef7805d 100644 --- a/lib/screens/home/details_screen.dart +++ b/lib/screens/home/details_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/providers/mqtt_provider.dart'; import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; import 'package:mobile_flutter_iot/widgets/glass_card.dart'; import 'package:mobile_flutter_iot/widgets/sensor_chart.dart'; +import 'package:provider/provider.dart'; class SensorArguments { final String id; @@ -10,6 +12,7 @@ class SensorArguments { final IconData icon; final Color color; final String status; + final String ipAddress; SensorArguments({ required this.id, @@ -18,6 +21,7 @@ class SensorArguments { required this.icon, required this.color, this.status = 'Stable', + this.ipAddress = '192.168.1.XXX', }); } @@ -31,22 +35,26 @@ class DetailsScreen extends StatefulWidget { class _DetailsScreenState extends State { bool _isManualControlOn = true; String? _currentValue; + String? _customIp; final _userRepository = LocalUserRepository(); - Future _editValue(SensorArguments args) async { - final controller = TextEditingController(text: _currentValue ?? args.value); + Future _editIpAddress(SensorArguments args) async { + final mqtt = Provider.of(context, listen: false); + final controller = TextEditingController(text: _customIp ?? args.ipAddress); - final newValue = await showDialog( + final newIp = await showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: const Color(0xFF1E293B), - title: Text('Edit ${args.title} Value'), + title: const Text('Edit Device IP / Broker'), content: TextField( controller: controller, autofocus: true, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( - hintText: 'Enter new value (e.g. 25.5°C)', + hintText: 'e.g. 192.168.1.XXX', + labelText: 'Target IP Address', + labelStyle: TextStyle(color: Colors.white38), enabledBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.white24), ), @@ -60,24 +68,67 @@ class _DetailsScreenState extends State { TextButton( onPressed: () => Navigator.pop(context, controller.text), child: const Text( - 'Save', - style: TextStyle(color: Color(0xFF38BDF8)), + 'Update & Reconnect', + style: TextStyle(color: Color(0xFF4ADE80)), ), ), ], ), ); + + if (!mounted) return; + + if (newIp != null && newIp.isNotEmpty) { + setState(() => _customIp = newIp); + + mqtt.disconnect(); + mqtt.initMqtt(newIp, 'flutter_client_reconnect'); + mqtt.connect(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Reconnecting to $newIp...'), + backgroundColor: Colors.blueGrey, + ), + ); + } + } + + Future _editValue(SensorArguments args) async { + final controller = TextEditingController(text: _currentValue ?? args.value); + final newValue = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + title: Text('Edit ${args.title} Value'), + content: TextField( + controller: controller, + autofocus: true, + style: const TextStyle(color: Colors.white), + decoration: + const InputDecoration(hintText: 'Enter new value (e.g. 25.5°C)'), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, controller.text), + child: + const Text('Save', style: TextStyle(color: Color(0xFF38BDF8))), + ), + ], + ), + ); if (newValue != null && newValue.isNotEmpty) { final devices = await _userRepository.getDevices(); final index = devices.indexWhere((d) => d.id == args.id); - if (index != -1) { devices[index].value = newValue; await _userRepository.saveDevices(devices); - setState(() { - _currentValue = newValue; - }); + setState(() => _currentValue = newValue); } } } @@ -122,6 +173,39 @@ class _DetailsScreenState extends State { ), ), const SizedBox(height: 20), + GestureDetector( + onTap: () => _editIpAddress(args), + child: GlassCard( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 15), + child: Row( + children: [ + const Icon(Icons.lan_outlined, color: Colors.white38), + const SizedBox(width: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'DEVICE IP / BROKER', + style: TextStyle(fontSize: 10, color: Colors.white38), + ), + Text( + _customIp ?? args.ipAddress, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const Spacer(), + const Icon( + Icons.edit_note, + color: Color(0xFF38BDF8), + size: 20, + ), + ], + ), + ), + ), + const SizedBox(height: 20), Row( children: [ Expanded( @@ -160,12 +244,7 @@ class _DetailsScreenState extends State { ), trailing: Switch( value: _isManualControlOn, - activeThumbColor: args.color.withValues(alpha: 0.3), - onChanged: (v) { - setState(() { - _isManualControlOn = v; - }); - }, + onChanged: (v) => setState(() => _isManualControlOn = v), ), ), ), diff --git a/lib/widgets/indicator.dart b/lib/widgets/indicator.dart index 8c9da42..491026b 100644 --- a/lib/widgets/indicator.dart +++ b/lib/widgets/indicator.dart @@ -1,8 +1,12 @@ -// Віджет пульсуючого індикатора для Header import 'package:flutter/material.dart'; class SystemPulseIndicator extends StatefulWidget { - const SystemPulseIndicator({super.key}); + final Color color; + + const SystemPulseIndicator({ + required this.color, + super.key, + }); @override State createState() => _SystemPulseIndicatorState(); @@ -17,7 +21,7 @@ class _SystemPulseIndicatorState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(seconds: 2), + duration: const Duration(seconds: 1), )..repeat(reverse: true); } @@ -34,10 +38,16 @@ class _SystemPulseIndicatorState extends State child: Container( width: 10, height: 10, - decoration: const BoxDecoration( - color: Color(0xFF4ADE80), + decoration: BoxDecoration( + color: widget.color, shape: BoxShape.circle, - boxShadow: [BoxShadow(color: Color(0xFF4ADE80), blurRadius: 8)], + boxShadow: [ + BoxShadow( + color: widget.color.withValues(alpha: 0.6), + blurRadius: 8, + spreadRadius: 2, + ), + ], ), ), ); diff --git a/test/widget_test.dart b/test/widget_test.dart index 3a48e54..7187fc6 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -4,12 +4,26 @@ // utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. + import 'package:flutter_test/flutter_test.dart'; import 'package:mobile_flutter_iot/main.dart'; +import 'package:mobile_flutter_iot/providers/auth_provider.dart'; +import 'package:mobile_flutter_iot/providers/mqtt_provider.dart'; +import 'package:provider/provider.dart'; void main() { testWidgets('Auth screens load test', (WidgetTester tester) async { - await tester.pumpWidget(const SmartWorkspaceApp(isLoggedIn: false)); + final authProvider = AuthProvider(); + + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: authProvider), + ChangeNotifierProvider(create: (_) => MqttProvider()), + ], + child: const SmartWorkspaceApp(), + ), + ); expect(find.text('SMART WORKSPACE'), findsOneWidget); expect(find.text('INITIALIZE LOGIN'), findsOneWidget); @@ -22,6 +36,6 @@ void main() { await tester.tap(registerButton); await tester.pumpAndSettle(); - expect(find.text('CREATE ACCOUNT'), findsOneWidget); + expect(find.text('CREATE ACCESS KEY'), findsOneWidget); }); } From c333811331398b27af5e4d8b7cd7805bf0682f16 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:40:20 +0200 Subject: [PATCH 4/6] Update profile screen with provider --- lib/profile/profile_screen.dart | 446 +++++++++++++++----------------- 1 file changed, 214 insertions(+), 232 deletions(-) diff --git a/lib/profile/profile_screen.dart b/lib/profile/profile_screen.dart index 27cab65..146a616 100644 --- a/lib/profile/profile_screen.dart +++ b/lib/profile/profile_screen.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:mobile_flutter_iot/models/user_model.dart'; +import 'package:mobile_flutter_iot/providers/auth_provider.dart'; import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; import 'package:mobile_flutter_iot/widgets/glass_card.dart'; import 'package:mobile_flutter_iot/widgets/profile_item.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:provider/provider.dart'; class ProfileScreen extends StatefulWidget { const ProfileScreen({super.key}); @@ -16,7 +17,6 @@ class ProfileScreen extends StatefulWidget { class _ProfileScreenState extends State { bool _notifications = true; bool _darkMode = true; - bool _isAutoLoginEnabled = false; UserModel? _currentUser; final _userRepository = LocalUserRepository(); @@ -29,11 +29,54 @@ class _ProfileScreenState extends State { Future _loadUserData() async { final user = await _userRepository.getUser(); - final prefs = await SharedPreferences.getInstance(); - setState(() { - _currentUser = user; - _isAutoLoginEnabled = prefs.getBool('isLoggedIn') ?? false; - }); + if (mounted) { + setState(() { + _currentUser = user; + }); + } + } + + void _showLogoutDialog() { + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + backgroundColor: const Color(0xFF1E293B), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: const Text( + 'System Termination', + style: TextStyle(color: Colors.white), + ), + content: const Text('Are you sure you want to logout?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () async { + final auth = Provider.of(context, listen: false); + + Navigator.pop(dialogContext); + + await auth.logout(); + + if (mounted) { + Navigator.pushNamedAndRemoveUntil( + context, + '/login', + (route) => false, + ); + } + }, + child: const Text('LOGOUT', + style: TextStyle(color: Colors.redAccent),), + ), + ], + ); + }, + ); } Future _deleteAccount() async { @@ -50,30 +93,24 @@ class _ProfileScreenState extends State { ], ), content: const Text( - 'This will permanently erase your encryption keys, ' - 'saved devices, and local profile. Continue?', + 'This will permanently erase your encryption keys ' + 'and local profile. Continue?', style: TextStyle(color: Colors.white70), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text( - 'CANCEL', - style: TextStyle(color: Colors.white38), - ), + child: + const Text('CANCEL', style: TextStyle(color: Colors.white38)), ), - Container( - margin: const EdgeInsets.only(right: 8), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF87171).withValues(alpha: 0.2), - foregroundColor: const Color(0xFFF87171), - elevation: 0, - side: const BorderSide(color: Color(0xFFF87171)), - ), - onPressed: () => Navigator.pop(context, true), - child: const Text('CONFIRM PURGE'), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFF87171).withValues(alpha: 0.2), + foregroundColor: const Color(0xFFF87171), + side: const BorderSide(color: Color(0xFFF87171)), ), + onPressed: () => Navigator.pop(context, true), + child: const Text('CONFIRM PURGE'), ), ], ), @@ -82,7 +119,14 @@ class _ProfileScreenState extends State { if (confirm == true) { await _userRepository.deleteUser(); if (mounted) { - Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false); + await context.read().logout(); + if (mounted) { + Navigator.pushNamedAndRemoveUntil( + context, + '/login', + (route) => false, + ); + } } } } @@ -93,28 +137,20 @@ class _ProfileScreenState extends State { void Function(String) onSave, ) async { final controller = TextEditingController(text: currentValue); - return showDialog( + return showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: const Color(0xFF1E293B), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - title: Text( - 'Update $title', - style: const TextStyle(color: Colors.white, fontSize: 18), - ), + title: + Text('Update $title', style: const TextStyle(color: Colors.white)), content: TextField( controller: controller, - autofocus: true, style: const TextStyle(color: Colors.white), decoration: InputDecoration( hintText: 'Enter new $title', - hintStyle: const TextStyle(color: Colors.white24), enabledBorder: const UnderlineInputBorder( borderSide: BorderSide(color: Colors.white24), ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Color(0xFF38BDF8)), - ), ), ), actions: [ @@ -124,16 +160,12 @@ class _ProfileScreenState extends State { ), TextButton( onPressed: () { - if (controller.text.isNotEmpty) { - onSave(controller.text); - Navigator.pop(context); - _loadUserData(); - } + onSave(controller.text); + Navigator.pop(context); + _loadUserData(); }, - child: const Text( - 'Save Changes', - style: TextStyle(color: Color(0xFF38BDF8)), - ), + child: + const Text('Save', style: TextStyle(color: Color(0xFF38BDF8))), ), ], ), @@ -143,7 +175,7 @@ class _ProfileScreenState extends State { @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; - final isWide = size.width > 600; + final auth = context.watch(); return Scaffold( backgroundColor: Colors.transparent, @@ -161,21 +193,33 @@ class _ProfileScreenState extends State { color: const Color(0xFF4ADE80).withValues(alpha: 0.08), size: size.width * 0.8, ), - CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ - _buildAppBar(isWide), - SliverPadding( - padding: EdgeInsets.symmetric( - horizontal: isWide ? size.width * 0.2 : 24, - vertical: 20, + const SliverAppBar( + expandedHeight: 100, + pinned: true, + backgroundColor: Colors.transparent, + flexibleSpace: FlexibleSpaceBar( + centerTitle: true, + title: Text( + 'USER PROFILE', + style: TextStyle( + letterSpacing: 2, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), ), + ), + SliverPadding( + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 20), sliver: SliverList( delegate: SliverChildListDelegate([ - _buildHeader(isWide), + _buildHeader(), const SizedBox(height: 32), - _buildSecurityStatus(), + _buildSecurityStatus(auth.isLoggedIn), const SizedBox(height: 24), const Text( 'PREFERENCES', @@ -197,7 +241,7 @@ class _ProfileScreenState extends State { ), ), const SizedBox(height: 12), - _buildActionButtons(context), + _buildActionButtons(), const SizedBox(height: 40), ]), ), @@ -209,25 +253,7 @@ class _ProfileScreenState extends State { ); } - Widget _buildAppBar(bool isWide) => SliverAppBar( - expandedHeight: isWide ? 150 : 100, - pinned: true, - backgroundColor: Colors.transparent, - elevation: 0, - flexibleSpace: const FlexibleSpaceBar( - centerTitle: true, - title: Text( - 'USER PROFILE', - style: TextStyle( - letterSpacing: 2, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - - Widget _buildHeader(bool isWide) { + Widget _buildHeader() { final name = _currentUser?.fullName ?? 'Loading...'; final dept = _currentUser?.department ?? 'Unknown Department'; final email = _currentUser?.email ?? 'no-email@system.io'; @@ -243,21 +269,15 @@ class _ProfileScreenState extends State { const Color(0xFF38BDF8).withValues(alpha: 0.5), const Color(0xFF4ADE80).withValues(alpha: 0.5), ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, ), ), - child: CircleAvatar( - radius: isWide ? 70 : 55, - backgroundColor: const Color(0xFF0F172A), + child: const CircleAvatar( + radius: 55, + backgroundColor: Color(0xFF0F172A), child: CircleAvatar( - radius: isWide ? 66 : 51, - backgroundColor: const Color(0xFF1E293B), - child: Icon( - Icons.person_rounded, - size: isWide ? 70 : 50, - color: Colors.white, - ), + radius: 51, + backgroundColor: Color(0xFF1E293B), + child: Icon(Icons.person_rounded, size: 50, color: Colors.white), ), ), ), @@ -276,11 +296,7 @@ class _ProfileScreenState extends State { }), child: Text( name, - style: const TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), + style: const TextStyle(fontSize: 26, fontWeight: FontWeight.bold), ), ), const SizedBox(height: 4), @@ -289,14 +305,13 @@ class _ProfileScreenState extends State { style: const TextStyle( color: Color(0xFF4ADE80), fontSize: 12, - fontWeight: FontWeight.w600, - letterSpacing: 1, + fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), GestureDetector( onTap: () => _editProfileField('Email', email, (val) async { - if (val.contains('@') && _currentUser != null) { + if (_currentUser != null) { final updated = UserModel( fullName: _currentUser!.fullName, email: val, @@ -317,11 +332,7 @@ class _ProfileScreenState extends State { ), child: Text( email, - style: const TextStyle( - color: Color(0xFF38BDF8), - fontSize: 12, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF38BDF8), fontSize: 12), ), ), ), @@ -329,151 +340,122 @@ class _ProfileScreenState extends State { ); } - Widget _buildSecurityStatus() => GlassCard( - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: _isAutoLoginEnabled - ? const Color(0xFF4ADE80).withValues(alpha: 0.1) - : Colors.orangeAccent.withValues(alpha: 0.1), - shape: BoxShape.circle, - ), - child: Icon( - _isAutoLoginEnabled - ? Icons.verified_user_rounded - : Icons.shield_moon_outlined, - color: _isAutoLoginEnabled - ? const Color(0xFF4ADE80) - : Colors.orangeAccent, - size: 20, - ), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _buildSecurityStatus(bool isActive) => GlassCard( + child: Row( children: [ - const Text( - 'Security Protocol', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + Icon( + isActive + ? Icons.verified_user_rounded + : Icons.shield_moon_outlined, + color: isActive ? const Color(0xFF4ADE80) : Colors.orangeAccent, ), - Text( - _isAutoLoginEnabled - ? 'Persistent Session: Active' - : 'Session Encryption: Local Only', - style: const TextStyle(fontSize: 11, color: Colors.white38), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Security Protocol', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + isActive + ? 'Persistent Session: Active' + : 'Session Encryption: Local Only', + style: const TextStyle(fontSize: 11, color: Colors.white38), + ), + ], ), ], ), - ], - ), - ); + ); Widget _buildSettingsGroup() => GlassCard( - child: Column( - children: [ - ProfileMenuItem( - icon: Icons.notifications_active_outlined, - title: 'System Alerts', - isSwitch: true, - value: _notifications, - onChanged: (v) => setState(() => _notifications = v), - ), - const Divider(color: Colors.white10, height: 1), - ProfileMenuItem( - icon: Icons.palette_outlined, - title: 'OLED Dark Mode', - isSwitch: true, - value: _darkMode, - onChanged: (v) => setState(() => _darkMode = v), - ), - const Divider(color: Colors.white10, height: 1), - ProfileMenuItem( - icon: Icons.hub_outlined, - title: 'Department Unit', - trailingText: _currentUser?.department ?? '...', - onTap: () => _editProfileField( - 'Department', - _currentUser?.department ?? '', - (val) async { - if (_currentUser != null) { - final updated = UserModel( - fullName: _currentUser!.fullName, - email: _currentUser!.email, - password: _currentUser!.password, - department: val, - ); - await _userRepository.saveUser(updated); - _loadUserData(); - } - }, - ), - ), - ], - ), - ); - - Widget _buildActionButtons(BuildContext context) => Column( - children: [ - GlassCard( - padding: EdgeInsets.zero, - child: ListTile( - onTap: () async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('isLoggedIn', false); - if (context.mounted) { - Navigator.pushNamedAndRemoveUntil( - context, - '/login', - (route) => false, - ); - } - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - leading: const Icon(Icons.logout, color: Color(0xFFF87171)), - title: const Text( - 'Logout', - style: TextStyle( - color: Color(0xFFF87171), - fontWeight: FontWeight.bold, + child: Column( + children: [ + ProfileMenuItem( + icon: Icons.notifications_active_outlined, + title: 'System Alerts', + isSwitch: true, + value: _notifications, + onChanged: (v) => setState(() => _notifications = v), ), - ), - trailing: const Icon( - Icons.arrow_forward_ios_rounded, - size: 14, - color: Colors.white24, - ), + const Divider(color: Colors.white10), + ProfileMenuItem( + icon: Icons.palette_outlined, + title: 'OLED Dark Mode', + isSwitch: true, + value: _darkMode, + onChanged: (v) => setState(() => _darkMode = v), + ), + const Divider(color: Colors.white10), + ProfileMenuItem( + icon: Icons.hub_outlined, + title: 'Department Unit', + trailingText: _currentUser?.department ?? '...', + onTap: () => _editProfileField( + 'Department', _currentUser?.department ?? '', (val) async { + if (_currentUser != null) { + final updated = UserModel( + fullName: _currentUser!.fullName, + email: _currentUser!.email, + password: _currentUser!.password, + department: val, + ); + await _userRepository.saveUser(updated); + } + }), + ), + ], ), - ), - const SizedBox(height: 16), - GestureDetector( - onTap: _deleteAccount, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: const Color(0xFFF87171).withValues(alpha: 0.3), + ); + + Widget _buildActionButtons() => Column( + children: [ + GlassCard( + padding: EdgeInsets.zero, + child: ListTile( + onTap: _showLogoutDialog, + leading: const Icon(Icons.logout, color: Color(0xFFF87171)), + title: const Text( + 'Logout', + style: TextStyle( + color: Color(0xFFF87171), + fontWeight: FontWeight.bold, + ), + ), + trailing: const Icon( + Icons.arrow_forward_ios_rounded, + size: 14, + color: Colors.white24, + ), ), - color: const Color(0xFFF87171).withValues(alpha: 0.05), ), - child: const Center( - child: Text( - 'DELETE ACCOUNT ', - style: TextStyle( - color: Color(0xFFF87171), - fontSize: 11, - fontWeight: FontWeight.bold, - letterSpacing: 2, + const SizedBox(height: 16), + GestureDetector( + onTap: _deleteAccount, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: const Color(0xFFF87171).withValues(alpha: 0.3), + ), + color: const Color(0xFFF87171).withValues(alpha: 0.05), + ), + child: const Center( + child: Text( + 'DELETE ACCOUNT', + style: TextStyle( + color: Color(0xFFF87171), + fontSize: 11, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), ), ), ), - ), - ), - ], - ); + ], + ); } From 31806d0e69979a809cc79e94f2b6d6e385a0df5c Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:41:29 +0200 Subject: [PATCH 5/6] Update some screens and widgets to dart format --- lib/screens/home/add_device_screen.dart | 5 ++-- lib/screens/home/alerts_screen.dart | 2 -- lib/screens/home/control_screen.dart | 9 ++----- lib/screens/home/home_screen.dart | 33 +++++++++++-------------- lib/widgets/workspace_card.dart | 3 +-- 5 files changed, 19 insertions(+), 33 deletions(-) diff --git a/lib/screens/home/add_device_screen.dart b/lib/screens/home/add_device_screen.dart index ba0ec4b..cbeed93 100644 --- a/lib/screens/home/add_device_screen.dart +++ b/lib/screens/home/add_device_screen.dart @@ -134,9 +134,8 @@ class _AddDeviceScreenState extends State { : Colors.white.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all( - color: _selectedIcon == icon - ? _selectedColor - : Colors.transparent, + color: + _selectedIcon == icon ? _selectedColor : Colors.transparent, ), ), child: Icon( diff --git a/lib/screens/home/alerts_screen.dart b/lib/screens/home/alerts_screen.dart index 82d0610..895f32f 100644 --- a/lib/screens/home/alerts_screen.dart +++ b/lib/screens/home/alerts_screen.dart @@ -39,7 +39,6 @@ class AlertsScreen extends StatelessWidget { return Scaffold( extendBodyBehindAppBar: true, - appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, @@ -64,7 +63,6 @@ class AlertsScreen extends StatelessWidget { right: 100, child: _buildGlow(const Color(0xFF38BDF8).withValues(alpha: 0.05)), ), - ListView.separated( padding: const EdgeInsets.fromLTRB(20, 120, 20, 20), itemCount: alerts.length, diff --git a/lib/screens/home/control_screen.dart b/lib/screens/home/control_screen.dart index 1f0f381..c097d75 100644 --- a/lib/screens/home/control_screen.dart +++ b/lib/screens/home/control_screen.dart @@ -44,7 +44,6 @@ class _ControlScreenState extends State { color: const Color(0xFF38BDF8).withValues(alpha: 0.1), size: 250, ), - SafeArea( child: Center( child: SingleChildScrollView( @@ -64,14 +63,12 @@ class _ControlScreenState extends State { ), ), const SizedBox(height: 40), - const Text( 'Lighting System', style: TextStyle(color: Colors.white70), ), const SizedBox(height: 12), _buildLightingCard(), - const SizedBox(height: 32), const Text( 'Climate Control', @@ -99,7 +96,6 @@ class _ControlScreenState extends State { ), ], ), - const SizedBox(height: 48), PrimaryButton( text: 'EMERGENCY SHUTDOWN', @@ -135,9 +131,8 @@ class _ControlScreenState extends State { Slider( value: _brightness, activeColor: const Color(0xFF38BDF8), - onChanged: _isLightOn - ? (v) => setState(() => _brightness = v) - : null, + onChanged: + _isLightOn ? (v) => setState(() => _brightness = v) : null, ), ], ), diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index dd0ac0a..78f13b2 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; // Використання винесеного BlurBlob +import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; import 'package:mobile_flutter_iot/widgets/fan_widget.dart'; import 'package:mobile_flutter_iot/widgets/glass_card.dart'; -import 'package:mobile_flutter_iot/widgets/tech_node.dart'; // Імпорт нового віджета +import 'package:mobile_flutter_iot/widgets/tech_node.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -19,9 +19,8 @@ class _HomeScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(_isSystemOn ? 'SYSTEM INITIALIZED' : 'SYSTEM SHUTDOWN'), - backgroundColor: _isSystemOn - ? const Color(0xFF38BDF8) - : Colors.redAccent, + backgroundColor: + _isSystemOn ? const Color(0xFF38BDF8) : Colors.redAccent, duration: const Duration(seconds: 1), ), ); @@ -33,21 +32,17 @@ class _HomeScreenState extends State { backgroundColor: Colors.transparent, body: Stack( children: [ - Positioned( - top: -50, - left: -50, - child: BlurBlob( - color: const Color(0xFF38BDF8).withValues(alpha: 0.1), - size: 200, - ), + BlurBlob( + alignment: Alignment.topLeft, + translation: const Offset(-0.2, -0.3), + color: const Color(0xFF4ADE80).withValues(alpha: 0.08), + size: 280, ), - Positioned( - bottom: 100, - right: -80, - child: BlurBlob( - color: const Color(0xFF4ADE80).withValues(alpha: 0.05), - size: 250, - ), + BlurBlob( + alignment: Alignment.bottomRight, + translation: const Offset(0.3, 0.2), + color: const Color(0xFF38BDF8).withValues(alpha: 0.1), + size: 320, ), SafeArea( child: Column( diff --git a/lib/widgets/workspace_card.dart b/lib/widgets/workspace_card.dart index 8351531..004cfb8 100644 --- a/lib/widgets/workspace_card.dart +++ b/lib/widgets/workspace_card.dart @@ -78,8 +78,7 @@ class WorkspaceCard extends StatelessWidget { ), const SizedBox(height: 10), GestureDetector( - onTap: - onAnalyticsTap ?? + onTap: onAnalyticsTap ?? () { Navigator.pushNamed( context, From 62b4829789200c181ae125579c63b45f130a8b6a Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:48:10 +0200 Subject: [PATCH 6/6] Fix main.dart to dart format --- lib/main.dart | 2 +- lib/profile/profile_screen.dart | 6 ++++-- lib/screens/auth/login_screen.dart | 1 - lib/screens/home/details_screen.dart | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 47cce83..5ad33a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -70,7 +70,7 @@ class _RootHandlerState extends State { Future _checkInitialConnectivity() async { final isOnline = await ConnectivityService().hasConnection(); - + if (!mounted) return; final auth = Provider.of(context, listen: false); diff --git a/lib/profile/profile_screen.dart b/lib/profile/profile_screen.dart index 146a616..f39209d 100644 --- a/lib/profile/profile_screen.dart +++ b/lib/profile/profile_screen.dart @@ -70,8 +70,10 @@ class _ProfileScreenState extends State { ); } }, - child: const Text('LOGOUT', - style: TextStyle(color: Colors.redAccent),), + child: const Text( + 'LOGOUT', + style: TextStyle(color: Colors.redAccent), + ), ), ], ); diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart index 55b9255..27bf1b8 100644 --- a/lib/screens/auth/login_screen.dart +++ b/lib/screens/auth/login_screen.dart @@ -41,7 +41,6 @@ class _LoginScreenState extends State { final isValid = await _userRepository.validateCredentials(email, password); if (!mounted) return; - if (isValid) { if (mounted) { await context.read().login(email, password); diff --git a/lib/screens/home/details_screen.dart b/lib/screens/home/details_screen.dart index ef7805d..091d406 100644 --- a/lib/screens/home/details_screen.dart +++ b/lib/screens/home/details_screen.dart @@ -75,7 +75,7 @@ class _DetailsScreenState extends State { ], ), ); - + if (!mounted) return; if (newIp != null && newIp.isNotEmpty) {