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: