From 703d41a54e349405217de55cbaafda2b0ef5a7a7 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:49:22 +0200 Subject: [PATCH 1/9] Add UserModel and abstract UserRepository --- lib/models/device_model.dart | 41 +++++++++++++++ lib/models/user_model.dart | 31 +++++++++++ lib/repository/local_user_repository.dart | 63 +++++++++++++++++++++++ lib/repository/user_repository.dart | 8 +++ 4 files changed, 143 insertions(+) create mode 100644 lib/models/device_model.dart create mode 100644 lib/models/user_model.dart create mode 100644 lib/repository/local_user_repository.dart create mode 100644 lib/repository/user_repository.dart diff --git a/lib/models/device_model.dart b/lib/models/device_model.dart new file mode 100644 index 0000000..698c474 --- /dev/null +++ b/lib/models/device_model.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class DeviceModel { + final String id; + String title; + String value; + String status; + IconData icon; + Color color; + + DeviceModel({ + required this.id, + required this.title, + required this.value, + required this.status, + required this.icon, + required this.color, + }); + + Map toMap() { + return { + 'id': id, + 'title': title, + 'value': value, + 'status': status, + 'icon': icon.codePoint, + 'color': color.value, + }; + } + + factory DeviceModel.fromMap(Map map) { + return DeviceModel( + id: map['id'] as String, + title: map['title'] as String, + value: map['value'] as String, + status: map['status'] as String, + icon: IconData(map['icon'] as int, fontFamily: 'MaterialIcons'), + color: Color(map['color'] as int), + ); + } +} diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart new file mode 100644 index 0000000..c9ec7fa --- /dev/null +++ b/lib/models/user_model.dart @@ -0,0 +1,31 @@ +class UserModel { + final String fullName; + final String email; + final String password; + final String department; + + UserModel({ + required this.fullName, + required this.email, + required this.password, + required this.department, + }); + + factory UserModel.fromJson(Map json) { + return UserModel( + fullName: json['fullName'] as String, + email: json['email'] as String, + password: json['password'] as String, + department: json['department'] as String, + ); + } + + Map toJson() { + return { + 'fullName': fullName, + 'email': email, + 'password': password, + 'department': department, + }; + } +} diff --git a/lib/repository/local_user_repository.dart b/lib/repository/local_user_repository.dart new file mode 100644 index 0000000..7a807c6 --- /dev/null +++ b/lib/repository/local_user_repository.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; + +import 'package:mobile_flutter_iot/models/device_model.dart'; +import 'package:mobile_flutter_iot/models/user_model.dart'; +import 'package:mobile_flutter_iot/repository/user_repository.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LocalUserRepository implements UserRepository { + static const String _userKey = 'user_data'; + static const String _devicesKey = 'devices_list'; + + @override + Future saveUser(UserModel user) async { + final prefs = await SharedPreferences.getInstance(); + final String userJson = jsonEncode(user.toJson()); + await prefs.setString(_userKey, userJson); + } + + @override + Future getUser() async { + final prefs = await SharedPreferences.getInstance(); + final String? userJson = prefs.getString(_userKey); + if (userJson == null) return null; + final Map userMap = + jsonDecode(userJson) as Map; + return UserModel.fromJson(userMap); + } + + @override + Future deleteUser() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_userKey); + await prefs.remove(_devicesKey); + await prefs.setBool('isLoggedIn', false); + } + + @override + Future validateCredentials(String email, String password) async { + final user = await getUser(); + if (user == null) return false; + return user.email == email && user.password == password; + } + + Future saveDevices(List devices) async { + final prefs = await SharedPreferences.getInstance(); + final String devicesJson = jsonEncode( + devices.map((d) => d.toMap()).toList(), + ); + await prefs.setString(_devicesKey, devicesJson); + } + + Future> getDevices() async { + final prefs = await SharedPreferences.getInstance(); + final String? devicesJson = prefs.getString(_devicesKey); + + if (devicesJson == null) return []; + + final List decoded = jsonDecode(devicesJson) as List; + return decoded + .map((item) => DeviceModel.fromMap(item as Map)) + .toList(); + } +} diff --git a/lib/repository/user_repository.dart b/lib/repository/user_repository.dart new file mode 100644 index 0000000..f89c6a2 --- /dev/null +++ b/lib/repository/user_repository.dart @@ -0,0 +1,8 @@ +import 'package:mobile_flutter_iot/models/user_model.dart'; + +abstract class UserRepository { + Future saveUser(UserModel user); + Future getUser(); + Future deleteUser(); + Future validateCredentials(String email, String password); +} From 0d1535f1b8eeb684f34b3b11e691afea78e8d9e4 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:50:03 +0200 Subject: [PATCH 2/9] Implement local auth with validation (Login/Register) --- lib/screens/auth/login_screen.dart | 83 +++++++++++++---- lib/screens/auth/register_screen.dart | 128 ++++++++++++++++++-------- 2 files changed, 155 insertions(+), 56 deletions(-) diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart index 1819365..db90a91 100644 --- a/lib/screens/auth/login_screen.dart +++ b/lib/screens/auth/login_screen.dart @@ -1,13 +1,67 @@ import 'package:flutter/material.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/glass_input.dart'; import 'package:mobile_flutter_iot/widgets/primary_button.dart'; import 'package:shared_preferences/shared_preferences.dart'; -class LoginScreen extends StatelessWidget { +class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + + final _userRepository = LocalUserRepository(); + + void _handleLogin() async { + final email = _emailController.text.trim(); + final password = _passwordController.text.trim(); + + if (email.isEmpty || password.isEmpty) { + _showStatusMessage('Please fill in all fields', isError: true); + return; + } + + final isValid = await _userRepository.validateCredentials(email, password); + + if (isValid) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('isLoggedIn', true); + + if (mounted) { + _showStatusMessage('Access Granted. Welcome back!'); + Navigator.pushReplacementNamed(context, '/main'); + } + } else { + if (mounted) { + _showStatusMessage('Invalid email or password', isError: true); + } + } + } + + void _showStatusMessage(String message, {bool isError = false}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: isError ? Colors.redAccent : Colors.green, + duration: const Duration(seconds: 2), + ), + ); + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -16,16 +70,15 @@ class LoginScreen extends StatelessWidget { BlurBlob( alignment: Alignment.topRight, translation: const Offset(0.2, -0.3), - color: const Color(0xFF38BDF8).withValues(alpha: 0.2), + color: const Color(0xFF38BDF8).withValues(alpha: 0.1), size: 250, ), BlurBlob( alignment: Alignment.bottomLeft, translation: const Offset(-0.3, 0.3), - color: const Color(0xFF4ADE80).withValues(alpha: 0.1), + color: const Color(0xFF4ADE80).withValues(alpha: 0.08), size: 300, ), - SafeArea( child: Center( child: SingleChildScrollView( @@ -47,7 +100,7 @@ class LoginScreen extends StatelessWidget { style: TextStyle(color: Colors.white30, fontSize: 12), ), const SizedBox(height: 40), - _buildLoginForm(context), + _buildForm(), ], ), ), @@ -72,32 +125,24 @@ class LoginScreen extends StatelessWidget { ); } - Widget _buildLoginForm(BuildContext context) { + Widget _buildForm() { return GlassCard( child: Column( children: [ - const GlassInput( + GlassInput( hintText: 'System ID / Email', icon: Icons.alternate_email, + controller: _emailController, ), const SizedBox(height: 16), - const GlassInput( + GlassInput( hintText: 'Password', icon: Icons.fingerprint, isPassword: true, + controller: _passwordController, ), const SizedBox(height: 24), - PrimaryButton( - text: 'INITIALIZE LOGIN', - onPressed: () async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('isLoggedIn', true); - - if (context.mounted) { - Navigator.pushReplacementNamed(context, '/main'); - } - }, - ), + PrimaryButton(text: 'INITIALIZE LOGIN', onPressed: _handleLogin), const SizedBox(height: 12), TextButton( onPressed: () => Navigator.pushNamed(context, '/register'), diff --git a/lib/screens/auth/register_screen.dart b/lib/screens/auth/register_screen.dart index f5e33ef..2ad923e 100644 --- a/lib/screens/auth/register_screen.dart +++ b/lib/screens/auth/register_screen.dart @@ -1,37 +1,101 @@ 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/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'; -class RegisterScreen extends StatelessWidget { +class RegisterScreen extends StatefulWidget { const RegisterScreen({super.key}); + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final _nameController = TextEditingController(); + final _emailController = TextEditingController(); + final _deptController = TextEditingController(); + final _passwordController = TextEditingController(); + + final _userRepository = LocalUserRepository(); + + void _handleRegister() async { + final name = _nameController.text.trim(); + final email = _emailController.text.trim(); + final dept = _deptController.text.trim(); + final password = _passwordController.text.trim(); + + if (name.isEmpty || email.isEmpty || dept.isEmpty || password.isEmpty) { + _showError('All fields are required'); + return; + } + + if (!email.contains('@') || !email.contains('.')) { + _showError('Please enter a valid email address'); + return; + } + + if (password.length < 6) { + _showError('Password must be at least 6 characters'); + return; + } + + final newUser = UserModel( + fullName: name, + email: email, + password: password, + department: dept, + ); + + await _userRepository.saveUser(newUser); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Registration Successful! Please login.')), + ); + Navigator.pop(context); + } + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message), backgroundColor: Colors.redAccent), + ); + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _deptController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ - const BlurBlob( + BlurBlob( alignment: Alignment.topLeft, - translation: Offset(-0.2, -0.3), - color: Color(0xFF4ADE80), + translation: const Offset(-0.2, -0.3), + color: const Color(0xFF4ADE80).withValues(alpha: 0.08), size: 280, ), - - const BlurBlob( + BlurBlob( alignment: Alignment.bottomRight, - translation: Offset(0.3, 0.2), - color: Color(0xFF38BDF8), + translation: const Offset(0.3, 0.2), + color: const Color(0xFF38BDF8).withValues(alpha: 0.1), size: 320, ), - SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 20), _buildTopIcon(), @@ -41,16 +105,10 @@ class RegisterScreen extends StatelessWidget { style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - letterSpacing: 2, ), ), - const Text( - 'Register your device in the network', - style: TextStyle(color: Colors.white30, fontSize: 12), - ), const SizedBox(height: 40), - - _buildRegisterForm(context), + _buildForm(), ], ), ), @@ -67,7 +125,7 @@ class RegisterScreen extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: const Color(0xFF4ADE80).withValues(alpha: 0.3), + color: const Color(0xFF4ADE80).withValues(alpha: 0.03), width: 2, ), ), @@ -79,46 +137,42 @@ class RegisterScreen extends StatelessWidget { ); } - Widget _buildRegisterForm(BuildContext context) { + Widget _buildForm() { return GlassCard( child: Column( - mainAxisSize: MainAxisSize.min, children: [ - const GlassInput( + GlassInput( hintText: 'Full Name', - icon: Icons.person_outline_rounded, + icon: Icons.person_outline, + controller: _nameController, ), const SizedBox(height: 16), - const GlassInput( + GlassInput( hintText: 'Email Address', - icon: Icons.alternate_email_rounded, + icon: Icons.alternate_email, + controller: _emailController, ), const SizedBox(height: 16), - const GlassInput( + GlassInput( hintText: 'Department (e.g., KSA, IoT)', - icon: Icons.business_center_outlined, + icon: Icons.business_center, + controller: _deptController, ), const SizedBox(height: 16), - const GlassInput( + GlassInput( hintText: 'Access Password', - icon: Icons.lock_open_rounded, + icon: Icons.lock_open, isPassword: true, + controller: _passwordController, ), const SizedBox(height: 24), - PrimaryButton( - text: 'CREATE ACCESS KEY', - onPressed: () => Navigator.pop(context), - ), + PrimaryButton(text: 'CREATE ACCESS KEY', onPressed: _handleRegister), const SizedBox(height: 12), TextButton( onPressed: () => Navigator.pop(context), child: const Text( 'RETURN TO ENTRANCE', - style: TextStyle( - color: Colors.white38, - fontSize: 11, - letterSpacing: 1, - ), + style: TextStyle(color: Colors.white38, fontSize: 11), ), ), ], From 972f424baa72a4fbc46831be12f639895ae5dea2 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:50:52 +0200 Subject: [PATCH 3/9] Add dynamic device management on Dashboard and Profile --- lib/profile/profile_screen.dart | 459 ++++++++++++++++++------ lib/screens/home/add_device_screen.dart | 151 ++++++++ lib/screens/home/dashboard_screen.dart | 291 +++++++++------ lib/widgets/glass_input.dart | 3 + lib/widgets/profile_item.dart | 20 +- lib/widgets/workspace_card.dart | 31 +- 6 files changed, 736 insertions(+), 219 deletions(-) create mode 100644 lib/screens/home/add_device_screen.dart diff --git a/lib/profile/profile_screen.dart b/lib/profile/profile_screen.dart index cb8a890..75b48da 100644 --- a/lib/profile/profile_screen.dart +++ b/lib/profile/profile_screen.dart @@ -1,4 +1,6 @@ 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/widgets/blur_blob.dart'; import 'package:mobile_flutter_iot/widgets/glass_card.dart'; import 'package:mobile_flutter_iot/widgets/profile_item.dart'; @@ -16,19 +18,127 @@ class _ProfileScreenState extends State { bool _darkMode = true; bool _isAutoLoginEnabled = false; + UserModel? _currentUser; + final _userRepository = LocalUserRepository(); + @override void initState() { super.initState(); - _loadSessionStatus(); + _loadUserData(); } - Future _loadSessionStatus() async { + Future _loadUserData() async { + final user = await _userRepository.getUser(); final prefs = await SharedPreferences.getInstance(); setState(() { + _currentUser = user; _isAutoLoginEnabled = prefs.getBool('isLoggedIn') ?? false; }); } + Future _deleteAccount() async { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: const Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Color(0xFFF87171)), + SizedBox(width: 10), + Text('System Purge', style: TextStyle(color: Colors.white)), + ], + ), + content: const Text( + 'This will permanently erase your encryption keys, saved devices, 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), + ), + ), + 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'), + ), + ), + ], + ), + ); + + if (confirm == true) { + await _userRepository.deleteUser(); + if (mounted) { + Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false); + } + } + } + + Future _editProfileField( + String title, + String currentValue, + void Function(String) onSave, + ) async { + final controller = TextEditingController(text: currentValue); + 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), + ), + 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: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + if (controller.text.isNotEmpty) { + onSave(controller.text); + Navigator.pop(context); + _loadUserData(); + } + }, + child: const Text( + 'Save Changes', + style: TextStyle(color: Color(0xFF38BDF8)), + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; @@ -52,6 +162,7 @@ class _ProfileScreenState extends State { ), CustomScrollView( + physics: const BouncingScrollPhysics(), slivers: [ _buildAppBar(isWide), SliverPadding( @@ -63,13 +174,29 @@ class _ProfileScreenState extends State { delegate: SliverChildListDelegate([ _buildHeader(isWide), const SizedBox(height: 32), - _buildSecurityStatus(), - const SizedBox(height: 24), + const Text( + 'PREFERENCES', + style: TextStyle( + color: Colors.white24, + fontSize: 11, + letterSpacing: 1.5, + ), + ), + const SizedBox(height: 12), _buildSettingsGroup(), - const SizedBox(height: 24), - _buildLogoutButton(context), + const SizedBox(height: 32), + const Text( + 'ACCOUNT ACTIONS', + style: TextStyle( + color: Colors.white24, + fontSize: 11, + letterSpacing: 1.5, + ), + ), + const SizedBox(height: 12), + _buildActionButtons(context), const SizedBox(height: 40), ]), ), @@ -81,28 +208,159 @@ 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) { + final name = _currentUser?.fullName ?? 'Loading...'; + final dept = _currentUser?.department ?? 'Unknown Department'; + final email = _currentUser?.email ?? 'no-email@system.io'; + + return Column( + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + 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: CircleAvatar( + radius: isWide ? 66 : 51, + backgroundColor: const Color(0xFF1E293B), + child: Icon( + Icons.person_rounded, + size: isWide ? 70 : 50, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(height: 20), + GestureDetector( + onTap: () => _editProfileField('Full Name', name, (val) async { + if (_currentUser != null) { + final updated = UserModel( + fullName: val, + email: _currentUser!.email, + password: _currentUser!.password, + department: _currentUser!.department, + ); + await _userRepository.saveUser(updated); + } + }), + child: Text( + name, + style: const TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ), + const SizedBox(height: 4), + Text( + dept.toUpperCase(), + style: const TextStyle( + color: Color(0xFF4ADE80), + fontSize: 12, + fontWeight: FontWeight.w600, + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () => _editProfileField('Email', email, (val) async { + if (val.contains('@') && _currentUser != null) { + final updated = UserModel( + fullName: _currentUser!.fullName, + email: val, + password: _currentUser!.password, + department: _currentUser!.department, + ); + await _userRepository.saveUser(updated); + } + }), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFF38BDF8).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: const Color(0xFF38BDF8).withValues(alpha: 0.2), + ), + ), + child: Text( + email, + style: const TextStyle( + color: Color(0xFF38BDF8), + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ); + } + Widget _buildSecurityStatus() => GlassCard( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - Icon( - _isAutoLoginEnabled ? Icons.verified_user : Icons.gpp_maybe, - color: _isAutoLoginEnabled - ? const Color(0xFF4ADE80) - : Colors.orangeAccent, + 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: 12), + const SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'Session Security', + 'Security Protocol', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), Text( _isAutoLoginEnabled - ? 'Auto-login active (Web LocalStorage)' - : 'Session not saved', + ? 'Persistent Session: Active' + : 'Session Encryption: Local Only', style: const TextStyle(fontSize: 11, color: Colors.white38), ), ], @@ -111,109 +369,110 @@ class _ProfileScreenState extends State { ), ); - Widget _buildAppBar(bool isWide) => SliverAppBar( - expandedHeight: isWide ? 150 : 100, - pinned: true, - backgroundColor: Colors.transparent, - flexibleSpace: const FlexibleSpaceBar( - centerTitle: true, - title: Text( - 'USER PROFILE', - style: TextStyle(letterSpacing: 2, fontSize: 16), - ), - ), - ); - - Widget _buildHeader(bool isWide) => Column( - children: [ - Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [Color(0xFF38BDF8), Color(0xFF4ADE80)], - ), - ), - child: CircleAvatar( - radius: isWide ? 70 : 50, - backgroundColor: const Color(0xFF1E293B), - child: Icon( - Icons.person, - size: isWide ? 70 : 50, - color: Colors.white, - ), - ), - ), - const SizedBox(height: 16), - const Text( - 'Artem Dev', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - const Text( - 'System Administrator', - style: TextStyle(color: Colors.white38, fontSize: 13), - ), - ], - ); - Widget _buildSettingsGroup() => GlassCard( child: Column( children: [ ProfileMenuItem( - icon: Icons.notifications_none, - title: 'Push Notifications', + icon: Icons.notifications_active_outlined, + title: 'System Alerts', isSwitch: true, value: _notifications, onChanged: (v) => setState(() => _notifications = v), ), - const Divider(color: Colors.white10), + const Divider(color: Colors.white10, height: 1), ProfileMenuItem( - icon: Icons.dark_mode_outlined, - title: 'Dark Mode', + icon: Icons.palette_outlined, + title: 'OLED Dark Mode', isSwitch: true, value: _darkMode, onChanged: (v) => setState(() => _darkMode = v), ), - const Divider(color: Colors.white10), - const ProfileMenuItem( - icon: Icons.devices_other, - title: 'WorkSpace', - trailingText: 'Lab-605a', - ), - const Divider(color: Colors.white10), - const ProfileMenuItem( - icon: Icons.language, - title: 'Language', - trailingText: 'EN', + 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 _buildLogoutButton(BuildContext context) => 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, - ); - } - }, - leading: const Icon(Icons.logout, color: Color(0xFFF87171)), - title: const Text( - 'Logout', - style: TextStyle(color: Color(0xFFF87171), fontWeight: FontWeight.bold), + 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, + ), + ), + trailing: const Icon( + Icons.arrow_forward_ios_rounded, + size: 14, + color: Colors.white24, + ), + ), ), - subtitle: const Text( - 'Clears local session data', - style: TextStyle(fontSize: 10, color: Colors.white24), + 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, + ), + ), + ), + ), ), - ), + ], ); } diff --git a/lib/screens/home/add_device_screen.dart b/lib/screens/home/add_device_screen.dart new file mode 100644 index 0000000..3d869dc --- /dev/null +++ b/lib/screens/home/add_device_screen.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/models/device_model.dart'; +import 'package:mobile_flutter_iot/widgets/glass_input.dart'; +import 'package:mobile_flutter_iot/widgets/primary_button.dart'; + +class AddDeviceScreen extends StatefulWidget { + final DeviceModel? device; + + const AddDeviceScreen({super.key, this.device}); + + @override + State createState() => _AddDeviceScreenState(); +} + +class _AddDeviceScreenState extends State { + late TextEditingController _titleController; + late Color _selectedColor; + late IconData _selectedIcon; + + final List _colors = [ + const Color(0xFF4ADE80), + const Color(0xFF38BDF8), + const Color(0xFFFACC15), + const Color(0xFFF87171), + const Color(0xFFC084FC), + ]; + + final List _icons = [ + Icons.air, + Icons.ac_unit, + Icons.sensors, + Icons.lightbulb_outline, + Icons.water_drop_outlined, + Icons.thermostat, + ]; + + @override + void initState() { + super.initState(); + _titleController = TextEditingController(text: widget.device?.title ?? ''); + _selectedColor = widget.device?.color ?? _colors[0]; + _selectedIcon = widget.device?.icon ?? _icons[0]; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.device == null ? 'ADD SENSOR' : 'EDIT SENSOR'), + backgroundColor: Colors.transparent, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GlassInput( + hintText: 'Sensor Name', + icon: Icons.edit, + controller: _titleController, + ), + const SizedBox(height: 32), + const Text('SELECT COLOR', style: TextStyle(color: Colors.white70)), + const SizedBox(height: 12), + _buildColorPicker(), + const SizedBox(height: 32), + const Text('SELECT ICON', style: TextStyle(color: Colors.white70)), + const SizedBox(height: 12), + _buildIconPicker(), + const SizedBox(height: 48), + PrimaryButton( + text: widget.device == null ? 'CREATE DEVICE' : 'SAVE CHANGES', + onPressed: () { + if (_titleController.text.isEmpty) return; + + final result = DeviceModel( + id: widget.device?.id ?? DateTime.now().toString(), + title: _titleController.text, + value: widget.device?.value ?? '0 units', + status: widget.device?.status ?? 'INITIALIZING', + icon: _selectedIcon, + color: _selectedColor, + ); + Navigator.pop(context, result); + }, + ), + ], + ), + ), + ); + } + + Widget _buildColorPicker() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _colors.map((color) { + return GestureDetector( + onTap: () => setState(() => _selectedColor = color), + child: Container( + width: 45, + height: 45, + decoration: BoxDecoration( + color: color.withOpacity(0.2), + shape: BoxShape.circle, + border: Border.all( + color: _selectedColor == color ? color : Colors.transparent, + width: 2, + ), + ), + child: Center( + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + ), + ), + ); + }).toList(), + ); + } + + Widget _buildIconPicker() { + return Wrap( + spacing: 16, + children: _icons.map((icon) { + return GestureDetector( + onTap: () => setState(() => _selectedIcon = icon), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _selectedIcon == icon + ? _selectedColor.withValues(alpha: 0.2) + : Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _selectedIcon == icon + ? _selectedColor + : Colors.transparent, + ), + ), + child: Icon( + icon, + color: _selectedIcon == icon ? _selectedColor : Colors.white38, + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/screens/home/dashboard_screen.dart b/lib/screens/home/dashboard_screen.dart index eb79eae..4d030b6 100644 --- a/lib/screens/home/dashboard_screen.dart +++ b/lib/screens/home/dashboard_screen.dart @@ -1,124 +1,207 @@ -import 'dart:developer'; import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/models/device_model.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'; -class DashboardScreen extends StatelessWidget { +class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @override - Widget build(BuildContext context) { - final sensorData = [ - { - 'title': 'Air Quality (MQ135)', - 'value': '420 ppm', - 'status': 'EXCELLENT', - 'icon': Icons.air, - 'color': const Color(0xFF4ADE80), - 'subtitle': 'Sensor R0: 76.63', - }, - { - 'title': 'Climate Control', - 'value': '22.4°C', - 'status': 'AUTO MODE', - 'icon': Icons.ac_unit, - 'color': const Color(0xFF38BDF8), - 'subtitle': 'Fan: Idle', - }, - { - 'title': 'Motion Security', - 'value': 'No Motion', - 'status': 'SECURE', - 'icon': Icons.sensors, - 'color': Colors.orangeAccent, - 'subtitle': 'Light: Auto-off', - }, - { - 'title': 'System Stats', - 'value': '78% Health', - 'status': 'All Nominal', - 'icon': Icons.analytics_outlined, - 'color': Colors.purpleAccent, - 'subtitle': 'Uptime: 12h 45m', - }, - ]; + State createState() => _DashboardScreenState(); +} - return Scaffold( - backgroundColor: Colors.transparent, - body: Stack( - children: [ - SafeArea( - child: Column( - children: [ - AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - centerTitle: true, - title: const Text( - 'DASHBOARD', - style: TextStyle( - letterSpacing: 2, - fontWeight: FontWeight.bold, - ), - ), - ), - const Padding( - padding: EdgeInsets.all(16), - child: GlassCard( - padding: EdgeInsets.symmetric(vertical: 12, horizontal: 20), - child: Row( - children: [ - SystemPulseIndicator(), - SizedBox(width: 12), - Text( - 'Monitor SYSTEM: ONLINE', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - Spacer(), - Text( - 'MQTT ACTIVE', - style: TextStyle( - color: Color(0xFF4ADE80), - fontSize: 10, - ), - ), - ], - ), - ), - ), - Expanded( - child: ListView.separated( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - itemCount: sensorData.length, - separatorBuilder: (context, index) => - const SizedBox(height: 16), - itemBuilder: (context, index) { - final item = sensorData[index]; - return WorkspaceCard( - title: item['title'] as String, - value: item['value'] as String, - status: item['status'] as String, - subtitle: item['subtitle'] as String, - icon: item['icon'] as IconData, - accentColor: item['color'] as Color, - ); - }, - ), - ), - ], +class _DashboardScreenState extends State { + final LocalUserRepository _repository = LocalUserRepository(); + List _devices = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadDevices(); + } + + Future _loadDevices() async { + final savedDevices = await _repository.getDevices(); + setState(() { + _devices = savedDevices; + _isLoading = false; + }); + } + + Future _syncData() async { + await _repository.saveDevices(_devices); + } + + void _onAddPressed() async { + final DeviceModel? result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => const AddDeviceScreen()), + ); + + if (result != null) { + setState(() => _devices.add(result)); + await _syncData(); + } + } + + void _onEditDevice(DeviceModel device, int index) async { + final DeviceModel? result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => AddDeviceScreen(device: device)), + ); + + if (result != null) { + setState(() => _devices[index] = result); + await _syncData(); + } + } + + 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 (mounted) Navigator.pop(context); + }, + child: const Text( + 'DELETE', + style: TextStyle(color: Colors.redAccent), ), ), ], ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: SafeArea( + child: Column( + children: [ + _buildAppBar(), + _buildStatusCard(), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _devices.isEmpty + ? _buildEmptyState() + : _buildDeviceList(), + ), + ], + ), + ), floatingActionButton: FloatingActionButton( - onPressed: () => log('Scan for ESP32 devices initiated'), + onPressed: _onAddPressed, backgroundColor: const Color(0xFF38BDF8), child: const Icon(Icons.add, color: Colors.white), ), ); } + + Widget _buildAppBar() { + return AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + centerTitle: true, + title: const Text( + 'DASHBOARD', + style: TextStyle(letterSpacing: 2, fontWeight: FontWeight.bold), + ), + ); + } + + Widget _buildStatusCard() { + return const Padding( + padding: EdgeInsets.all(16), + child: GlassCard( + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 20), + child: Row( + children: [ + SystemPulseIndicator(), + SizedBox(width: 12), + Text( + 'SYSTEM: ONLINE', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + ), + Spacer(), + Text( + 'MQTT ACTIVE', + style: TextStyle(color: Color(0xFF4ADE80), fontSize: 10), + ), + ], + ), + ), + ); + } + + Widget _buildEmptyState() { + return const Center( + child: Text( + 'No devices found.\nTap + to add.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white30), + ), + ); + } + + Widget _buildDeviceList() { + 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]; + return GestureDetector( + onTap: () async { + final result = await Navigator.pushNamed( + context, + '/details', + arguments: SensorArguments( + id: device.id, + title: device.title, + value: device.value, + icon: device.icon, + color: device.color, + ), + ); + + if (result is Map && result.containsKey('deleteId')) { + _onDeleteDevice(index); + } + }, + onLongPress: () => _onEditDevice(device, index), + child: WorkspaceCard( + id: device.id, + title: device.title, + value: device.value, + status: device.status, + subtitle: + 'ID: ${device.id.length > 8 ? device.id.substring(0, 8) : device.id}', + icon: device.icon, + accentColor: device.color, + ), + ); + }, + ); + } } diff --git a/lib/widgets/glass_input.dart b/lib/widgets/glass_input.dart index 96ce8de..a61ecc0 100644 --- a/lib/widgets/glass_input.dart +++ b/lib/widgets/glass_input.dart @@ -4,10 +4,12 @@ class GlassInput extends StatelessWidget { final String hintText; final IconData icon; final bool isPassword; + final TextEditingController? controller; const GlassInput({ required this.hintText, required this.icon, + this.controller, super.key, this.isPassword = false, }); @@ -21,6 +23,7 @@ class GlassInput extends StatelessWidget { border: Border.all(color: Colors.white.withValues(alpha: 0.1)), ), child: TextField( + controller: controller, obscureText: isPassword, style: const TextStyle(color: Colors.white), decoration: InputDecoration( diff --git a/lib/widgets/profile_item.dart b/lib/widgets/profile_item.dart index 6eeb2e8..7e2bfe0 100644 --- a/lib/widgets/profile_item.dart +++ b/lib/widgets/profile_item.dart @@ -7,6 +7,7 @@ class ProfileMenuItem extends StatelessWidget { final String? trailingText; final bool value; final void Function(bool)? onChanged; + final VoidCallback? onTap; const ProfileMenuItem({ required this.icon, @@ -16,11 +17,13 @@ class ProfileMenuItem extends StatelessWidget { this.trailingText, this.value = false, this.onChanged, + this.onTap, }); @override Widget build(BuildContext context) { return ListTile( + onTap: isSwitch ? null : onTap, leading: Icon(icon, color: const Color(0xFF38BDF8)), title: Text(title, style: const TextStyle(fontSize: 16)), trailing: isSwitch @@ -29,9 +32,20 @@ class ProfileMenuItem extends StatelessWidget { onChanged: onChanged, activeThumbColor: const Color(0xFF38BDF8), ) - : Text( - trailingText ?? '', - style: const TextStyle(color: Colors.white38), + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + trailingText ?? '', + style: const TextStyle(color: Colors.white38), + ), + if (onTap != null) + const Icon( + Icons.chevron_right, + color: Colors.white10, + size: 20, + ), + ], ), ); } diff --git a/lib/widgets/workspace_card.dart b/lib/widgets/workspace_card.dart index 8f7d95e..8351531 100644 --- a/lib/widgets/workspace_card.dart +++ b/lib/widgets/workspace_card.dart @@ -3,20 +3,24 @@ import 'package:mobile_flutter_iot/screens/home/details_screen.dart'; import 'package:mobile_flutter_iot/widgets/glass_card.dart'; class WorkspaceCard extends StatelessWidget { + final String id; final String title; final String value; final String status; final String subtitle; final IconData icon; final Color accentColor; + final VoidCallback? onAnalyticsTap; const WorkspaceCard({ + required this.id, required this.title, required this.value, required this.status, required this.subtitle, required this.icon, required this.accentColor, + this.onAnalyticsTap, super.key, }); @@ -74,18 +78,21 @@ class WorkspaceCard extends StatelessWidget { ), const SizedBox(height: 10), GestureDetector( - onTap: () { - Navigator.pushNamed( - context, - '/details', - arguments: SensorArguments( - title: title, - value: value, - icon: icon, - color: accentColor, - ), - ); - }, + onTap: + onAnalyticsTap ?? + () { + Navigator.pushNamed( + context, + '/details', + arguments: SensorArguments( + id: id, + title: title, + value: value, + icon: icon, + color: accentColor, + ), + ); + }, child: Text( 'ANALYTICS', style: TextStyle( From 246b7eae1cf29ca0d328055b1cca0fd6a288db87 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:51:13 +0200 Subject: [PATCH 4/9] Update details_screen.dart --- lib/screens/home/details_screen.dart | 130 +++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 19 deletions(-) diff --git a/lib/screens/home/details_screen.dart b/lib/screens/home/details_screen.dart index 067c3eb..46ca3cf 100644 --- a/lib/screens/home/details_screen.dart +++ b/lib/screens/home/details_screen.dart @@ -1,38 +1,108 @@ import 'package:flutter/material.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'; class SensorArguments { + final String id; final String title; final String value; final IconData icon; final Color color; + final String status; SensorArguments({ + required this.id, required this.title, required this.value, required this.icon, required this.color, + this.status = 'Stable', }); } -class DetailsScreen extends StatelessWidget { +class DetailsScreen extends StatefulWidget { const DetailsScreen({super.key}); + @override + State createState() => _DetailsScreenState(); +} + +class _DetailsScreenState extends State { + bool _isManualControlOn = true; + String? _currentValue; + final _userRepository = LocalUserRepository(); + + 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)', + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white24), + ), + ), + ), + 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; + }); + } + } + } + @override Widget build(BuildContext context) { final Object? rawArgs = ModalRoute.of(context)?.settings.arguments; - if (rawArgs == null || rawArgs is! SensorArguments) { return const _ErrorDetailsView(); } - final args = rawArgs; return Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, title: Text('${args.title.toUpperCase()} ANALYSIS'), + actions: [ + IconButton( + icon: const Icon( + Icons.delete_sweep_outlined, + color: Colors.redAccent, + ), + onPressed: () => Navigator.pop(context, {'deleteId': args.id}), + ), + ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), @@ -55,18 +125,22 @@ class DetailsScreen extends StatelessWidget { Row( children: [ Expanded( - child: _buildMiniStat( - 'Current Value', - args.value, - args.icon, - args.color, + child: GestureDetector( + onTap: () => _editValue(args), + child: _buildMiniStat( + 'Current Value', + _currentValue ?? args.value, + args.icon, + args.color, + isEditable: true, + ), ), ), const SizedBox(width: 16), Expanded( child: _buildMiniStat( 'Status', - 'Stable', + args.status, Icons.check_circle_outline, const Color(0xFF4ADE80), ), @@ -76,12 +150,22 @@ class DetailsScreen extends StatelessWidget { const SizedBox(height: 20), GlassCard( child: ListTile( - leading: Icon(args.icon, color: args.color), + leading: Icon( + _isManualControlOn ? args.icon : Icons.power_off_outlined, + color: _isManualControlOn ? args.color : Colors.white24, + ), title: Text('Manual ${args.title} Control'), + subtitle: Text( + _isManualControlOn ? 'System Active' : 'System Paused', + ), trailing: Switch( - value: true, - onChanged: (v) {}, - activeThumbColor: args.color, + value: _isManualControlOn, + activeThumbColor: args.color.withValues(alpha: 0.3), + onChanged: (v) { + setState(() { + _isManualControlOn = v; + }); + }, ), ), ), @@ -95,13 +179,23 @@ class DetailsScreen extends StatelessWidget { String label, String value, IconData icon, - Color color, - ) { + Color color, { + bool isEditable = false, + }) { return GlassCard( padding: const EdgeInsets.all(12), child: Column( children: [ - Icon(icon, color: color), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: color, size: 16), + if (isEditable) ...[ + const SizedBox(width: 4), + const Icon(Icons.edit, color: Colors.white24, size: 12), + ], + ], + ), const SizedBox(height: 8), Text( label, @@ -119,15 +213,13 @@ class DetailsScreen extends StatelessWidget { class _ErrorDetailsView extends StatelessWidget { const _ErrorDetailsView(); - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('ERROR')), body: const Center( child: Text( - 'Sensor data not found.\nPlease return to Dashboard.', - textAlign: TextAlign.center, + 'Sensor data not found.', style: TextStyle(color: Colors.white54), ), ), From cd4796abf345c4c47dad6dc30520322ea07ef146 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:10:59 +0200 Subject: [PATCH 5/9] Resolve linter warnings --- lib/models/device_model.dart | 2 +- lib/profile/profile_screen.dart | 3 ++- lib/screens/home/add_device_screen.dart | 2 +- lib/screens/home/dashboard_screen.dart | 20 +++++++++++++++----- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/models/device_model.dart b/lib/models/device_model.dart index 698c474..1bf9c5c 100644 --- a/lib/models/device_model.dart +++ b/lib/models/device_model.dart @@ -24,7 +24,7 @@ class DeviceModel { 'value': value, 'status': status, 'icon': icon.codePoint, - 'color': color.value, + 'color': color.toARGB32(), }; } diff --git a/lib/profile/profile_screen.dart b/lib/profile/profile_screen.dart index 75b48da..27cab65 100644 --- a/lib/profile/profile_screen.dart +++ b/lib/profile/profile_screen.dart @@ -50,7 +50,8 @@ 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, ' + 'saved devices, and local profile. Continue?', style: TextStyle(color: Colors.white70), ), actions: [ diff --git a/lib/screens/home/add_device_screen.dart b/lib/screens/home/add_device_screen.dart index 3d869dc..ba0ec4b 100644 --- a/lib/screens/home/add_device_screen.dart +++ b/lib/screens/home/add_device_screen.dart @@ -100,7 +100,7 @@ class _AddDeviceScreenState extends State { width: 45, height: 45, decoration: BoxDecoration( - color: color.withOpacity(0.2), + color: color.withValues(alpha: 0.2), shape: BoxShape.circle, border: Border.all( color: _selectedColor == color ? color : Colors.transparent, diff --git a/lib/screens/home/dashboard_screen.dart b/lib/screens/home/dashboard_screen.dart index 4d030b6..242dae3 100644 --- a/lib/screens/home/dashboard_screen.dart +++ b/lib/screens/home/dashboard_screen.dart @@ -28,6 +28,7 @@ class _DashboardScreenState extends State { Future _loadDevices() async { final savedDevices = await _repository.getDevices(); + if (!mounted) return; setState(() { _devices = savedDevices; _isLoading = false; @@ -44,6 +45,8 @@ class _DashboardScreenState extends State { MaterialPageRoute(builder: (context) => const AddDeviceScreen()), ); + if (!mounted) return; + if (result != null) { setState(() => _devices.add(result)); await _syncData(); @@ -56,6 +59,8 @@ class _DashboardScreenState extends State { MaterialPageRoute(builder: (context) => AddDeviceScreen(device: device)), ); + if (!mounted) return; + if (result != null) { setState(() => _devices[index] = result); await _syncData(); @@ -77,7 +82,7 @@ class _DashboardScreenState extends State { onPressed: () async { setState(() => _devices.removeAt(index)); await _syncData(); - if (mounted) Navigator.pop(context); + if (context.mounted) Navigator.pop(context); }, child: const Text( 'DELETE', @@ -102,8 +107,8 @@ class _DashboardScreenState extends State { child: _isLoading ? const Center(child: CircularProgressIndicator()) : _devices.isEmpty - ? _buildEmptyState() - : _buildDeviceList(), + ? _buildEmptyState() + : _buildDeviceList(), ), ], ), @@ -171,6 +176,10 @@ class _DashboardScreenState extends State { if (index >= _devices.length) return const SizedBox.shrink(); final device = _devices[index]; + final String deviceId = device.id.length > 8 + ? device.id.substring(0, 8) + : device.id; + return GestureDetector( onTap: () async { final result = await Navigator.pushNamed( @@ -185,6 +194,8 @@ class _DashboardScreenState extends State { ), ); + if (!mounted) return; + if (result is Map && result.containsKey('deleteId')) { _onDeleteDevice(index); } @@ -195,8 +206,7 @@ class _DashboardScreenState extends State { title: device.title, value: device.value, status: device.status, - subtitle: - 'ID: ${device.id.length > 8 ? device.id.substring(0, 8) : device.id}', + subtitle: 'ID: $deviceId', icon: device.icon, accentColor: device.color, ), From 06d54e5190120e6e12fba7ad368520d6fb6d17b0 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:14:39 +0200 Subject: [PATCH 6/9] Fix Dashboard screen to dart format --- lib/screens/home/dashboard_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/home/dashboard_screen.dart b/lib/screens/home/dashboard_screen.dart index 242dae3..7a61c56 100644 --- a/lib/screens/home/dashboard_screen.dart +++ b/lib/screens/home/dashboard_screen.dart @@ -107,8 +107,8 @@ class _DashboardScreenState extends State { child: _isLoading ? const Center(child: CircularProgressIndicator()) : _devices.isEmpty - ? _buildEmptyState() - : _buildDeviceList(), + ? _buildEmptyState() + : _buildDeviceList(), ), ], ), From ade5cd5d9dc244ae7ba7865abf3e7b23adf20ed1 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:21:19 +0200 Subject: [PATCH 7/9] Add synchronization between screens --- lib/screens/home/dashboard_screen.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/screens/home/dashboard_screen.dart b/lib/screens/home/dashboard_screen.dart index 7a61c56..4477e00 100644 --- a/lib/screens/home/dashboard_screen.dart +++ b/lib/screens/home/dashboard_screen.dart @@ -198,6 +198,8 @@ class _DashboardScreenState extends State { if (result is Map && result.containsKey('deleteId')) { _onDeleteDevice(index); + } else { + _loadDevices(); } }, onLongPress: () => _onEditDevice(device, index), From 7de12c6f9bad6c2b12d28eba6dbe86b0892f81ac Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:46:23 +0200 Subject: [PATCH 8/9] Update register screen --- lib/screens/auth/register_screen.dart | 94 +++++++++++++++++++-------- lib/widgets/glass_input.dart | 55 +++++++++++----- 2 files changed, 105 insertions(+), 44 deletions(-) diff --git a/lib/screens/auth/register_screen.dart b/lib/screens/auth/register_screen.dart index 2ad923e..5f25ead 100644 --- a/lib/screens/auth/register_screen.dart +++ b/lib/screens/auth/register_screen.dart @@ -21,27 +21,54 @@ class _RegisterScreenState extends State { final _userRepository = LocalUserRepository(); + String? _nameError; + String? _emailError; + String? _deptError; + String? _passwordError; + void _handleRegister() async { + setState(() { + _nameError = null; + _emailError = null; + _deptError = null; + _passwordError = null; + }); + final name = _nameController.text.trim(); final email = _emailController.text.trim(); final dept = _deptController.text.trim(); final password = _passwordController.text.trim(); - if (name.isEmpty || email.isEmpty || dept.isEmpty || password.isEmpty) { - _showError('All fields are required'); - return; + bool hasError = false; + + if (name.isEmpty) { + setState(() => _nameError = 'Please enter your full name'); + hasError = true; + } + + if (email.isEmpty) { + setState(() => _emailError = 'Email address is required'); + hasError = true; + } else if (!email.contains('@') || !email.contains('.')) { + setState(() => _emailError = 'Invalid email format (missing @ or .)'); + hasError = true; } - if (!email.contains('@') || !email.contains('.')) { - _showError('Please enter a valid email address'); - return; + if (dept.isEmpty) { + setState(() => _deptError = 'Specify your university department'); + hasError = true; } - if (password.length < 6) { - _showError('Password must be at least 6 characters'); - return; + if (password.isEmpty) { + setState(() => _passwordError = 'Security password is required'); + hasError = true; + } else if (password.length < 6) { + setState(() => _passwordError = 'Password must be at least 6 characters'); + hasError = true; } + if (hasError) return; + final newUser = UserModel( fullName: name, email: email, @@ -53,18 +80,15 @@ class _RegisterScreenState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Registration Successful! Please login.')), + const SnackBar( + content: Text('Access Key Created! You can now log in.'), + backgroundColor: Color(0xFF4ADE80), + ), ); Navigator.pop(context); } } - void _showError(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.redAccent), - ); - } - @override void dispose() { _nameController.dispose(); @@ -94,6 +118,7 @@ class _RegisterScreenState extends State { SafeArea( child: Center( child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( children: [ @@ -101,14 +126,21 @@ class _RegisterScreenState extends State { _buildTopIcon(), const SizedBox(height: 30), const Text( - 'CREATE ACCOUNT', + 'CREATE ACCESS KEY', style: TextStyle( - fontSize: 24, + fontSize: 22, fontWeight: FontWeight.bold, + letterSpacing: 2, ), ), + const SizedBox(height: 8), + const Text( + 'Register in the IoT System', + style: TextStyle(color: Colors.white38, fontSize: 13), + ), const SizedBox(height: 40), _buildForm(), + const SizedBox(height: 20), ], ), ), @@ -125,13 +157,13 @@ class _RegisterScreenState extends State { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: const Color(0xFF4ADE80).withValues(alpha: 0.03), + color: const Color(0xFF4ADE80).withValues(alpha: 0.1), width: 2, ), ), child: const Icon( Icons.app_registration_rounded, - size: 60, + size: 50, color: Color(0xFF4ADE80), ), ); @@ -145,34 +177,42 @@ class _RegisterScreenState extends State { hintText: 'Full Name', icon: Icons.person_outline, controller: _nameController, + errorText: _nameError, ), const SizedBox(height: 16), GlassInput( hintText: 'Email Address', icon: Icons.alternate_email, controller: _emailController, + errorText: _emailError, ), const SizedBox(height: 16), GlassInput( hintText: 'Department (e.g., KSA, IoT)', - icon: Icons.business_center, + icon: Icons.business_center_outlined, controller: _deptController, + errorText: _deptError, ), const SizedBox(height: 16), GlassInput( hintText: 'Access Password', - icon: Icons.lock_open, + icon: Icons.lock_open_rounded, isPassword: true, controller: _passwordController, + errorText: _passwordError, ), - const SizedBox(height: 24), - PrimaryButton(text: 'CREATE ACCESS KEY', onPressed: _handleRegister), - const SizedBox(height: 12), + const SizedBox(height: 32), + PrimaryButton(text: 'INITIALIZE ACCOUNT', onPressed: _handleRegister), + const SizedBox(height: 16), TextButton( onPressed: () => Navigator.pop(context), child: const Text( - 'RETURN TO ENTRANCE', - style: TextStyle(color: Colors.white38, fontSize: 11), + 'ALREADY HAVE A KEY? RETURN', + style: TextStyle( + color: Colors.white24, + fontSize: 10, + letterSpacing: 1, + ), ), ), ], diff --git a/lib/widgets/glass_input.dart b/lib/widgets/glass_input.dart index a61ecc0..fb606d4 100644 --- a/lib/widgets/glass_input.dart +++ b/lib/widgets/glass_input.dart @@ -5,35 +5,56 @@ class GlassInput extends StatelessWidget { final IconData icon; final bool isPassword; final TextEditingController? controller; + final String? errorText; // Додаємо поле для помилки const GlassInput({ required this.hintText, required this.icon, this.controller, + this.errorText, // Додаємо в конструктор super.key, this.isPassword = false, }); @override Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(15), - border: Border.all(color: Colors.white.withValues(alpha: 0.1)), - ), - child: TextField( - controller: controller, - obscureText: isPassword, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - border: InputBorder.none, - prefixIcon: Icon(icon, color: const Color(0xFF38BDF8), size: 20), - hintText: hintText, - hintStyle: const TextStyle(color: Colors.white38, fontSize: 14), - contentPadding: const EdgeInsets.symmetric(vertical: 15), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DecoratedBox( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(15), + border: Border.all( + // Якщо є помилка, робимо рамку червонуватою + color: errorText != null + ? Colors.redAccent.withValues(alpha: 0.5) + : Colors.white.withValues(alpha: 0.1), + ), + ), + child: TextField( + controller: controller, + obscureText: isPassword, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + border: InputBorder.none, + prefixIcon: Icon(icon, color: const Color(0xFF38BDF8), size: 20), + hintText: hintText, + hintStyle: const TextStyle(color: Colors.white38, fontSize: 14), + contentPadding: const EdgeInsets.symmetric(vertical: 15), + ), + ), ), - ), + // Якщо помилка є, виводимо її маленьким текстом знизу + if (errorText != null) + Padding( + padding: const EdgeInsets.only(left: 12, top: 6), + child: Text( + errorText!, + style: const TextStyle(color: Colors.redAccent, fontSize: 11), + ), + ), + ], ); } } From bcd23b07e2ffff8db4a50707e6715cde4b689a05 Mon Sep 17 00:00:00 2001 From: Art-Invis <151573705+Art-Invis@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:45:19 +0200 Subject: [PATCH 9/9] Implement interactive shake-to-refresh logic --- .../reports/problems/problems-report.html | 663 ++++++++++++++++++ lib/screens/home/dashboard_screen.dart | 65 +- pubspec.lock | 32 + pubspec.yaml | 5 +- 4 files changed, 753 insertions(+), 12 deletions(-) create mode 100644 android/build/reports/problems/problems-report.html diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..e56bba0 --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/lib/screens/home/dashboard_screen.dart b/lib/screens/home/dashboard_screen.dart index 4477e00..7a95267 100644 --- a/lib/screens/home/dashboard_screen.dart +++ b/lib/screens/home/dashboard_screen.dart @@ -1,12 +1,14 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:mobile_flutter_iot/models/device_model.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:shake/shake.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @@ -19,11 +21,60 @@ class _DashboardScreenState extends State { final LocalUserRepository _repository = LocalUserRepository(); List _devices = []; bool _isLoading = true; + late ShakeDetector _shakeDetector; @override void initState() { super.initState(); _loadDevices(); + + _shakeDetector = ShakeDetector.autoStart( + onPhoneShake: (_) { + _handleShake(); + }, + shakeThresholdGravity: 1.5, + ); + } + + @override + void dispose() { + _shakeDetector.stopListening(); + super.dispose(); + } + + void _handleShake() { + () async { + if (!mounted || _devices.isEmpty) 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 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)}%'; + } else { + device.value = '${random.nextInt(100)} units'; + } + } + + await _syncData(); + _loadDevices(); + }(); } Future _loadDevices() async { @@ -44,9 +95,7 @@ class _DashboardScreenState extends State { context, MaterialPageRoute(builder: (context) => const AddDeviceScreen()), ); - if (!mounted) return; - if (result != null) { setState(() => _devices.add(result)); await _syncData(); @@ -58,9 +107,7 @@ class _DashboardScreenState extends State { context, MaterialPageRoute(builder: (context) => AddDeviceScreen(device: device)), ); - if (!mounted) return; - if (result != null) { setState(() => _devices[index] = result); await _syncData(); @@ -174,12 +221,10 @@ class _DashboardScreenState extends State { separatorBuilder: (context, index) => const SizedBox(height: 16), itemBuilder: (context, index) { if (index >= _devices.length) return const SizedBox.shrink(); - final device = _devices[index]; - final String deviceId = device.id.length > 8 + final String shortId = device.id.length > 8 ? device.id.substring(0, 8) : device.id; - return GestureDetector( onTap: () async { final result = await Navigator.pushNamed( @@ -193,9 +238,7 @@ class _DashboardScreenState extends State { color: device.color, ), ); - if (!mounted) return; - if (result is Map && result.containsKey('deleteId')) { _onDeleteDevice(index); } else { @@ -208,7 +251,7 @@ class _DashboardScreenState extends State { title: device.title, value: device.value, status: device.status, - subtitle: 'ID: $deviceId', + subtitle: 'ID: $shortId', icon: device.icon, accentColor: device.color, ), diff --git a/pubspec.lock b/pubspec.lock index 49f051d..3939540 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -128,6 +128,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -200,6 +208,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + sensors_plus: + dependency: "direct main" + description: + name: sensors_plus + sha256: "89e2bfc3d883743539ce5774a2b93df61effde40ff958ecad78cd66b1a8b8d52" + url: "https://pub.dev" + source: hosted + version: "6.1.2" + sensors_plus_platform_interface: + dependency: transitive + description: + name: sensors_plus_platform_interface + sha256: "58815d2f5e46c0c41c40fb39375d3f127306f7742efe3b891c0b1c87e2b5cd5d" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + shake: + dependency: "direct main" + description: + name: shake + sha256: "7bb2bd14e9cd23a0d569f8a286b2b63ba1552ac348914d2d41ec757117ddda4e" + url: "https://pub.dev" + source: hosted + version: "3.0.0" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 19ddaae..2487a60 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,10 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 - shared_preferences: ^2.2.0 + + shake: ^3.0.0 # Оновлена версія + sensors_plus: ^6.1.1 # Сумісна версія + shared_preferences: ^2.2.2 dev_dependencies: flutter_test: