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/models/device_model.dart b/lib/models/device_model.dart new file mode 100644 index 0000000..1bf9c5c --- /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.toARGB32(), + }; + } + + 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/profile/profile_screen.dart b/lib/profile/profile_screen.dart index cb8a890..27cab65 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,128 @@ 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 +163,7 @@ class _ProfileScreenState extends State { ), CustomScrollView( + physics: const BouncingScrollPhysics(), slivers: [ _buildAppBar(isWide), SliverPadding( @@ -63,13 +175,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 +209,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 +370,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/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); +} 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..5f25ead 100644 --- a/lib/screens/auth/register_screen.dart +++ b/lib/screens/auth/register_screen.dart @@ -1,56 +1,146 @@ 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(); + + 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(); + + 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 (dept.isEmpty) { + setState(() => _deptError = 'Specify your university department'); + hasError = true; + } + + 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, + password: password, + department: dept, + ); + + 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), + ), + ); + Navigator.pop(context); + } + } + + @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( + physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 20), _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 your device in the network', - style: TextStyle(color: Colors.white30, fontSize: 12), + 'Register in the IoT System', + style: TextStyle(color: Colors.white38, fontSize: 13), ), const SizedBox(height: 40), - - _buildRegisterForm(context), + _buildForm(), + const SizedBox(height: 20), ], ), ), @@ -67,56 +157,60 @@ 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.1), width: 2, ), ), child: const Icon( Icons.app_registration_rounded, - size: 60, + size: 50, color: Color(0xFF4ADE80), ), ); } - 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, + errorText: _nameError, ), const SizedBox(height: 16), - const GlassInput( + GlassInput( hintText: 'Email Address', - icon: Icons.alternate_email_rounded, + icon: Icons.alternate_email, + controller: _emailController, + errorText: _emailError, ), const SizedBox(height: 16), - const GlassInput( + GlassInput( hintText: 'Department (e.g., KSA, IoT)', icon: Icons.business_center_outlined, + controller: _deptController, + errorText: _deptError, ), const SizedBox(height: 16), - const GlassInput( + GlassInput( hintText: 'Access Password', icon: Icons.lock_open_rounded, isPassword: true, + controller: _passwordController, + errorText: _passwordError, ), - const SizedBox(height: 24), - PrimaryButton( - text: 'CREATE ACCESS KEY', - onPressed: () => Navigator.pop(context), - ), - 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', + 'ALREADY HAVE A KEY? RETURN', style: TextStyle( - color: Colors.white38, - fontSize: 11, + color: Colors.white24, + fontSize: 10, letterSpacing: 1, ), ), diff --git a/lib/screens/home/add_device_screen.dart b/lib/screens/home/add_device_screen.dart new file mode 100644 index 0000000..ba0ec4b --- /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.withValues(alpha: 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..7a95267 100644 --- a/lib/screens/home/dashboard_screen.dart +++ b/lib/screens/home/dashboard_screen.dart @@ -1,124 +1,262 @@ -import 'dart:developer'; +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 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(); +} + +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, + ); + } - 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, - ); - }, - ), - ), - ], + @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 { + final savedDevices = await _repository.getDevices(); + if (!mounted) return; + 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 (!mounted) return; + 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 (!mounted) return; + 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 (context.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]; + final String shortId = device.id.length > 8 + ? device.id.substring(0, 8) + : device.id; + 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 (!mounted) return; + if (result is Map && result.containsKey('deleteId')) { + _onDeleteDevice(index); + } else { + _loadDevices(); + } + }, + onLongPress: () => _onEditDevice(device, index), + child: WorkspaceCard( + id: device.id, + title: device.title, + value: device.value, + status: device.status, + subtitle: 'ID: $shortId', + icon: device.icon, + accentColor: device.color, + ), + ); + }, + ); + } } 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), ), ), diff --git a/lib/widgets/glass_input.dart b/lib/widgets/glass_input.dart index 96ce8de..fb606d4 100644 --- a/lib/widgets/glass_input.dart +++ b/lib/widgets/glass_input.dart @@ -4,33 +4,57 @@ class GlassInput extends StatelessWidget { final String hintText; 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( - 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), + ), + ), + ], ); } } 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( 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: