diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7e5f039 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +web/** linguist-generated=true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3820a95..f071790 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/instance diff --git a/AzuriteConfig b/AzuriteConfig new file mode 100644 index 0000000..3541169 --- /dev/null +++ b/AzuriteConfig @@ -0,0 +1 @@ +{"instaceID":"3263f8fd-b41b-4b62-b68c-71c637aec757"} \ No newline at end of file diff --git a/instance/smart_workspace.db b/instance/smart_workspace.db index abd146d..ce51547 100644 Binary files a/instance/smart_workspace.db and b/instance/smart_workspace.db differ diff --git a/lib/blocs/add_device/add_device_bloc.dart b/lib/blocs/add_device/add_device_bloc.dart new file mode 100644 index 0000000..47480b0 --- /dev/null +++ b/lib/blocs/add_device/add_device_bloc.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/blocs/add_device/add_device_event.dart'; +import 'package:mobile_flutter_iot/blocs/add_device/add_device_state.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; + +class AddDeviceBloc extends Bloc { + final ApiService apiService; + + AddDeviceBloc({ + required this.apiService, + required Color initialColor, + required IconData initialIcon, + }) : super( + AddDeviceState( + selectedColor: initialColor, + selectedIcon: initialIcon, + ), + ) { + on( + (event, emit) => emit(state.copyWith(selectedColor: event.color)), + ); + on( + (event, emit) => emit(state.copyWith(selectedIcon: event.icon)), + ); + on(_onSaveRequested); + } + + Future _onSaveRequested( + AddDeviceSaveRequested event, + Emitter emit, + ) async { + emit(state.copyWith(isLoading: true)); + try { + final success = event.isNew + ? await apiService.addDevice(event.device) + : await apiService.updateDevice(event.device); + + if (success) { + emit(state.copyWith(isLoading: false, isSuccess: true)); + } else { + emit( + state.copyWith( + isLoading: false, + isSuccess: false, + errorMessage: 'API Error: Saved to local cache only.', + ), + ); + } + } catch (e) { + emit( + state.copyWith( + isLoading: false, + isSuccess: false, + errorMessage: 'Connection failed.', + ), + ); + } + } +} diff --git a/lib/blocs/add_device/add_device_event.dart b/lib/blocs/add_device/add_device_event.dart new file mode 100644 index 0000000..aade28c --- /dev/null +++ b/lib/blocs/add_device/add_device_event.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/models/device_model.dart'; + +abstract class AddDeviceEvent extends Equatable { + const AddDeviceEvent(); + + @override + List get props => []; +} + +class AddDeviceColorChanged extends AddDeviceEvent { + final Color color; + const AddDeviceColorChanged(this.color); + @override + List get props => [color]; +} + +class AddDeviceIconChanged extends AddDeviceEvent { + final IconData icon; + const AddDeviceIconChanged(this.icon); + @override + List get props => [icon]; +} + +class AddDeviceSaveRequested extends AddDeviceEvent { + final DeviceModel device; + final bool isNew; + const AddDeviceSaveRequested({required this.device, required this.isNew}); + @override + List get props => [device, isNew]; +} diff --git a/lib/blocs/add_device/add_device_state.dart b/lib/blocs/add_device/add_device_state.dart new file mode 100644 index 0000000..55f9f52 --- /dev/null +++ b/lib/blocs/add_device/add_device_state.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +class AddDeviceState extends Equatable { + final Color selectedColor; + final IconData selectedIcon; + final bool isLoading; + final bool? isSuccess; + final String? errorMessage; + + const AddDeviceState({ + required this.selectedColor, + required this.selectedIcon, + this.isLoading = false, + this.isSuccess, + this.errorMessage, + }); + + AddDeviceState copyWith({ + Color? selectedColor, + IconData? selectedIcon, + bool? isLoading, + bool? isSuccess, + String? errorMessage, + }) { + return AddDeviceState( + selectedColor: selectedColor ?? this.selectedColor, + selectedIcon: selectedIcon ?? this.selectedIcon, + isLoading: isLoading ?? this.isLoading, + isSuccess: isSuccess, + errorMessage: errorMessage, + ); + } + + @override + List get props => + [selectedColor, selectedIcon, isLoading, isSuccess, errorMessage]; +} diff --git a/lib/blocs/auth/auth_bloc.dart b/lib/blocs/auth/auth_bloc.dart new file mode 100644 index 0000000..adebbe8 --- /dev/null +++ b/lib/blocs/auth/auth_bloc.dart @@ -0,0 +1,128 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mobile_flutter_iot/blocs/auth/auth_event.dart'; +import 'package:mobile_flutter_iot/blocs/auth/auth_state.dart'; +import 'package:mobile_flutter_iot/models/user_model.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; + +class AuthBloc extends Bloc { + final ApiService apiService; + final LocalUserRepository userRepository; + final _storage = const FlutterSecureStorage(); + + AuthBloc({ + required this.apiService, + required this.userRepository, + }) : super(AuthInitial()) { + on(_onCheckRequested); + on(_onLoginRequested); + on(_onRegisterRequested); + on(_onLogoutRequested); + } + + Future _onCheckRequested( + AuthCheckRequested event, + Emitter emit, + ) async { + emit(AuthLoading()); + try { + final token = await _storage.read(key: 'access_token'); + if (token != null) { + emit(AuthAuthenticated()); + } else { + emit(AuthUnauthenticated()); + } + } catch (_) { + emit(AuthUnauthenticated()); + } + } + + Future _onLoginRequested( + AuthLoginRequested event, + Emitter emit, + ) async { + emit(AuthLoading()); + try { + final result = await apiService.login(event.email, event.password); + + if (result != null && result['token'] != null) { + final token = result['token'].toString(); + await _storage.write(key: 'access_token', value: token); + + if (result['user'] != null) { + final userData = result['user'] as Map; + + final user = UserModel( + fullName: userData['fullName']?.toString() ?? 'Unknown', + email: userData['email']?.toString() ?? event.email, + password: event.password, // Беремо з івенту + department: userData['department']?.toString() ?? 'IoT', + ); + await userRepository.saveUser(user); + } + + emit(AuthAuthenticated()); + return; + } + } catch (e) { + // + } + + final localUser = await userRepository.getUser(); + + if (localUser != null && + localUser.email == event.email && + localUser.password == event.password) { + await _storage.write(key: 'access_token', value: 'offline_mode_token'); + emit(AuthAuthenticated()); + } else { + emit( + const AuthError( + 'Login Failed: Invalid credentials or Server Offline', + ), + ); + emit(AuthUnauthenticated()); + } + } + + Future _onRegisterRequested( + AuthRegisterRequested event, + Emitter emit, + ) async { + emit(AuthLoading()); + + final userToSave = UserModel( + fullName: event.name, + email: event.email, + password: event.password, + department: 'IoT', + ); + + try { + final success = await apiService.register(userToSave); + + if (success) { + add(AuthLoginRequested(email: event.email, password: event.password)); + } else { + emit(const AuthError('Помилка реєстрації. Можливо, email вже існує.')); + } + } catch (e) { + await userRepository.saveUser(userToSave); + await _storage.write(key: 'access_token', value: 'offline_mode_token'); + emit(AuthAuthenticated()); + } + } + + Future _onLogoutRequested( + AuthLogoutRequested event, + Emitter emit, + ) async { + emit(AuthLoading()); + + await _storage.delete(key: 'access_token'); + await userRepository.deleteUser(); + + emit(AuthUnauthenticated()); + } +} diff --git a/lib/blocs/auth/auth_event.dart b/lib/blocs/auth/auth_event.dart new file mode 100644 index 0000000..253bda9 --- /dev/null +++ b/lib/blocs/auth/auth_event.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; + +abstract class AuthEvent extends Equatable { + const AuthEvent(); + + @override + List get props => []; +} + +class AuthCheckRequested extends AuthEvent {} + +class AuthLoginRequested extends AuthEvent { + final String email; + final String password; + + const AuthLoginRequested({required this.email, required this.password}); + + @override + List get props => [email, password]; +} + +class AuthRegisterRequested extends AuthEvent { + final String email; + final String password; + final String name; + + const AuthRegisterRequested({ + required this.email, + required this.password, + required this.name, + }); + + @override + List get props => [email, password, name]; +} + +class AuthLogoutRequested extends AuthEvent {} diff --git a/lib/blocs/auth/auth_state.dart b/lib/blocs/auth/auth_state.dart new file mode 100644 index 0000000..5f506c0 --- /dev/null +++ b/lib/blocs/auth/auth_state.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; + +abstract class AuthState extends Equatable { + const AuthState(); + + @override + List get props => []; +} + +class AuthInitial extends AuthState {} + +class AuthLoading extends AuthState {} + +class AuthAuthenticated extends AuthState {} + +class AuthUnauthenticated extends AuthState {} + +class AuthError extends AuthState { + final String message; + + const AuthError(this.message); + + @override + List get props => [message]; +} diff --git a/lib/blocs/details/details_bloc.dart b/lib/blocs/details/details_bloc.dart new file mode 100644 index 0000000..032b6fd --- /dev/null +++ b/lib/blocs/details/details_bloc.dart @@ -0,0 +1,102 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/blocs/details/details_event.dart'; +import 'package:mobile_flutter_iot/blocs/details/details_state.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; + +class DetailsBloc extends Bloc { + final ApiService apiService; + final LocalUserRepository userRepository; + + DetailsBloc({ + required this.apiService, + required this.userRepository, + }) : super(const DetailsState(isManualControlOn: true)) { + on((event, emit) { + emit(state.copyWith(isManualControlOn: event.isOn)); + }); + + on((event, emit) { + emit( + state.copyWith( + customIp: event.newIp, + alertMessage: 'Reconnecting to ${event.newIp}...', + ), + ); + }); + + on(_onUpdateValueRequested); + on(_onSaveSnapshotRequested); + on(_onDeleteDeviceRequested); + } + + Future _onUpdateValueRequested( + DetailsUpdateValueRequested event, + Emitter emit, + ) async { + final devices = await userRepository.getDevices(); + emit( + state.copyWith( + currentValue: event.newValue, + alertMessage: 'Value updated!', + ), + ); + + final index = devices.indexWhere((d) => d.value == state.currentValue); + if (index != -1) { + await apiService.updateDevice(devices[index]); + await userRepository.saveDevices(devices); + } + } + + Future _onDeleteDeviceRequested( + DetailsDeleteDeviceRequested event, + Emitter emit, + ) async { + final success = await apiService.deleteDevice(event.deviceId); + if (success) { + final devices = await userRepository.getDevices(); + devices.removeWhere((d) => d.id == event.deviceId); + await userRepository.saveDevices(devices); + + emit( + state.copyWith( + isDeleted: true, + alertMessage: 'Device deleted from cloud!', + ), + ); + } else { + emit( + state.copyWith( + alertMessage: 'Failed to delete (Check connection)', + isError: true, + ), + ); + } + } + + Future _onSaveSnapshotRequested( + DetailsSaveSnapshotRequested event, + Emitter emit, + ) async { + emit(state.copyWith(isSavingSnapshot: true)); + final success = await apiService.saveLog(event.sensorId, event.value); + if (success) { + emit( + state.copyWith( + isSavingSnapshot: false, + alertMessage: 'Snapshot saved to cloud! 📸', + isError: false, + ), + ); + } else { + emit( + state.copyWith( + isSavingSnapshot: false, + alertMessage: 'Failed to save snapshot. Check connection.', + isError: true, + ), + ); + } + } +} diff --git a/lib/blocs/details/details_event.dart b/lib/blocs/details/details_event.dart new file mode 100644 index 0000000..657ece4 --- /dev/null +++ b/lib/blocs/details/details_event.dart @@ -0,0 +1,44 @@ +import 'package:equatable/equatable.dart'; + +abstract class DetailsEvent extends Equatable { + const DetailsEvent(); + + @override + List get props => []; +} + +class DetailsToggleManualControlRequested extends DetailsEvent { + final bool isOn; + const DetailsToggleManualControlRequested(this.isOn); + @override + List get props => [isOn]; +} + +class DetailsSaveSnapshotRequested extends DetailsEvent { + final String sensorId; + final String value; + const DetailsSaveSnapshotRequested(this.sensorId, this.value); + @override + List get props => [sensorId, value]; +} + +class DetailsUpdateIpRequested extends DetailsEvent { + final String newIp; + const DetailsUpdateIpRequested(this.newIp); + @override + List get props => [newIp]; +} + +class DetailsUpdateValueRequested extends DetailsEvent { + final String newValue; + const DetailsUpdateValueRequested(this.newValue); + @override + List get props => [newValue]; +} + +class DetailsDeleteDeviceRequested extends DetailsEvent { + final String deviceId; + const DetailsDeleteDeviceRequested(this.deviceId); + @override + List get props => [deviceId]; +} diff --git a/lib/blocs/details/details_state.dart b/lib/blocs/details/details_state.dart new file mode 100644 index 0000000..28eb905 --- /dev/null +++ b/lib/blocs/details/details_state.dart @@ -0,0 +1,53 @@ +import 'package:equatable/equatable.dart'; + +class DetailsState extends Equatable { + final String? currentValue; + final String? customIp; + final bool isManualControlOn; + final bool isSavingSnapshot; + final String? alertMessage; + final bool isError; + final bool isDeleted; + + const DetailsState({ + this.currentValue, + this.customIp, + this.isManualControlOn = false, + this.isSavingSnapshot = false, + this.alertMessage, + this.isError = false, + this.isDeleted = false, + }); + + DetailsState copyWith({ + String? currentValue, + String? customIp, + bool? isManualControlOn, + bool? isSavingSnapshot, + String? alertMessage, + bool? isError, + bool? isDeleted, + bool clearAlert = false, + }) { + return DetailsState( + currentValue: currentValue ?? this.currentValue, + customIp: customIp ?? this.customIp, + isManualControlOn: isManualControlOn ?? this.isManualControlOn, + isSavingSnapshot: isSavingSnapshot ?? this.isSavingSnapshot, + alertMessage: clearAlert ? null : (alertMessage ?? this.alertMessage), + isError: !clearAlert && (isError ?? this.isError), + isDeleted: isDeleted ?? this.isDeleted, + ); + } + + @override + List get props => [ + currentValue, + customIp, + isManualControlOn, + isSavingSnapshot, + alertMessage, + isError, + isDeleted, + ]; +} diff --git a/lib/blocs/device/device_bloc.dart b/lib/blocs/device/device_bloc.dart new file mode 100644 index 0000000..d5d93ba --- /dev/null +++ b/lib/blocs/device/device_bloc.dart @@ -0,0 +1,100 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/blocs/device/device_event.dart'; +import 'package:mobile_flutter_iot/blocs/device/device_state.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; + +class DeviceBloc extends Bloc { + final ApiService apiService; + final LocalUserRepository localRepo; + + DeviceBloc({ + required this.apiService, + required this.localRepo, + }) : super(DeviceInitial()) { + on(_onFetchRequested); + on(_onAddRequested); + on(_onUpdateRequested); + on(_onDeleteRequested); + } + + Future _onFetchRequested( + DeviceFetchRequested event, + Emitter emit, + ) async { + emit(DeviceLoading()); + try { + final devices = await apiService.fetchDevices(); + + if (devices != null) { + await localRepo.saveDevices(devices); + emit(DeviceLoaded(devices)); + } else { + _loadFromCacheFallback(emit); + } + } catch (e) { + _loadFromCacheFallback(emit); + } + } + + Future _onAddRequested( + DeviceAddRequested event, + Emitter emit, + ) async { + try { + final success = await apiService.addDevice(event.device); + if (success) { + add(DeviceFetchRequested()); + } else { + emit(const DeviceError('Failed to add device to cloud.')); + } + } catch (e) { + emit(DeviceError('Connection error: $e')); + } + } + + Future _onUpdateRequested( + DeviceUpdateRequested event, + Emitter emit, + ) async { + try { + final success = await apiService.updateDevice(event.device); + if (success) { + add(DeviceFetchRequested()); + } else { + emit(const DeviceError('Failed to update device.')); + } + } catch (e) { + emit(DeviceError('Connection error: $e')); + } + } + + Future _onDeleteRequested( + DeviceDeleteRequested event, + Emitter emit, + ) async { + try { + final success = await apiService.deleteDevice(event.deviceId); + if (success) { + add(DeviceFetchRequested()); + } else { + emit(const DeviceError('Failed to delete device.')); + } + } catch (e) { + emit(DeviceError('Connection error: $e')); + } + } + + Future _loadFromCacheFallback(Emitter emit) async { + try { + final localDevices = await localRepo.getDevices(); + if (localDevices.isNotEmpty) { + emit(DeviceLoaded(localDevices, isOffline: true)); + } else { + emit(const DeviceError('No internet and no cached data available.')); + } + } catch (_) { + emit(const DeviceError('Critical error reading local cache.')); + } + } +} diff --git a/lib/blocs/device/device_event.dart b/lib/blocs/device/device_event.dart new file mode 100644 index 0000000..6c69174 --- /dev/null +++ b/lib/blocs/device/device_event.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; +import 'package:mobile_flutter_iot/models/device_model.dart'; + +abstract class DeviceEvent extends Equatable { + const DeviceEvent(); + + @override + List get props => []; +} + +class DeviceFetchRequested extends DeviceEvent {} + +class DeviceAddRequested extends DeviceEvent { + final DeviceModel device; + const DeviceAddRequested(this.device); + @override + List get props => [device]; +} + +class DeviceUpdateRequested extends DeviceEvent { + final DeviceModel device; + const DeviceUpdateRequested(this.device); + @override + List get props => [device]; +} + +class DeviceDeleteRequested extends DeviceEvent { + final String deviceId; + const DeviceDeleteRequested(this.deviceId); + @override + List get props => [deviceId]; +} diff --git a/lib/blocs/device/device_state.dart b/lib/blocs/device/device_state.dart new file mode 100644 index 0000000..ca41223 --- /dev/null +++ b/lib/blocs/device/device_state.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; +import 'package:mobile_flutter_iot/models/device_model.dart'; + +abstract class DeviceState extends Equatable { + const DeviceState(); + + @override + List get props => []; +} + +class DeviceInitial extends DeviceState {} + +class DeviceLoading extends DeviceState {} + +class DeviceLoaded extends DeviceState { + final List devices; + final bool isOffline; + + const DeviceLoaded(this.devices, {this.isOffline = false}); + + @override + List get props => [devices, isOffline]; +} + +class DeviceError extends DeviceState { + final String message; + + const DeviceError(this.message); + + @override + List get props => [message]; +} diff --git a/lib/blocs/mqtt/mqtt_bloc.dart b/lib/blocs/mqtt/mqtt_bloc.dart new file mode 100644 index 0000000..c8c6c8e --- /dev/null +++ b/lib/blocs/mqtt/mqtt_bloc.dart @@ -0,0 +1,165 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_event.dart'; +import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_state.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; +import 'package:mqtt_client/mqtt_client.dart'; +import 'package:mqtt_client/mqtt_server_client.dart'; + +class MqttBloc extends Bloc { + final ApiService _apiService; + StreamSubscription>>? _updatesSub; + bool _hasLoggedReadViolation = false; + + MqttBloc({required ApiService apiService}) + : _apiService = apiService, + super(const MqttState()) { + on(_onInitialize); + on(_onConnect); + on(_onDisconnect); + on(_onToggleHardware); + on(_onMessageReceived); + on(_onUpdateLockHours); + } + + void _onInitialize(MqttInitializeRequested event, Emitter emit) { + final client = MqttServerClient(event.serverIp, event.clientId) + ..port = 1883 + ..logging(on: false) + ..keepAlivePeriod = 20 + ..autoReconnect = true; + + client.onDisconnected = () => add(MqttDisconnectRequested()); + client.onConnected = () { + _subscribeToTopics(client); + emit(state.copyWith(status: MqttStatus.connected)); + }; + + emit( + state.copyWith( + client: client, + statusMessage: 'Initialized on ${event.serverIp}', + ), + ); + } + + Future _onConnect( + MqttConnectRequested event, + Emitter emit, + ) async { + if (state.client == null || state.status == MqttStatus.connected) return; + + emit( + state.copyWith( + status: MqttStatus.connecting, + statusMessage: 'Connecting...', + ), + ); + + try { + await state.client!.connect(); + } catch (e) { + log('MQTT ERROR: $e'); + state.client?.disconnect(); + emit( + state.copyWith(status: MqttStatus.error, statusMessage: 'Error: $e'), + ); + } + } + + void _subscribeToTopics(MqttServerClient client) { + client.subscribe('sensors/air', MqttQos.atMostOnce); + + _updatesSub?.cancel(); + _updatesSub = client.updates!.listen((messages) { + final recMess = messages[0].payload as MqttPublishMessage; + final payload = + MqttPublishPayload.bytesToStringAsString(recMess.payload.message); + + add(MqttMessageReceived('sensors/air', payload)); + }); + } + + Future _onMessageReceived( + MqttMessageReceived event, + Emitter emit, + ) async { + if (_isTimeRestricted()) { + if (!_hasLoggedReadViolation) { + await _apiService.saveLog( + 'SECURITY_POLICY', + 'Blocked stream at (${DateTime.now().hour}:00)', + ); + _hasLoggedReadViolation = true; + } + emit(state.copyWith(airQuality: '0')); + return; + } + + _hasLoggedReadViolation = false; + emit(state.copyWith(airQuality: event.message)); + } + + Future _onToggleHardware( + MqttToggleHardwareRequested event, + Emitter emit, + ) async { + if (state.client == null || state.status != MqttStatus.connected) return; + + if (_isTimeRestricted()) { + await _apiService.saveLog( + 'SECURITY_POLICY', + 'LED blocked at (${DateTime.now().hour}:00)', + ); + return; + } + + final builder = MqttClientPayloadBuilder() + ..addString(event.isOn ? 'ON' : 'OFF'); + + state.client! + .publishMessage('commands/led', MqttQos.atMostOnce, builder.payload!); + + emit(state.copyWith(isLedOn: event.isOn)); + } + + void _onDisconnect(MqttDisconnectRequested event, Emitter emit) { + state.client?.disconnect(); + _updatesSub?.cancel(); + emit( + state.copyWith( + status: MqttStatus.disconnected, + statusMessage: 'Disconnected', + ), + ); + } + + void _onUpdateLockHours( + MqttUpdateLockHoursRequested event, + Emitter emit, + ) { + emit( + state.copyWith( + startLockHour: event.startHour, + endLockHour: event.endHour, + ), + ); + } + + bool _isTimeRestricted() { + final hour = DateTime.now().hour; + if (state.startLockHour > state.endLockHour) { + return hour >= state.startLockHour || hour < state.endLockHour; + } + return hour >= state.startLockHour && hour < state.endLockHour; + } + + @override + Future close() { + _updatesSub?.cancel(); + state.client?.disconnect(); + return super.close(); + } +} diff --git a/lib/blocs/mqtt/mqtt_event.dart b/lib/blocs/mqtt/mqtt_event.dart new file mode 100644 index 0000000..5c58dd2 --- /dev/null +++ b/lib/blocs/mqtt/mqtt_event.dart @@ -0,0 +1,51 @@ +import 'package:equatable/equatable.dart'; + +abstract class MqttEvent extends Equatable { + const MqttEvent(); + + @override + List get props => []; +} + +class MqttInitializeRequested extends MqttEvent { + final String serverIp; + final String clientId; + + const MqttInitializeRequested(this.serverIp, this.clientId); + + @override + List get props => [serverIp, clientId]; +} + +class MqttConnectRequested extends MqttEvent {} + +class MqttDisconnectRequested extends MqttEvent {} + +class MqttToggleHardwareRequested extends MqttEvent { + final bool isOn; + + const MqttToggleHardwareRequested(this.isOn); + + @override + List get props => [isOn]; +} + +class MqttMessageReceived extends MqttEvent { + final String topic; + final String message; + + const MqttMessageReceived(this.topic, this.message); + + @override + List get props => [topic, message]; +} + +class MqttUpdateLockHoursRequested extends MqttEvent { + final int startHour; + final int endHour; + + const MqttUpdateLockHoursRequested(this.startHour, this.endHour); + + @override + List get props => [startHour, endHour]; +} diff --git a/lib/blocs/mqtt/mqtt_state.dart b/lib/blocs/mqtt/mqtt_state.dart new file mode 100644 index 0000000..11b54e8 --- /dev/null +++ b/lib/blocs/mqtt/mqtt_state.dart @@ -0,0 +1,55 @@ +import 'package:equatable/equatable.dart'; +import 'package:mqtt_client/mqtt_server_client.dart'; + +enum MqttStatus { disconnected, connecting, connected, error } + +class MqttState extends Equatable { + final MqttServerClient? client; + final MqttStatus status; + final bool isLedOn; + final String statusMessage; + final String airQuality; + final int startLockHour; + final int endLockHour; + + const MqttState({ + this.client, + this.status = MqttStatus.disconnected, + this.isLedOn = false, + this.statusMessage = 'Disconnected', + this.airQuality = '0.0', + this.startLockHour = 22, + this.endLockHour = 6, + }); + + MqttState copyWith({ + MqttServerClient? client, + MqttStatus? status, + bool? isLedOn, + String? statusMessage, + String? airQuality, + int? startLockHour, + int? endLockHour, + }) { + return MqttState( + client: client ?? this.client, + status: status ?? this.status, + isLedOn: isLedOn ?? this.isLedOn, + statusMessage: statusMessage ?? this.statusMessage, + airQuality: airQuality ?? this.airQuality, + startLockHour: startLockHour ?? this.startLockHour, + endLockHour: endLockHour ?? this.endLockHour, + ); + } + + @override + List get props => [ + client, + status, + isLedOn, + statusMessage, + airQuality, + startLockHour, + endLockHour, + ]; +} diff --git a/lib/blocs/profile/profile_bloc.dart b/lib/blocs/profile/profile_bloc.dart new file mode 100644 index 0000000..73f3aa5 --- /dev/null +++ b/lib/blocs/profile/profile_bloc.dart @@ -0,0 +1,182 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/blocs/profile/profile_event.dart'; +import 'package:mobile_flutter_iot/blocs/profile/profile_state.dart'; +import 'package:mobile_flutter_iot/models/user_model.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; + +class ProfileBloc extends Bloc { + final ApiService apiService; + final LocalUserRepository userRepository; + + ProfileBloc({ + required this.apiService, + required this.userRepository, + }) : super(const ProfileState()) { + on(_onLoadRequested); + on(_onUpdateRequested); + on(_onDeleteAccountRequested); + + on(_onToggleNotifications); + on(_onToggleDarkMode); + on(_onUpdateFieldRequested); + } + + Future _onLoadRequested( + ProfileLoadRequested event, + Emitter emit, + ) async { + emit(state.copyWith(isLoading: true, clearAlert: true)); + + try { + final user = await userRepository.getUser(); + emit(state.copyWith(isLoading: false, user: user)); + } catch (e) { + emit( + state.copyWith( + isLoading: false, + alertMessage: 'Failed to load profile data', + isError: true, + ), + ); + } + } + + Future _onUpdateRequested( + ProfileUpdateRequested event, + Emitter emit, + ) async { + emit(state.copyWith(isLoading: true, clearAlert: true)); + + try { + final success = await apiService.updateUserProfile(event.updatedUser); + if (success) { + await userRepository.saveUser(event.updatedUser); + emit( + state.copyWith( + isLoading: false, + user: event.updatedUser, + alertMessage: 'Profile updated successfully', + isError: false, + ), + ); + } else { + emit( + state.copyWith( + isLoading: false, + alertMessage: 'Failed to update profile on server', + isError: true, + ), + ); + } + } catch (e) { + emit( + state.copyWith( + isLoading: false, + alertMessage: 'Connection error: $e', + isError: true, + ), + ); + } + } + + Future _onDeleteAccountRequested( + ProfileDeleteAccountRequested event, + Emitter emit, + ) async { + emit(state.copyWith(isLoading: true, clearAlert: true)); + + try { + final success = await apiService.deleteAccount(); + if (success) { + emit( + state.copyWith( + isLoading: false, + isAccountDeleted: true, + alertMessage: 'System Purge Complete. Account erased.', + isError: false, + ), + ); + } else { + emit( + state.copyWith( + isLoading: false, + alertMessage: 'Failed to initiate System Purge', + isError: true, + ), + ); + } + } catch (e) { + emit( + state.copyWith( + isLoading: false, + alertMessage: 'Connection error: $e', + isError: true, + ), + ); + } + } + + void _onToggleNotifications( + ProfileToggleNotifications event, + Emitter emit, + ) { + emit(state.copyWith(notifications: event.value)); + } + + void _onToggleDarkMode( + ProfileToggleDarkMode event, + Emitter emit, + ) { + emit(state.copyWith(darkMode: event.value)); + } + + Future _onUpdateFieldRequested( + ProfileUpdateFieldRequested event, + Emitter emit, + ) async { + final user = state.user; + if (user == null) return; + + final updatedUser = UserModel( + fullName: event.fieldTitle == 'Name' ? event.newValue : user.fullName, + email: event.fieldTitle == 'Email' ? event.newValue : user.email, + password: user.password, + department: + event.fieldTitle == 'Department' ? event.newValue : user.department, + ); + + emit(state.copyWith(isLoading: true, clearAlert: true)); + try { + final success = await apiService.updateUserProfile(updatedUser); + if (success) { + await userRepository.saveUser(updatedUser); + emit( + state.copyWith( + isLoading: false, + user: updatedUser, + alertMessage: '${event.fieldTitle} updated successfully!', + isError: false, + ), + ); + } else { + emit( + state.copyWith( + isLoading: false, + alertMessage: + 'Failed to update ${event.fieldTitle} (Check connection/email)', + isError: true, + ), + ); + } + } catch (e) { + emit( + state.copyWith( + isLoading: false, + alertMessage: 'Connection error: $e', + isError: true, + ), + ); + } + } +} diff --git a/lib/blocs/profile/profile_event.dart b/lib/blocs/profile/profile_event.dart new file mode 100644 index 0000000..0bb91bf --- /dev/null +++ b/lib/blocs/profile/profile_event.dart @@ -0,0 +1,44 @@ +import 'package:equatable/equatable.dart'; +import 'package:mobile_flutter_iot/models/user_model.dart'; + +abstract class ProfileEvent extends Equatable { + const ProfileEvent(); + + @override + List get props => []; +} + +class ProfileLoadRequested extends ProfileEvent {} + +class ProfileUpdateRequested extends ProfileEvent { + final UserModel updatedUser; + + const ProfileUpdateRequested(this.updatedUser); + + @override + List get props => [updatedUser]; +} + +class ProfileDeleteAccountRequested extends ProfileEvent {} + +class ProfileToggleNotifications extends ProfileEvent { + final bool value; + const ProfileToggleNotifications(this.value); + @override + List get props => [value]; +} + +class ProfileToggleDarkMode extends ProfileEvent { + final bool value; + const ProfileToggleDarkMode(this.value); + @override + List get props => [value]; +} + +class ProfileUpdateFieldRequested extends ProfileEvent { + final String fieldTitle; + final String newValue; + const ProfileUpdateFieldRequested(this.fieldTitle, this.newValue); + @override + List get props => [fieldTitle, newValue]; +} diff --git a/lib/blocs/profile/profile_state.dart b/lib/blocs/profile/profile_state.dart new file mode 100644 index 0000000..3eb35b8 --- /dev/null +++ b/lib/blocs/profile/profile_state.dart @@ -0,0 +1,54 @@ +import 'package:equatable/equatable.dart'; +import 'package:mobile_flutter_iot/models/user_model.dart'; + +class ProfileState extends Equatable { + final bool isLoading; + final UserModel? user; + final bool notifications; + final bool darkMode; + final String? alertMessage; + final bool isError; + final bool isAccountDeleted; + + const ProfileState({ + this.isLoading = false, + this.user, + this.notifications = true, + this.darkMode = true, + this.alertMessage, + this.isError = false, + this.isAccountDeleted = false, + }); + + ProfileState copyWith({ + bool? isLoading, + UserModel? user, + bool? notifications, + bool? darkMode, + String? alertMessage, + bool? isError, + bool? isAccountDeleted, + bool clearAlert = false, + }) { + return ProfileState( + isLoading: isLoading ?? this.isLoading, + user: user ?? this.user, + notifications: notifications ?? this.notifications, + darkMode: darkMode ?? this.darkMode, + alertMessage: clearAlert ? null : (alertMessage ?? this.alertMessage), + isError: !clearAlert && (isError ?? this.isError), + isAccountDeleted: isAccountDeleted ?? this.isAccountDeleted, + ); + } + + @override + List get props => [ + isLoading, + user, + notifications, + darkMode, + alertMessage, + isError, + isAccountDeleted, + ]; +} diff --git a/lib/cubits/add_device_cubit.dart b/lib/cubits/add_device_cubit.dart new file mode 100644 index 0000000..3b0a149 --- /dev/null +++ b/lib/cubits/add_device_cubit.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/models/device_model.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; + +class AddDeviceState { + final Color selectedColor; + final IconData selectedIcon; + final bool isLoading; + final bool? isSuccess; + final String? errorMessage; + + const AddDeviceState({ + required this.selectedColor, + required this.selectedIcon, + this.isLoading = false, + this.isSuccess, + this.errorMessage, + }); + + AddDeviceState copyWith({ + Color? selectedColor, + IconData? selectedIcon, + bool? isLoading, + bool? isSuccess, + String? errorMessage, + }) { + return AddDeviceState( + selectedColor: selectedColor ?? this.selectedColor, + selectedIcon: selectedIcon ?? this.selectedIcon, + isLoading: isLoading ?? this.isLoading, + isSuccess: isSuccess, + errorMessage: errorMessage, + ); + } +} + +class AddDeviceCubit extends Cubit { + final ApiService apiService; + + AddDeviceCubit({ + required this.apiService, + required Color initialColor, + required IconData initialIcon, + }) : super( + AddDeviceState( + selectedColor: initialColor, + selectedIcon: initialIcon, + ), + ); + + void selectColor(Color color) { + emit(state.copyWith(selectedColor: color)); + } + + void selectIcon(IconData icon) { + emit(state.copyWith(selectedIcon: icon)); + } + + Future saveDevice(DeviceModel device, bool isNew) async { + emit(state.copyWith(isLoading: true)); + + try { + final success = isNew + ? await apiService.addDevice(device) + : await apiService.updateDevice(device); + + if (success) { + emit(state.copyWith(isLoading: false, isSuccess: true)); + } else { + emit( + state.copyWith( + isLoading: false, + isSuccess: false, + errorMessage: 'API Error: Saved to local cache only.', + ), + ); + } + } catch (e) { + emit( + state.copyWith( + isLoading: false, + isSuccess: false, + errorMessage: 'Connection failed.', + ), + ); + } + } +} diff --git a/lib/cubits/auth_cubit.dart b/lib/cubits/auth_cubit.dart new file mode 100644 index 0000000..318c639 --- /dev/null +++ b/lib/cubits/auth_cubit.dart @@ -0,0 +1,120 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mobile_flutter_iot/models/user_model.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; + +abstract class AuthState {} + +class AuthInitial extends AuthState {} + +class AuthLoading extends AuthState {} + +class AuthAuthenticated extends AuthState { + final UserModel user; + final bool isOffline; + AuthAuthenticated(this.user, {this.isOffline = false}); +} + +class AuthUnauthenticated extends AuthState {} + +class AuthRegistered extends AuthState { + final bool isOffline; + AuthRegistered({this.isOffline = false}); +} + +class AuthError extends AuthState { + final String message; + AuthError(this.message); +} + +class AuthCubit extends Cubit { + final ApiService apiService; + final LocalUserRepository userRepository; + + final _storage = const FlutterSecureStorage(); + + AuthCubit({required this.apiService, required this.userRepository}) + : super(AuthInitial()); + + Future checkAuth() async { + emit(AuthLoading()); + + try { + final token = await _storage.read(key: 'access_token'); + if (token != null) { + final user = await userRepository.getUser(); + if (user != null) { + emit(AuthAuthenticated(user)); + return; + } + } + emit(AuthUnauthenticated()); + } catch (e) { + emit(AuthUnauthenticated()); + } + } + + Future login(String email, String password) async { + emit(AuthLoading()); + + try { + final result = await apiService.login(email, password); + + if (result != null && result['token'] != null) { + final token = result['token'].toString(); + final userData = result['user'] as Map; + + await _storage.write(key: 'access_token', value: token); + + final user = UserModel( + fullName: userData['fullName']?.toString() ?? 'Unknown', + email: userData['email']?.toString() ?? email, + password: password, + department: userData['department']?.toString() ?? 'Unknown', + ); + await userRepository.saveUser(user); + + emit(AuthAuthenticated(user)); + return; + } + } catch (e) { + // + } + + final localUser = await userRepository.getUser(); + + if (localUser != null && + localUser.email == email && + localUser.password == password) { + await _storage.write(key: 'access_token', value: 'offline_mode_token'); + emit(AuthAuthenticated(localUser, isOffline: true)); + } else { + emit(AuthError('Login Failed: Invalid credentials or Server Offline')); + emit(AuthUnauthenticated()); + } + } + + Future register(UserModel user) async { + emit(AuthLoading()); + try { + final success = await apiService.register(user); + if (success) { + await userRepository.saveUser(user); + emit(AuthRegistered()); + } else { + emit(AuthError('Server error or email already exists.')); + } + } catch (e) { + await userRepository.saveUser(user); + emit(AuthRegistered(isOffline: true)); + } + } + + Future logout() async { + emit(AuthLoading()); + await _storage.delete(key: 'access_token'); + await userRepository.deleteUser(); + emit(AuthUnauthenticated()); + } +} diff --git a/lib/cubits/control_cubit.dart b/lib/cubits/control_cubit.dart new file mode 100644 index 0000000..4c42c5b --- /dev/null +++ b/lib/cubits/control_cubit.dart @@ -0,0 +1,57 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ControlState { + final bool isLightOn; + final double brightness; + final bool isAcOn; + final bool isVentOn; + final bool isEmergencyTriggered; + + const ControlState({ + this.isLightOn = true, + this.brightness = 0.7, + this.isAcOn = true, + this.isVentOn = false, + this.isEmergencyTriggered = false, + }); + + ControlState copyWith({ + bool? isLightOn, + double? brightness, + bool? isAcOn, + bool? isVentOn, + bool? isEmergencyTriggered, + }) { + return ControlState( + isLightOn: isLightOn ?? this.isLightOn, + brightness: brightness ?? this.brightness, + isAcOn: isAcOn ?? this.isAcOn, + isVentOn: isVentOn ?? this.isVentOn, + isEmergencyTriggered: isEmergencyTriggered ?? false, + ); + } +} + +class ControlCubit extends Cubit { + ControlCubit() : super(const ControlState()); + + void toggleLight(bool value) => emit(state.copyWith(isLightOn: value)); + + void setBrightness(double value) => emit(state.copyWith(brightness: value)); + + void toggleAc(bool value) => emit(state.copyWith(isAcOn: value)); + + void toggleVent(bool value) => emit(state.copyWith(isVentOn: value)); + + void emergencyShutdown() { + emit( + state.copyWith( + isLightOn: false, + brightness: 0, + isAcOn: false, + isVentOn: false, + isEmergencyTriggered: true, + ), + ); + } +} diff --git a/lib/cubits/details_cubit.dart b/lib/cubits/details_cubit.dart new file mode 100644 index 0000000..fe87eb1 --- /dev/null +++ b/lib/cubits/details_cubit.dart @@ -0,0 +1,126 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; + +class DetailsState { + final bool isSavingSnapshot; + final bool isManualControlOn; + final String? currentValue; + final String? customIp; + final String? alertMessage; + final bool isError; + final bool isDeleted; + + const DetailsState({ + this.isSavingSnapshot = false, + this.isManualControlOn = true, + this.currentValue, + this.customIp, + this.alertMessage, + this.isError = false, + this.isDeleted = false, + }); + + DetailsState copyWith({ + bool? isSavingSnapshot, + bool? isManualControlOn, + String? currentValue, + String? customIp, + String? alertMessage, + bool? isError, + bool? isDeleted, + }) { + return DetailsState( + isSavingSnapshot: isSavingSnapshot ?? this.isSavingSnapshot, + isManualControlOn: isManualControlOn ?? this.isManualControlOn, + currentValue: currentValue ?? this.currentValue, + customIp: customIp ?? this.customIp, + alertMessage: alertMessage, + isError: isError ?? this.isError, + isDeleted: isDeleted ?? this.isDeleted, + ); + } +} + +class DetailsCubit extends Cubit { + final ApiService apiService; + final LocalUserRepository userRepository; + + DetailsCubit({required this.apiService, required this.userRepository}) + : super(const DetailsState()); + + Future saveSnapshot(String id, String value) async { + emit(state.copyWith(isSavingSnapshot: true)); + final success = await apiService.saveLog(id, value); + + if (success) { + emit( + state.copyWith( + isSavingSnapshot: false, + alertMessage: 'Snapshot saved to cloud! 📸', + isError: false, + ), + ); + } else { + emit( + state.copyWith( + isSavingSnapshot: false, + alertMessage: 'Failed to save snapshot. Check connection.', + isError: true, + ), + ); + } + } + + Future deleteDevice(String deviceId) async { + final success = await apiService.deleteDevice(deviceId); + if (success) { + final devices = await userRepository.getDevices(); + devices.removeWhere((d) => d.id == deviceId); + await userRepository.saveDevices(devices); + emit( + state.copyWith( + isDeleted: true, + alertMessage: 'Device deleted from cloud!', + ), + ); + } else { + emit( + state.copyWith( + alertMessage: 'Failed to delete (Check connection)', + isError: true, + ), + ); + } + } + + Future updateValue(String id, String newValue) async { + final devices = await userRepository.getDevices(); + final index = devices.indexWhere((d) => d.id == id); + + if (index != -1) { + devices[index].value = newValue; + await apiService.updateDevice(devices[index]); + await userRepository.saveDevices(devices); + emit( + state.copyWith( + currentValue: newValue, + alertMessage: 'Value updated!', + ), + ); + } + } + + void updateIp(String newIp) { + emit( + state.copyWith( + customIp: newIp, + alertMessage: 'Reconnecting to $newIp...', + ), + ); + } + + void toggleManualControl(bool value) { + emit(state.copyWith(isManualControlOn: value)); + } +} diff --git a/lib/cubits/device_cubit.dart b/lib/cubits/device_cubit.dart new file mode 100644 index 0000000..0fa5aa7 --- /dev/null +++ b/lib/cubits/device_cubit.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_bloc/flutter_bloc.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/services/api_service.dart'; +import 'package:shake/shake.dart'; + +abstract class DeviceState {} + +class DeviceInitial extends DeviceState {} + +class DeviceLoading extends DeviceState {} + +class DeviceLoaded extends DeviceState { + final List devices; + final String? alertMessage; + final bool isError; + + DeviceLoaded(this.devices, {this.alertMessage, this.isError = false}); +} + +class DeviceCubit extends Cubit { + final ApiService apiService; + final LocalUserRepository localRepo; + + StreamSubscription>? _connectivitySub; + ShakeDetector? _shakeDetector; + + DeviceCubit({required this.apiService, required this.localRepo}) + : super(DeviceInitial()) { + _initListeners(); + loadDevices(); + } + + void _initListeners() { + _connectivitySub = Connectivity().onConnectivityChanged.listen((results) { + final hasNet = !results.contains(ConnectivityResult.none); + if (hasNet && state is DeviceLoaded) { + loadDevices(alertMessage: 'ONLINE: Connection restored! Syncing...'); + } else if (!hasNet) { + loadDevices( + alertMessage: 'OFFLINE: Check your Wi-Fi connection', + isError: true, + ); + } + }); + + _shakeDetector = ShakeDetector.autoStart( + onPhoneShake: (_) => loadDevices(alertMessage: 'SHAKE: Data refreshed'), + shakeThresholdGravity: 1.5, + ); + } + + Future loadDevices({String? alertMessage, bool isError = false}) async { + if (state is! DeviceLoaded) emit(DeviceLoading()); + + final hasNet = !(await Connectivity().checkConnectivity()) + .contains(ConnectivityResult.none); + + if (!hasNet) { + final localDevices = await localRepo.getDevices(); + emit( + DeviceLoaded( + localDevices, + alertMessage: alertMessage ?? 'Offline Mode', + isError: true, + ), + ); + return; + } + + try { + final devices = await apiService.fetchDevices(); + if (devices != null) { + await localRepo.saveDevices(devices); + emit(DeviceLoaded(devices, alertMessage: alertMessage)); + } else { + throw Exception('API returned null'); + } + } catch (e) { + final localDevices = await localRepo.getDevices(); + emit( + DeviceLoaded( + localDevices, + alertMessage: 'Sync failed. Using local data.', + isError: true, + ), + ); + } + } + + Future addOrUpdateDeviceLocally(DeviceModel device) async { + List currentDevices = []; + + if (state is DeviceLoaded) { + currentDevices = List.from((state as DeviceLoaded).devices); + } else { + currentDevices = await localRepo.getDevices(); + } + + final index = currentDevices.indexWhere((d) => d.id == device.id); + if (index >= 0) { + currentDevices[index] = device; + } else { + currentDevices.add(device); + } + + await localRepo.saveDevices(currentDevices); + + emit( + DeviceLoaded( + currentDevices, + alertMessage: 'Device cached locally 🛡️', + ), + ); + } + + @override + Future close() { + _connectivitySub?.cancel(); + _shakeDetector?.stopListening(); + return super.close(); + } +} diff --git a/lib/cubits/flashlight_cubit.dart b/lib/cubits/flashlight_cubit.dart new file mode 100644 index 0000000..99a323a --- /dev/null +++ b/lib/cubits/flashlight_cubit.dart @@ -0,0 +1,72 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:iot_flashlight/iot_flashlight.dart'; +import 'package:shake/shake.dart'; + +class FlashlightState { + final bool isOn; + final String? errorMessage; + + const FlashlightState({this.isOn = false, this.errorMessage}); + + FlashlightState copyWith({bool? isOn, String? errorMessage}) { + return FlashlightState( + isOn: isOn ?? this.isOn, + errorMessage: errorMessage, + ); + } +} + +class FlashlightCubit extends Cubit { + ShakeDetector? _detector; + int _tapCount = 0; + DateTime? _lastTapTime; + + FlashlightCubit() : super(const FlashlightState()) { + _initShakeDetector(); + } + + void _initShakeDetector() { + _detector = ShakeDetector.autoStart( + onPhoneShake: (_) { + debugPrint('📳 Shake detected via Cubit!'); + toggleFlashlight(); + }, + ); + } + + Future toggleFlashlight() async { + try { + final isOn = await IotFlashlight.toggle(); + emit(state.copyWith(isOn: isOn)); + } catch (e) { + if (e.toString().contains('UNSUPPORTED_PLATFORM')) { + emit(state.copyWith(errorMessage: 'UNSUPPORTED_PLATFORM')); + } else { + emit(state.copyWith(errorMessage: e.toString())); + } + } + } + + void handleSecretTap() { + final now = DateTime.now(); + if (_lastTapTime == null || + now.difference(_lastTapTime!) > const Duration(milliseconds: 500)) { + _tapCount = 1; + } else { + _tapCount++; + } + _lastTapTime = now; + + if (_tapCount == 5) { + _tapCount = 0; + toggleFlashlight(); + } + } + + @override + Future close() { + _detector?.stopListening(); + return super.close(); + } +} diff --git a/lib/cubits/home_cubit.dart b/lib/cubits/home_cubit.dart new file mode 100644 index 0000000..afd2a3e --- /dev/null +++ b/lib/cubits/home_cubit.dart @@ -0,0 +1,35 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class HomeState { + final bool isSystemOn; + final String? alertMessage; + + const HomeState({ + this.isSystemOn = true, + this.alertMessage, + }); + + HomeState copyWith({ + bool? isSystemOn, + String? alertMessage, + }) { + return HomeState( + isSystemOn: isSystemOn ?? this.isSystemOn, + alertMessage: alertMessage, + ); + } +} + +class HomeCubit extends Cubit { + HomeCubit() : super(const HomeState()); + + void toggleSystemPower() { + final newState = !state.isSystemOn; + emit( + state.copyWith( + isSystemOn: newState, + alertMessage: newState ? 'SYSTEM INITIALIZED' : 'SYSTEM SHUTDOWN', + ), + ); + } +} diff --git a/lib/cubits/mqtt_cubit.dart b/lib/cubits/mqtt_cubit.dart new file mode 100644 index 0000000..1a500d7 --- /dev/null +++ b/lib/cubits/mqtt_cubit.dart @@ -0,0 +1,173 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; +import 'package:mqtt_client/mqtt_client.dart'; +import 'package:mqtt_client/mqtt_server_client.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +enum MqttStatus { disconnected, connecting, connected, error } + +class MqttState { + final MqttStatus status; + final String airQuality; + final bool isLedOn; + final int startLockHour; + final int endLockHour; + final MqttServerClient? client; + + const MqttState({ + this.status = MqttStatus.disconnected, + this.airQuality = '0', + this.isLedOn = false, + this.startLockHour = 22, + this.endLockHour = 6, + this.client, + }); + + MqttState copyWith({ + MqttStatus? status, + String? airQuality, + bool? isLedOn, + int? startLockHour, + int? endLockHour, + MqttServerClient? client, + }) { + return MqttState( + status: status ?? this.status, + airQuality: airQuality ?? this.airQuality, + isLedOn: isLedOn ?? this.isLedOn, + startLockHour: startLockHour ?? this.startLockHour, + endLockHour: endLockHour ?? this.endLockHour, + client: client ?? this.client, + ); + } +} + +class MqttCubit extends Cubit { + final ApiService _apiService; + bool _hasLoggedReadViolation = false; + StreamSubscription>>? _updatesSub; + + MqttCubit({required ApiService apiService}) + : _apiService = apiService, + super(const MqttState()); + + Future loadLockPolicy() async { + final prefs = await SharedPreferences.getInstance(); + emit( + state.copyWith( + startLockHour: prefs.getInt('start_lock') ?? 22, + endLockHour: prefs.getInt('end_lock') ?? 6, + ), + ); + } + + Future updateLockHours(int start, int end) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('start_lock', start); + await prefs.setInt('end_lock', end); + _hasLoggedReadViolation = false; + emit(state.copyWith(startLockHour: start, endLockHour: end)); + } + + bool isTimeRestricted() { + final hour = DateTime.now().hour; + if (state.startLockHour > state.endLockHour) { + return hour >= state.startLockHour || hour < state.endLockHour; + } + return hour >= state.startLockHour && hour < state.endLockHour; + } + + void initMqtt(String broker, String clientId) { + loadLockPolicy(); + final client = MqttServerClient(broker, clientId) + ..port = 1883 + ..logging(on: false) + ..keepAlivePeriod = 20 + ..autoReconnect = true; + + client.onDisconnected = + () => emit(state.copyWith(status: MqttStatus.disconnected)); + client.onConnected = () { + emit(state.copyWith(status: MqttStatus.connected, client: client)); + _subscribeToTopics(); + }; + + emit(state.copyWith(client: client)); + } + + Future connect() async { + if (state.client == null || + state.status == MqttStatus.connected || + state.status == MqttStatus.connecting) { + return; + } + emit(state.copyWith(status: MqttStatus.connecting)); + try { + await state.client!.connect(); + } catch (e) { + debugPrint('MQTT ERROR: $e'); + if (state.client?.connectionStatus?.state != + MqttConnectionState.disconnected) { + state.client?.disconnect(); + } + emit(state.copyWith(status: MqttStatus.error)); + } + } + + void _subscribeToTopics() { + if (state.client == null || state.status != MqttStatus.connected) return; + state.client!.subscribe('sensors/air', MqttQos.atMostOnce); + + _updatesSub?.cancel(); + _updatesSub = state.client!.updates!.listen((messages) async { + if (isTimeRestricted()) { + emit(state.copyWith(airQuality: '0')); + if (!_hasLoggedReadViolation) { + await _apiService.saveLog( + 'SECURITY_POLICY', + 'VIOLATION: Blocked stream at (${DateTime.now().hour}:00)', + ); + _hasLoggedReadViolation = true; + } + return; + } + _hasLoggedReadViolation = false; + final recMess = messages[0].payload as MqttPublishMessage; + final payload = + MqttPublishPayload.bytesToStringAsString(recMess.payload.message); + emit(state.copyWith(airQuality: payload)); + }); + } + + Future toggleLed() async { + if (state.client == null || state.status != MqttStatus.connected) return; + if (isTimeRestricted()) { + await _apiService.saveLog( + 'SECURITY_POLICY', + 'VIOLATION: LED control blocked at (${DateTime.now().hour}:00)', + ); + return; + } + final newState = !state.isLedOn; + final builder = MqttClientPayloadBuilder() + ..addString(newState ? 'ON' : 'OFF'); + state.client! + .publishMessage('commands/led', MqttQos.atMostOnce, builder.payload!); + emit(state.copyWith(isLedOn: newState)); + } + + void disconnect() { + state.client?.disconnect(); + _updatesSub?.cancel(); + emit(state.copyWith(status: MqttStatus.disconnected)); + } + + @override + Future close() { + _updatesSub?.cancel(); + state.client?.disconnect(); + return super.close(); + } +} diff --git a/lib/cubits/profile_cubit.dart b/lib/cubits/profile_cubit.dart new file mode 100644 index 0000000..633cdcf --- /dev/null +++ b/lib/cubits/profile_cubit.dart @@ -0,0 +1,109 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/models/user_model.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; + +class ProfileState { + final UserModel? user; + final bool notifications; + final bool darkMode; + final String? alertMessage; + final bool isError; + final bool isAccountDeleted; + + const ProfileState({ + this.user, + this.notifications = true, + this.darkMode = true, + this.alertMessage, + this.isError = false, + this.isAccountDeleted = false, + }); + + ProfileState copyWith({ + UserModel? user, + bool? notifications, + bool? darkMode, + String? alertMessage, + bool? isError, + bool? isAccountDeleted, + }) { + return ProfileState( + user: user ?? this.user, + notifications: notifications ?? this.notifications, + darkMode: darkMode ?? this.darkMode, + alertMessage: alertMessage, + isError: isError ?? this.isError, + isAccountDeleted: isAccountDeleted ?? this.isAccountDeleted, + ); + } +} + +class ProfileCubit extends Cubit { + final ApiService apiService; + final LocalUserRepository userRepository; + + ProfileCubit({required this.apiService, required this.userRepository}) + : super(const ProfileState()) { + loadUser(); + } + + Future loadUser() async { + final user = await userRepository.getUser(); + emit(state.copyWith(user: user)); + } + + void toggleNotifications(bool value) => + emit(state.copyWith(notifications: value)); + void toggleDarkMode(bool value) => emit(state.copyWith(darkMode: value)); + + Future updateField(String field, String newValue) async { + final user = state.user; + if (user == null) return; + + final updatedUser = UserModel( + fullName: field == 'Name' ? newValue : user.fullName, + email: field == 'Email' ? newValue : user.email, + password: user.password, + department: field == 'Department' ? newValue : user.department, + ); + + final success = await apiService.updateUserProfile(updatedUser); + if (success) { + await userRepository.saveUser(updatedUser); + emit( + state.copyWith( + user: updatedUser, + alertMessage: '$field updated successfully!', + ), + ); + } else { + emit( + state.copyWith( + alertMessage: 'Failed to update $field (Check connection/email)', + isError: true, + ), + ); + } + } + + Future deleteAccount() async { + final success = await apiService.deleteAccount(); + if (success) { + await userRepository.deleteUser(); + emit( + state.copyWith( + isAccountDeleted: true, + alertMessage: 'Cloud Account Destroyed.', + ), + ); + } else { + emit( + state.copyWith( + alertMessage: 'Failed to delete account.', + isError: true, + ), + ); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 5ad33a8..1395f3c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,32 +1,63 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/providers/auth_provider.dart'; -import 'package:mobile_flutter_iot/providers/mqtt_provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mobile_flutter_iot/cubits/auth_cubit.dart'; +import 'package:mobile_flutter_iot/cubits/device_cubit.dart'; +import 'package:mobile_flutter_iot/cubits/mqtt_cubit.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; import 'package:mobile_flutter_iot/screens/auth/login_screen.dart'; import 'package:mobile_flutter_iot/screens/auth/register_screen.dart'; import 'package:mobile_flutter_iot/screens/home/details_screen.dart'; import 'package:mobile_flutter_iot/screens/main/main_wrapper.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; import 'package:mobile_flutter_iot/services/connectivity_service.dart'; -import 'package:provider/provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - final authProvider = AuthProvider(); - await authProvider.checkAuth(); + final apiService = ApiService(); + final localUserRepository = LocalUserRepository(); + final connectivityService = ConnectivityService(); + + final token = await const FlutterSecureStorage().read(key: 'access_token'); + final initialRoute = token != null ? '/main' : '/login'; runApp( - MultiProvider( + MultiRepositoryProvider( providers: [ - ChangeNotifierProvider.value(value: authProvider), - ChangeNotifierProvider(create: (_) => MqttProvider()), + RepositoryProvider.value(value: apiService), + RepositoryProvider.value(value: localUserRepository), + RepositoryProvider.value(value: connectivityService), ], - child: const SmartWorkspaceApp(), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => AuthCubit( + apiService: context.read(), + userRepository: context.read(), + )..checkAuth(), + ), + BlocProvider( + create: (context) => DeviceCubit( + apiService: context.read(), + localRepo: context.read(), + ), + ), + BlocProvider( + create: (context) => MqttCubit( + apiService: context.read(), + ), + ), + ], + child: SmartWorkspaceApp(initialRoute: initialRoute), + ), ), ); } class SmartWorkspaceApp extends StatelessWidget { - const SmartWorkspaceApp({super.key}); + final String initialRoute; + const SmartWorkspaceApp({required this.initialRoute, super.key}); @override Widget build(BuildContext context) { @@ -43,7 +74,7 @@ class SmartWorkspaceApp extends StatelessWidget { error: Color(0xFFF87171), ), ), - home: const RootHandler(), + initialRoute: initialRoute, routes: { '/login': (context) => const LoginScreen(), '/register': (context) => const RegisterScreen(), @@ -54,56 +85,95 @@ class SmartWorkspaceApp extends StatelessWidget { } } -class RootHandler extends StatefulWidget { - const RootHandler({super.key}); - - @override - State createState() => _RootHandlerState(); -} - -class _RootHandlerState extends State { - @override - void initState() { - super.initState(); - _checkInitialConnectivity(); - } +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ - Future _checkInitialConnectivity() async { - final isOnline = await ConnectivityService().hasConnection(); +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_event.dart'; +// import 'package:mobile_flutter_iot/blocs/device/device_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_bloc.dart'; +// import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +// import 'package:mobile_flutter_iot/screens/auth/login_screen.dart'; +// import 'package:mobile_flutter_iot/screens/auth/register_screen.dart'; +// import 'package:mobile_flutter_iot/screens/home/details_screen.dart'; +// import 'package:mobile_flutter_iot/screens/main/main_wrapper.dart'; +// import 'package:mobile_flutter_iot/services/api_service.dart'; +// import 'package:mobile_flutter_iot/services/connectivity_service.dart'; - if (!mounted) return; +// void main() async { +// WidgetsFlutterBinding.ensureInitialized(); - final auth = Provider.of(context, listen: false); +// final apiService = ApiService(); +// final localUserRepository = LocalUserRepository(); +// final connectivityService = ConnectivityService(); - if (!auth.isLoggedIn) return; +// final token = await const FlutterSecureStorage().read(key: 'access_token'); +// final initialRoute = token != null ? '/main' : '/login'; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; +// runApp( +// MultiRepositoryProvider( +// providers: [ +// RepositoryProvider.value(value: apiService), +// RepositoryProvider.value(value: localUserRepository), +// RepositoryProvider.value(value: connectivityService), +// ], +// child: MultiBlocProvider( +// providers: [ +// BlocProvider( +// create: (context) => AuthBloc( +// apiService: context.read(), +// userRepository: context.read(), +// )..add(AuthCheckRequested()), // ЗМІНЕНО НА EVENT +// ), +// BlocProvider( +// create: (context) => DeviceBloc( +// apiService: context.read(), +// localRepo: context.read(), +// ), +// ), +// BlocProvider( +// create: (context) => +// MqttBloc(), +// ), +// ], +// child: SmartWorkspaceApp(initialRoute: initialRoute), +// ), +// ), +// ); +// } - if (!isOnline) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Offline Mode: Session loaded from cache.'), - backgroundColor: Colors.orange, - behavior: SnackBarBehavior.floating, - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('System Online. Syncing data...'), - backgroundColor: Color(0xFF4ADE80), - behavior: SnackBarBehavior.floating, - duration: Duration(seconds: 2), - ), - ); - } - }); - } +// class SmartWorkspaceApp extends StatelessWidget { +// final String initialRoute; +// const SmartWorkspaceApp({required this.initialRoute, super.key}); - @override - Widget build(BuildContext context) { - final auth = context.watch(); - return auth.isLoggedIn ? const MainWrapper() : const LoginScreen(); - } -} +// @override +// Widget build(BuildContext context) { +// return MaterialApp( +// title: 'Smart Workspace Monitor', +// debugShowCheckedModeBanner: false, +// theme: ThemeData( +// brightness: Brightness.dark, +// scaffoldBackgroundColor: const Color(0xFF0F172A), +// primaryColor: const Color(0xFF38BDF8), +// colorScheme: const ColorScheme.dark( +// primary: Color(0xFF38BDF8), +// secondary: Color(0xFF4ADE80), +// error: Color(0xFFF87171), +// ), +// ), +// initialRoute: initialRoute, +// routes: { +// '/login': (context) => const LoginScreen(), +// '/register': (context) => const RegisterScreen(), +// '/main': (context) => const MainWrapper(), +// '/details': (context) => const DetailsScreen(), +// }, +// ); +// } +// } diff --git a/lib/profile/profile_screen.dart b/lib/profile/profile_screen.dart deleted file mode 100644 index 3159ee1..0000000 --- a/lib/profile/profile_screen.dart +++ /dev/null @@ -1,523 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/models/user_model.dart'; -import 'package:mobile_flutter_iot/providers/auth_provider.dart'; -import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; -import 'package:mobile_flutter_iot/services/api_service.dart'; -import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; -import 'package:mobile_flutter_iot/widgets/glass_card.dart'; -import 'package:mobile_flutter_iot/widgets/profile_item.dart'; -import 'package:provider/provider.dart'; - -class ProfileScreen extends StatefulWidget { - const ProfileScreen({super.key}); - - @override - State createState() => _ProfileScreenState(); -} - -class _ProfileScreenState extends State { - bool _notifications = true; - bool _darkMode = true; - - UserModel? _currentUser; - final _userRepository = LocalUserRepository(); - final _apiService = ApiService(); - - @override - void initState() { - super.initState(); - _loadUserData(); - } - - Future _loadUserData() async { - final user = await _userRepository.getUser(); - if (mounted) { - setState(() { - _currentUser = user; - }); - } - } - - void _showLogoutDialog() { - showDialog( - context: context, - builder: (dialogContext) { - return AlertDialog( - backgroundColor: const Color(0xFF1E293B), - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - title: const Text( - 'System Termination', - style: TextStyle(color: Colors.white), - ), - content: const Text('Are you sure you want to logout?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('CANCEL'), - ), - TextButton( - onPressed: () async { - final auth = Provider.of(context, listen: false); - Navigator.pop(dialogContext); - await auth.logout(); - - if (mounted) { - Navigator.pushNamedAndRemoveUntil( - context, - '/login', - (route) => false, - ); - } - }, - child: const Text( - 'LOGOUT', - style: TextStyle( - color: Colors.redAccent, - ), - ), - ), - ], - ); - }, - ); - } - - Future _deleteAccount() async { - final authProvider = context.read(); - - 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, cloud data, ' - '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)), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF87171).withValues(alpha: 0.2), - foregroundColor: const Color(0xFFF87171), - side: const BorderSide(color: Color(0xFFF87171)), - ), - onPressed: () => Navigator.pop(context, true), - child: const Text('CONFIRM PURGE'), - ), - ], - ), - ); - - if (confirm == true) { - final success = await _apiService.deleteAccount(); - - if (!mounted) return; - - if (success) { - await _userRepository.deleteUser(); - - await authProvider.logout(); - - if (!mounted) return; - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Cloud Account Destroyed.'), - backgroundColor: Colors.redAccent, - ), - ); - Navigator.pushNamedAndRemoveUntil( - context, - '/login', - (route) => false, - ); - } else { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to delete cloud account. Check connection.'), - backgroundColor: Colors.orange, - ), - ); - } - } - } - - Future _editProfileField( - String title, - String currentValue, - Future Function(String) onSave, - ) async { - final controller = TextEditingController(text: currentValue); - - return showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - backgroundColor: const Color(0xFF1E293B), - title: - Text('Update $title', style: const TextStyle(color: Colors.white)), - content: TextField( - controller: controller, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: 'Enter new $title', - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white24), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () async { - await onSave(controller.text); - - if (!dialogContext.mounted) return; - Navigator.pop(dialogContext); - _loadUserData(); - }, - child: - const Text('Save', style: TextStyle(color: Color(0xFF38BDF8))), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; - final auth = context.watch(); - - return Scaffold( - backgroundColor: Colors.transparent, - body: Stack( - children: [ - BlurBlob( - alignment: Alignment.topRight, - translation: const Offset(0.3, -0.3), - color: const Color(0xFF38BDF8).withValues(alpha: 0.1), - size: size.width * 0.7, - ), - BlurBlob( - alignment: Alignment.bottomLeft, - translation: const Offset(-0.3, 0.3), - color: const Color(0xFF4ADE80).withValues(alpha: 0.08), - size: size.width * 0.8, - ), - CustomScrollView( - physics: const BouncingScrollPhysics(), - slivers: [ - const SliverAppBar( - expandedHeight: 100, - pinned: true, - backgroundColor: Colors.transparent, - flexibleSpace: FlexibleSpaceBar( - centerTitle: true, - title: Text( - 'USER PROFILE', - style: TextStyle( - letterSpacing: 2, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - SliverPadding( - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 20), - sliver: SliverList( - delegate: SliverChildListDelegate([ - _buildHeader(), - const SizedBox(height: 32), - _buildSecurityStatus(auth.isLoggedIn), - 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: 32), - const Text( - 'ACCOUNT ACTIONS', - style: TextStyle( - color: Colors.white24, - fontSize: 11, - letterSpacing: 1.5, - ), - ), - const SizedBox(height: 12), - _buildActionButtons(), - const SizedBox(height: 40), - ]), - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildHeader() { - 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), - ], - ), - ), - child: const CircleAvatar( - radius: 55, - backgroundColor: Color(0xFF0F172A), - child: CircleAvatar( - radius: 51, - backgroundColor: Color(0xFF1E293B), - child: Icon(Icons.person_rounded, size: 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, - ); - final success = await _apiService.updateUserProfile(updated); - if (success) { - await _userRepository.saveUser(updated); - } else if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to sync with cloud'), - backgroundColor: Colors.orange, - ), - ); - } - } - }), - child: Text( - name, - style: const TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 4), - Text( - dept.toUpperCase(), - style: const TextStyle( - color: Color(0xFF4ADE80), - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () => _editProfileField('Email', email, (val) async { - if (_currentUser != null) { - final updated = UserModel( - fullName: _currentUser!.fullName, - email: val, - password: _currentUser!.password, - department: _currentUser!.department, - ); - final success = await _apiService.updateUserProfile(updated); - if (success) { - await _userRepository.saveUser(updated); - } else if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Email might be taken or offline'), - backgroundColor: Colors.orange, - ), - ); - } - } - }), - 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), - ), - ), - ), - ], - ); - } - - Widget _buildSecurityStatus(bool isActive) => GlassCard( - child: Row( - children: [ - Icon( - isActive - ? Icons.verified_user_rounded - : Icons.shield_moon_outlined, - color: isActive ? const Color(0xFF4ADE80) : Colors.orangeAccent, - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Security Protocol', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text( - isActive - ? 'Persistent Session: Active' - : 'Session Encryption: Local Only', - style: const TextStyle(fontSize: 11, color: Colors.white38), - ), - ], - ), - ], - ), - ); - - Widget _buildSettingsGroup() => GlassCard( - child: Column( - children: [ - ProfileMenuItem( - icon: Icons.notifications_active_outlined, - title: 'System Alerts', - isSwitch: true, - value: _notifications, - onChanged: (v) => setState(() => _notifications = v), - ), - const Divider(color: Colors.white10), - ProfileMenuItem( - icon: Icons.palette_outlined, - title: 'OLED Dark Mode', - isSwitch: true, - value: _darkMode, - onChanged: (v) => setState(() => _darkMode = v), - ), - const Divider(color: Colors.white10), - ProfileMenuItem( - icon: Icons.hub_outlined, - title: 'Department Unit', - trailingText: _currentUser?.department ?? '...', - onTap: () => _editProfileField( - 'Department', _currentUser?.department ?? '', (val) async { - if (_currentUser != null) { - final updated = UserModel( - fullName: _currentUser!.fullName, - email: _currentUser!.email, - password: _currentUser!.password, - department: val, - ); - final success = await _apiService.updateUserProfile(updated); - if (success) { - await _userRepository.saveUser(updated); - } else if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to sync with cloud'), - backgroundColor: Colors.orange, - ), - ); - } - } - }), - ), - ], - ), - ); - - Widget _buildActionButtons() => Column( - children: [ - GlassCard( - padding: EdgeInsets.zero, - child: ListTile( - onTap: _showLogoutDialog, - leading: const Icon(Icons.logout, color: Color(0xFFF87171)), - title: const Text( - 'Logout', - style: TextStyle( - color: Color(0xFFF87171), - fontWeight: FontWeight.bold, - ), - ), - trailing: const Icon( - Icons.arrow_forward_ios_rounded, - size: 14, - color: Colors.white24, - ), - ), - ), - 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/providers/auth_provider.dart b/lib/providers/auth_provider.dart deleted file mode 100644 index 20cf315..0000000 --- a/lib/providers/auth_provider.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:mobile_flutter_iot/services/api_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class AuthProvider with ChangeNotifier { - final _storage = const FlutterSecureStorage(); - final _apiService = ApiService(); - - bool _isLoggedIn = false; - String? _userEmail; - String? _userName; - - bool get isLoggedIn => _isLoggedIn; - String? get userEmail => _userEmail; - String? get userName => _userName; - - Future checkAuth() async { - final prefs = await SharedPreferences.getInstance(); - - final token = await _storage.read(key: 'access_token'); - - _isLoggedIn = (prefs.getBool('isLoggedIn') ?? false) && (token != null); - - if (_isLoggedIn) { - _userEmail = await _storage.read(key: 'user_email'); - _userName = await _storage.read(key: 'user_name'); - } - - notifyListeners(); - } - - Future login(String email, String password) async { - try { - final responseData = await _apiService.login(email, password); - - if (responseData != null && responseData.containsKey('token')) { - final prefs = await SharedPreferences.getInstance(); - - final token = responseData['token'].toString(); - final userJson = responseData['user'] as Map; - final userName = userJson['fullName'].toString(); - final userEmail = userJson['email'].toString(); - - await prefs.setBool('isLoggedIn', true); - await _storage.write(key: 'access_token', value: token); - await _storage.write(key: 'user_email', value: userEmail); - await _storage.write(key: 'user_name', value: userName); - - _isLoggedIn = true; - _userEmail = userEmail; - _userName = userName; - - notifyListeners(); - return true; - } - } catch (e) { - debugPrint('Auth Error: $e'); - } - return false; - } - - Future logout() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('isLoggedIn', false); - - await _storage.deleteAll(); - - _isLoggedIn = false; - _userEmail = null; - _userName = null; - - notifyListeners(); - } -} diff --git a/lib/providers/mqtt_provider.dart b/lib/providers/mqtt_provider.dart deleted file mode 100644 index 00930a4..0000000 --- a/lib/providers/mqtt_provider.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/services/api_service.dart'; -import 'package:mqtt_client/mqtt_client.dart'; -import 'package:mqtt_client/mqtt_server_client.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -enum MqttStatus { disconnected, connecting, connected, error } - -class MqttProvider with ChangeNotifier { - MqttServerClient? client; - MqttStatus _status = MqttStatus.disconnected; - String _airQuality = '0'; - bool _isLedOn = false; - final ApiService _apiService = ApiService(); - - int startLockHour = 22; - int endLockHour = 6; - - bool _hasLoggedReadViolation = false; - - MqttStatus get status => _status; - String get airQuality => _airQuality; - bool get isLedOn => _isLedOn; - - Future loadLockPolicy() async { - final prefs = await SharedPreferences.getInstance(); - startLockHour = prefs.getInt('start_lock') ?? 22; - endLockHour = prefs.getInt('end_lock') ?? 6; - notifyListeners(); - } - - Future updateLockHours(int start, int end) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setInt('start_lock', start); - await prefs.setInt('end_lock', end); - startLockHour = start; - endLockHour = end; - - _hasLoggedReadViolation = false; - notifyListeners(); - } - - bool isTimeRestricted() { - final now = DateTime.now(); - final hour = now.hour; - - if (startLockHour > endLockHour) { - return hour >= startLockHour || hour < endLockHour; - } else { - return hour >= startLockHour && hour < endLockHour; - } - } - - void initMqtt(String broker, String clientId) { - loadLockPolicy(); - - client = MqttServerClient(broker, clientId); - client!.port = 1883; - client!.logging(on: false); - client!.keepAlivePeriod = 20; - client!.autoReconnect = true; - - client!.onDisconnected = () { - _status = MqttStatus.disconnected; - notifyListeners(); - }; - - client!.onConnected = () { - _status = MqttStatus.connected; - _subscribeToTopics(); - notifyListeners(); - }; - - client!.onAutoReconnect = () => debugPrint('MQTT: Auto reconnecting...'); - client!.onAutoReconnected = () => debugPrint('MQTT: Auto reconnected'); - } - - Future connect() async { - if (client == null) return; - if (_status == MqttStatus.connected || _status == MqttStatus.connecting) { - return; - } - - _status = MqttStatus.connecting; - notifyListeners(); - - try { - await client!.connect(); - } catch (e) { - debugPrint('MQTT ERROR: $e'); - _status = MqttStatus.error; - if (client?.connectionStatus?.state != MqttConnectionState.disconnected) { - client?.disconnect(); - } - notifyListeners(); - } - } - - void _subscribeToTopics() { - if (client == null || _status != MqttStatus.connected) return; - - client!.subscribe('sensors/air', MqttQos.atMostOnce); - - client!.updates!.listen( - (List> messages) async { - if (isTimeRestricted()) { - _airQuality = '0'; - notifyListeners(); - - if (!_hasLoggedReadViolation) { - await _apiService.saveLog( - 'SECURITY_POLICY', - 'VIOLATION: Blocked incoming telemetry stream' - 'at (${DateTime.now().hour}:00)', - ); - _hasLoggedReadViolation = true; - debugPrint('Read blocked by time policy'); - } - return; - } - - _hasLoggedReadViolation = false; - - final recMess = messages[0].payload as MqttPublishMessage; - final payload = - MqttPublishPayload.bytesToStringAsString(recMess.payload.message); - _airQuality = payload; - notifyListeners(); - }, - onError: (Object error) { - debugPrint('MQTT Stream Error: $error'); - }, - cancelOnError: false, - ); - } - - Future toggleLed() async { - if (client == null || _status != MqttStatus.connected) return; - - if (isTimeRestricted()) { - await _apiService.saveLog( - 'SECURITY_POLICY', - 'VIOLATION: Attempted to control LED' - 'at restricted time (${DateTime.now().hour}:00)', - ); - debugPrint('Action blocked by time policy'); - return; - } - - try { - _isLedOn = !_isLedOn; - final builder = MqttClientPayloadBuilder(); - builder.addString(_isLedOn ? 'ON' : 'OFF'); - client! - .publishMessage('commands/led', MqttQos.atMostOnce, builder.payload!); - notifyListeners(); - } catch (e) { - debugPrint('MQTT Publish Error: $e'); - } - } - - void disconnect() { - try { - client?.disconnect(); - } catch (e) { - debugPrint('MQTT Disconnect Error: $e'); - } - _status = MqttStatus.disconnected; - notifyListeners(); - } -} diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart index ebb882e..15f86a6 100644 --- a/lib/screens/auth/login_screen.dart +++ b/lib/screens/auth/login_screen.dart @@ -1,61 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/providers/auth_provider.dart'; -import 'package:mobile_flutter_iot/services/connectivity_service.dart'; -import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; -import 'package:mobile_flutter_iot/widgets/glass_card.dart'; -import 'package:mobile_flutter_iot/widgets/glass_input.dart'; -import 'package:mobile_flutter_iot/widgets/primary_button.dart'; -import 'package:provider/provider.dart'; - -class LoginScreen extends StatefulWidget { +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/auth_cubit.dart'; +import 'package:mobile_flutter_iot/cubits/flashlight_cubit.dart'; +import 'package:mobile_flutter_iot/utils/login_form.dart'; +import 'package:mobile_flutter_iot/widgets/profile/override_background.dart'; +import 'package:mobile_flutter_iot/widgets/profile/warning_dialog.dart'; + +class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); - @override - State createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State { - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - final _connectivity = ConnectivityService(); - bool _isLoading = false; - - 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 bool isOnline = await _connectivity.hasConnection(); - if (!mounted) return; - - if (!isOnline) { - _showStatusMessage('No Internet! Access denied.', isError: true); - return; - } - - setState(() => _isLoading = true); - - final isValid = await context.read().login(email, password); - if (!mounted) return; - - setState(() => _isLoading = false); - - if (isValid) { - _showStatusMessage('Access Granted. Welcome back!'); - Navigator.pushReplacementNamed(context, '/main'); - } else { - _showStatusMessage( - 'Invalid email or password (API Error)', - isError: true, - ); - } - } - - void _showStatusMessage(String message, {bool isError = false}) { + void _showStatusMessage( + BuildContext context, + String message, { + bool isError = false, + }) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), @@ -66,110 +24,225 @@ class _LoginScreenState extends State { ); } - @override - void dispose() { - _emailController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - BlurBlob( - alignment: Alignment.topRight, - translation: const Offset(0.2, -0.3), - 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.08), - size: 300, - ), - SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - children: [ - _buildLogo(), - const SizedBox(height: 30), - const Text( - 'SMART WORKSPACE', - style: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - letterSpacing: 3, - ), - ), - const Text( - 'IOT NODE TERMINAL v2.0', - style: TextStyle(color: Colors.white30, fontSize: 12), - ), - const SizedBox(height: 40), - _buildForm(), - ], - ), - ), + return BlocProvider( + create: (context) => FlashlightCubit(), + child: Scaffold( + body: MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) { + if (state is AuthAuthenticated) { + _showStatusMessage(context, 'Access Granted. Welcome back!'); + Navigator.pushReplacementNamed(context, '/main'); + } else if (state is AuthError) { + _showStatusMessage(context, state.message, isError: true); + } + }, ), + BlocListener( + listener: (context, state) { + if (state.errorMessage == 'UNSUPPORTED_PLATFORM') { + PlatformWarningDialog.show(context); + } else if (state.errorMessage == null) { + _showStatusMessage( + context, + state.isOn + ? '🔦 SYSTEM OVERRIDE: ACTIVE' + : '✅ SYSTEM SECURE: NORMAL', + isError: state.isOn, + ); + } + }, + ), + ], + child: BlocBuilder( + builder: (context, authState) { + final isLoading = authState is AuthLoading; + + return BlocBuilder( + builder: (context, flashState) { + return Stack( + children: [ + OverrideBackground(isOverrideActive: flashState.isOn), + SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + GestureDetector( + onTap: () => context + .read() + .handleSecretTap(), + child: _buildLogo(flashState.isOn), + ), + const SizedBox(height: 30), + Text( + flashState.isOn + ? 'EMERGENCY MODE' + : 'SMART WORKSPACE', + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + letterSpacing: 3, + color: flashState.isOn + ? Colors.redAccent + : Colors.white, + ), + ), + const Text( + 'IOT NODE TERMINAL v2.0', + style: TextStyle( + color: Colors.white30, + fontSize: 12, + ), + ), + const SizedBox(height: 40), + LoginForm(isLoading: isLoading), + ], + ), + ), + ), + ), + ], + ); + }, + ); + }, ), - ], + ), ), ); } - Widget _buildLogo() { + Widget _buildLogo(bool isOverrideActive) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: const Color(0xFF38BDF8).withValues(alpha: 0.3), + color: isOverrideActive + ? Colors.redAccent.withValues(alpha: 0.5) + : const Color(0xFF38BDF8).withValues(alpha: 0.3), width: 2, ), ), - child: const Icon(Icons.hub_outlined, size: 60, color: Color(0xFF38BDF8)), - ); - } - - Widget _buildForm() { - return GlassCard( - child: Column( - children: [ - GlassInput( - hintText: 'System ID / Email', - icon: Icons.alternate_email, - controller: _emailController, - ), - const SizedBox(height: 16), - GlassInput( - hintText: 'Password', - icon: Icons.fingerprint, - isPassword: true, - controller: _passwordController, - ), - const SizedBox(height: 24), - if (_isLoading) - const CircularProgressIndicator(color: Color(0xFF38BDF8)) - else - PrimaryButton( - text: 'INITIALIZE LOGIN', - onPressed: _handleLogin, - ), - const SizedBox(height: 12), - TextButton( - onPressed: () => Navigator.pushNamed(context, '/register'), - child: const Text( - "Don't have an account? Register", - style: TextStyle(color: Colors.white38, fontSize: 12), - ), - ), - ], + child: Icon( + Icons.hub_outlined, + size: 60, + color: isOverrideActive ? Colors.redAccent : const Color(0xFF38BDF8), ), ); } } + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_state.dart'; +// import 'package:mobile_flutter_iot/utils/login_form.dart'; +// import 'package:mobile_flutter_iot/widgets/common/blur_blob.dart'; + +// class LoginScreen extends StatelessWidget { +// const LoginScreen({super.key}); + +// void _showStatusMessage( +// BuildContext context, +// String message, { +// bool isError = false, +// }) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text(message), +// backgroundColor: isError ? Colors.redAccent : const Color(0xFF4ADE80), +// duration: const Duration(seconds: 2), +// behavior: SnackBarBehavior.floating, +// ), +// ); +// } + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// body: BlocConsumer( +// listener: (context, state) { +// if (state is AuthAuthenticated) { +// _showStatusMessage(context, 'Access Granted. Welcome back!'); +// Navigator.pushReplacementNamed(context, '/main'); +// } else if (state is AuthError) { +// _showStatusMessage(context, state.message, isError: true); +// } +// }, +// builder: (context, state) { +// final isLoading = state is AuthLoading; + +// return Stack( +// children: [ +// BlurBlob( +// alignment: Alignment.topRight, +// translation: const Offset(0.2, -0.3), +// 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.08), +// size: 300, +// ), +// SafeArea( +// child: Center( +// child: SingleChildScrollView( +// padding: const EdgeInsets.symmetric(horizontal: 24), +// child: Column( +// children: [ +// _buildLogo(), +// const SizedBox(height: 30), +// const Text( +// 'SMART WORKSPACE', +// style: TextStyle( +// fontSize: 26, +// fontWeight: FontWeight.bold, +// letterSpacing: 3, +// ), +// ), +// const Text( +// 'IOT NODE TERMINAL v2.0', +// style: TextStyle(color: Colors.white30, fontSize: 12), +// ), +// const SizedBox(height: 40), +// LoginForm(isLoading: isLoading), +// ], +// ), +// ), +// ), +// ), +// ], +// ); +// }, +// ), +// ); +// } + +// Widget _buildLogo() { +// return Container( +// padding: const EdgeInsets.all(20), +// decoration: BoxDecoration( +// shape: BoxShape.circle, +// border: Border.all( +// color: const Color(0xFF38BDF8).withValues(alpha: 0.3), +// width: 2, +// ), +// ), +// child: const Icon(Icons.hub_outlined, size: 60, color: Color(0xFF38BDF8)), +// ); +// } +// } diff --git a/lib/screens/auth/register_screen.dart b/lib/screens/auth/register_screen.dart index c74f1c8..03bf922 100644 --- a/lib/screens/auth/register_screen.dart +++ b/lib/screens/auth/register_screen.dart @@ -1,105 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/models/user_model.dart'; -import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; -import 'package:mobile_flutter_iot/services/api_service.dart'; -import 'package:mobile_flutter_iot/services/connectivity_service.dart'; -import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; -import 'package:mobile_flutter_iot/widgets/glass_card.dart'; -import 'package:mobile_flutter_iot/widgets/glass_input.dart'; -import 'package:mobile_flutter_iot/widgets/primary_button.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/auth_cubit.dart'; +import 'package:mobile_flutter_iot/utils/register_form.dart'; +import 'package:mobile_flutter_iot/widgets/common/blur_blob.dart'; -class RegisterScreen extends StatefulWidget { +class RegisterScreen extends StatelessWidget { 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 _connectivity = ConnectivityService(); - final _apiService = ApiService(); // Підключаємо API - final _userRepository = LocalUserRepository(); - - String? _nameError, _emailError, _deptError, _passwordError; - bool _isLoading = false; - - void _handleRegister() async { - final bool isOnline = await _connectivity.hasConnection(); - - if (!mounted) return; - - if (!isOnline) { - _showStatusMessage( - 'No Internet connection to sync account', - isError: true, - ); - return; - } - - setState(() { - _nameError = _emailError = _deptError = _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 = 'Required'); - hasError = true; - } - if (email.isEmpty || !email.contains('@')) { - setState(() => _emailError = 'Invalid email'); - hasError = true; - } - if (dept.isEmpty) { - setState(() => _deptError = 'Required'); - hasError = true; - } - if (password.length < 6) { - setState(() => _passwordError = 'Min 6 chars'); - hasError = true; - } - - if (hasError) return; - - setState(() => _isLoading = true); - - final newUser = UserModel( - fullName: name, - email: email, - password: password, - department: dept, - ); - - final success = await _apiService.register(newUser); - - if (!mounted) return; - setState(() => _isLoading = false); - - if (success) { - await _userRepository.saveUser(newUser); - - if (!mounted) return; - - _showStatusMessage('Access Key Created in Cloud! You can now log in.'); - Navigator.pop(context); - } else { - _showStatusMessage( - 'Server error or email already exists.', - isError: true, - ); - } - } - - void _showStatusMessage(String message, {bool isError = false}) { + void _showStatusMessage( + BuildContext context, + String message, { + bool isError = false, + }) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), @@ -109,64 +21,72 @@ class _RegisterScreenState extends State { ); } - @override - void dispose() { - _nameController.dispose(); - _emailController.dispose(); - _deptController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return Scaffold( - body: Stack( - children: [ - BlurBlob( - alignment: Alignment.topLeft, - translation: const Offset(-0.2, -0.3), - color: const Color(0xFF4ADE80).withValues(alpha: 0.08), - size: 280, - ), - BlurBlob( - alignment: Alignment.bottomRight, - 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( - children: [ - const SizedBox(height: 20), - _buildTopIcon(), - const SizedBox(height: 30), - const Text( - 'CREATE ACCESS KEY', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - letterSpacing: 2, - ), - ), - const SizedBox(height: 8), - const Text( - 'Register in the IoT System', - style: TextStyle(color: Colors.white38, fontSize: 13), + body: BlocConsumer( + listener: (context, state) { + if (state is AuthRegistered) { + _showStatusMessage( + context, + 'Access Key Created in Cloud! You can now log in.', + ); + Navigator.pop(context); + } else if (state is AuthError) { + _showStatusMessage(context, state.message, isError: true); + } + }, + builder: (context, state) { + final isLoading = state is AuthLoading; + + return Stack( + children: [ + BlurBlob( + alignment: Alignment.topLeft, + translation: const Offset(-0.2, -0.3), + color: const Color(0xFF4ADE80).withValues(alpha: 0.08), + size: 280, + ), + BlurBlob( + alignment: Alignment.bottomRight, + 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( + children: [ + const SizedBox(height: 20), + _buildTopIcon(), + const SizedBox(height: 30), + const Text( + 'CREATE ACCESS KEY', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + const SizedBox(height: 8), + const Text( + 'Register in the IoT System', + style: TextStyle(color: Colors.white38, fontSize: 13), + ), + const SizedBox(height: 40), + RegisterForm(isLoading: isLoading), + const SizedBox(height: 20), + ], ), - const SizedBox(height: 40), - _buildForm(), - const SizedBox(height: 20), - ], + ), ), ), - ), - ), - ], + ], + ); + }, ), ); } @@ -188,61 +108,124 @@ class _RegisterScreenState extends State { ), ); } - - Widget _buildForm() { - return GlassCard( - child: Column( - children: [ - GlassInput( - hintText: 'Full Name', - icon: Icons.person_outline, - controller: _nameController, - errorText: _nameError, - ), - const SizedBox(height: 16), - GlassInput( - hintText: 'Email Address', - icon: Icons.alternate_email, - controller: _emailController, - errorText: _emailError, - ), - const SizedBox(height: 16), - GlassInput( - hintText: 'Department (e.g., KSA)', - icon: Icons.business_center_outlined, - controller: _deptController, - errorText: _deptError, - ), - const SizedBox(height: 16), - GlassInput( - hintText: 'Access Password', - icon: Icons.lock_open_rounded, - isPassword: true, - controller: _passwordController, - errorText: _passwordError, - ), - const SizedBox(height: 32), - if (_isLoading) - const CircularProgressIndicator(color: Color(0xFF4ADE80)) - else - PrimaryButton( - text: 'INITIALIZE ACCOUNT', - onPressed: _handleRegister, - ), - const SizedBox(height: 16), - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text( - 'ALREADY HAVE A KEY? RETURN', - style: TextStyle( - color: Colors.white24, - fontSize: 10, - letterSpacing: 1, - ), - ), - ), - ], - ), - ); - } } + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_state.dart'; +// import 'package:mobile_flutter_iot/utils/register_form.dart'; +// import 'package:mobile_flutter_iot/widgets/common/blur_blob.dart'; + +// class RegisterScreen extends StatelessWidget { +// const RegisterScreen({super.key}); + +// void _showStatusMessage( +// BuildContext context, +// String message, { +// bool isError = false, +// }) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text(message), +// backgroundColor: isError ? Colors.redAccent : const Color(0xFF4ADE80), +// duration: const Duration(seconds: 2), +// ), +// ); +// } + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// body: BlocConsumer( +// listener: (context, state) { +// if (state is AuthAuthenticated) { +// _showStatusMessage( +// context, +// 'Access Key Created! Logging you in...', +// ); +// Navigator.pushNamedAndRemoveUntil( +// context, '/main', (route) => false); +// } else if (state is AuthError) { +// _showStatusMessage(context, state.message, isError: true); +// } +// }, +// builder: (context, state) { +// final isLoading = state is AuthLoading; + +// return Stack( +// children: [ +// BlurBlob( +// alignment: Alignment.topLeft, +// translation: const Offset(-0.2, -0.3), +// color: const Color(0xFF4ADE80).withValues(alpha: 0.08), +// size: 280, +// ), +// BlurBlob( +// alignment: Alignment.bottomRight, +// 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( +// children: [ +// const SizedBox(height: 20), +// _buildTopIcon(), +// const SizedBox(height: 30), +// const Text( +// 'CREATE ACCESS KEY', +// style: TextStyle( +// fontSize: 22, +// fontWeight: FontWeight.bold, +// letterSpacing: 2, +// ), +// ), +// const SizedBox(height: 8), +// const Text( +// 'Register in the IoT System', +// style: TextStyle(color: Colors.white38, fontSize: 13), +// ), +// const SizedBox(height: 40), +// RegisterForm(isLoading: isLoading), +// const SizedBox(height: 20), +// ], +// ), +// ), +// ), +// ), +// ], +// ); +// }, +// ), +// ); +// } + +// Widget _buildTopIcon() { +// return Container( +// padding: const EdgeInsets.all(20), +// decoration: BoxDecoration( +// shape: BoxShape.circle, +// border: Border.all( +// color: const Color(0xFF4ADE80).withValues(alpha: 0.1), +// width: 2, +// ), +// ), +// child: const Icon( +// Icons.app_registration_rounded, +// size: 50, +// color: Color(0xFF4ADE80), +// ), +// ); +// } +// } diff --git a/lib/screens/home/add_device_screen.dart b/lib/screens/home/add_device_screen.dart index bd7c60d..b28c6c2 100644 --- a/lib/screens/home/add_device_screen.dart +++ b/lib/screens/home/add_device_screen.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/add_device_cubit.dart'; import 'package:mobile_flutter_iot/models/device_model.dart'; import 'package:mobile_flutter_iot/services/api_service.dart'; -import 'package:mobile_flutter_iot/widgets/glass_input.dart'; -import 'package:mobile_flutter_iot/widgets/primary_button.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_input.dart'; +import 'package:mobile_flutter_iot/widgets/common/primary_button.dart'; +import 'package:mobile_flutter_iot/widgets/devices/color_picker.dart'; +import 'package:mobile_flutter_iot/widgets/devices/icon_picker.dart'; class AddDeviceScreen extends StatefulWidget { final DeviceModel? device; @@ -15,21 +19,16 @@ class AddDeviceScreen extends StatefulWidget { class _AddDeviceScreenState extends State { late TextEditingController _titleController; - late Color _selectedColor; - late IconData _selectedIcon; - - bool _isLoading = false; - final ApiService _apiService = ApiService(); - - final List _colors = [ - const Color(0xFF4ADE80), - const Color(0xFF38BDF8), - const Color(0xFFFACC15), - const Color(0xFFF87171), - const Color(0xFFC084FC), + + static const List _colors = [ + Color(0xFF4ADE80), + Color(0xFF38BDF8), + Color(0xFFFACC15), + Color(0xFFF87171), + Color(0xFFC084FC), ]; - final List _icons = [ + static const List _icons = [ Icons.air, Icons.ac_unit, Icons.sensors, @@ -42,146 +41,292 @@ class _AddDeviceScreenState extends State { void initState() { super.initState(); _titleController = TextEditingController(text: widget.device?.title ?? ''); - _selectedColor = widget.device?.color ?? _colors[0]; - _selectedIcon = widget.device?.icon ?? _icons[0]; } - Future _handleSave() async { - if (_titleController.text.trim().isEmpty) return; + @override + void dispose() { + _titleController.dispose(); + super.dispose(); + } - setState(() => _isLoading = true); + void _handleSave(BuildContext context, AddDeviceState state) { + if (_titleController.text.trim().isEmpty) return; final deviceToSave = DeviceModel( id: widget.device?.id ?? DateTime.now().millisecondsSinceEpoch.toString(), title: _titleController.text.trim(), value: widget.device?.value ?? '0 units', status: widget.device?.status ?? 'INITIALIZING', - icon: _selectedIcon, - color: _selectedColor, + icon: state.selectedIcon, + color: state.selectedColor, ); - bool success; - if (widget.device == null) { - success = await _apiService.addDevice(deviceToSave); - } else { - success = await _apiService.updateDevice(deviceToSave); - } - - if (!mounted) return; - setState(() => _isLoading = false); - - if (success) { - Navigator.pop(context, deviceToSave); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('API Error: Saved to local cache only.'), - backgroundColor: Colors.orange, - ), - ); - Navigator.pop( - context, - deviceToSave, - ); - } + context + .read() + .saveDevice(deviceToSave, widget.device == null); } @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), - if (_isLoading) - const Center( - child: CircularProgressIndicator(color: Color(0xFF38BDF8)), - ) - else - PrimaryButton( - text: widget.device == null ? 'CREATE DEVICE' : 'SAVE CHANGES', - onPressed: _handleSave, - ), - ], - ), + return BlocProvider( + create: (context) => AddDeviceCubit( + apiService: context.read(), + initialColor: widget.device?.color ?? _colors[0], + initialIcon: widget.device?.icon ?? _icons[0], ), - ); - } + child: Builder( + builder: (context) { + return BlocConsumer( + listener: (context, state) { + if (state.isSuccess != null) { + if (state.isSuccess == false && state.errorMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage!), + backgroundColor: Colors.orange, + ), + ); + } - 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(), - ); - } + final deviceToSave = DeviceModel( + id: widget.device?.id ?? + DateTime.now().millisecondsSinceEpoch.toString(), + title: _titleController.text.trim(), + value: widget.device?.value ?? '0 units', + status: widget.device?.status ?? 'INITIALIZING', + icon: state.selectedIcon, + color: state.selectedColor, + ); - 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(), + Navigator.pop(context, deviceToSave); + } + }, + builder: (context, state) { + 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), + ColorPicker(state: state, colors: _colors), + const SizedBox(height: 32), + const Text( + 'SELECT ICON', + style: TextStyle(color: Colors.white70), + ), + const SizedBox(height: 12), + IconPicker(state: state, icons: _icons), + const SizedBox(height: 48), + if (state.isLoading) + const Center( + child: CircularProgressIndicator( + color: Color(0xFF38BDF8), + ), + ) + else + PrimaryButton( + text: widget.device == null + ? 'CREATE DEVICE' + : 'SAVE CHANGES', + onPressed: () => _handleSave(context, state), + ), + ], + ), + ), + ); + }, + ); + }, + ), ); } } + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/add_device/add_device_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/add_device/add_device_event.dart'; +// import 'package:mobile_flutter_iot/blocs/add_device/add_device_state.dart'; +// import 'package:mobile_flutter_iot/models/device_model.dart'; +// import 'package:mobile_flutter_iot/services/api_service.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_input.dart'; +// import 'package:mobile_flutter_iot/widgets/common/primary_button.dart'; +// import 'package:mobile_flutter_iot/widgets/devices/color_picker.dart'; +// import 'package:mobile_flutter_iot/widgets/devices/icon_picker.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; + +// static const List _colors = [ +// Color(0xFF4ADE80), +// Color(0xFF38BDF8), +// Color(0xFFFACC15), +// Color(0xFFF87171), +// Color(0xFFC084FC), +// ]; + +// static const 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 ?? ''); +// } + +// @override +// void dispose() { +// _titleController.dispose(); +// super.dispose(); +// } + +// void _handleSave(BuildContext context, AddDeviceState state) { +// if (_titleController.text.trim().isEmpty) return; + +// final deviceToSave = DeviceModel( +// id: widget.device?.id ?? DateTime.now().millisecondsSinceEpoch.toString(), +// title: _titleController.text.trim(), +// value: widget.device?.value ?? '0 units', +// status: widget.device?.status ?? 'INITIALIZING', +// icon: state.selectedIcon, +// color: state.selectedColor, +// ); + +// context.read().add( +// AddDeviceSaveRequested( +// device: deviceToSave, +// isNew: widget.device == null, +// ), +// ); +// } + +// @override +// Widget build(BuildContext context) { +// return BlocProvider( +// create: (context) => AddDeviceBloc( +// apiService: context.read(), +// initialColor: widget.device?.color ?? _colors[0], +// initialIcon: widget.device?.icon ?? _icons[0], +// ), +// child: Builder( +// builder: (context) { +// return BlocConsumer( +// listener: (context, state) { +// if (state.isSuccess != null) { +// if (state.isSuccess == false && state.errorMessage != null) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text(state.errorMessage!), +// backgroundColor: Colors.orange, +// ), +// ); +// } + +// final deviceToSave = DeviceModel( +// id: widget.device?.id ?? +// DateTime.now().millisecondsSinceEpoch.toString(), +// title: _titleController.text.trim(), +// value: widget.device?.value ?? '0 units', +// status: widget.device?.status ?? 'INITIALIZING', +// icon: state.selectedIcon, +// color: state.selectedColor, +// ); + +// Navigator.pop(context, deviceToSave); +// } +// }, +// builder: (context, state) { +// 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), +// ColorPicker(state: state, colors: _colors), +// const SizedBox(height: 32), +// const Text( +// 'SELECT ICON', +// style: TextStyle(color: Colors.white70), +// ), +// const SizedBox(height: 12), +// IconPicker(state: state, icons: _icons), +// const SizedBox(height: 48), +// if (state.isLoading) +// const Center( +// child: CircularProgressIndicator( +// color: Color(0xFF38BDF8), +// ), +// ) +// else +// PrimaryButton( +// text: widget.device == null +// ? 'CREATE DEVICE' +// : 'SAVE CHANGES', +// onPressed: () => _handleSave(context, state), +// ), +// ], +// ), +// ), +// ); +// }, +// ); +// }, +// ), +// ); +// } +// } diff --git a/lib/screens/home/alerts_screen.dart b/lib/screens/home/alerts_screen.dart index 895f32f..d65bbc3 100644 --- a/lib/screens/home/alerts_screen.dart +++ b/lib/screens/home/alerts_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/widgets/alert_card.dart'; +import 'package:mobile_flutter_iot/widgets/devices/alert_card.dart'; class AlertsScreen extends StatelessWidget { const AlertsScreen({super.key}); diff --git a/lib/screens/home/control_screen.dart b/lib/screens/home/control_screen.dart index c097d75..f04764d 100644 --- a/lib/screens/home/control_screen.dart +++ b/lib/screens/home/control_screen.dart @@ -1,138 +1,143 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; -import 'package:mobile_flutter_iot/widgets/control_title.dart'; -import 'package:mobile_flutter_iot/widgets/glass_card.dart'; -import 'package:mobile_flutter_iot/widgets/primary_button.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/control_cubit.dart'; +import 'package:mobile_flutter_iot/widgets/common/blur_blob.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +import 'package:mobile_flutter_iot/widgets/common/primary_button.dart'; +import 'package:mobile_flutter_iot/widgets/devices/control_title.dart'; -class ControlScreen extends StatefulWidget { +class ControlScreen extends StatelessWidget { const ControlScreen({super.key}); - @override - State createState() => _ControlScreenState(); -} - -class _ControlScreenState extends State { - bool _isLightOn = true; - double _brightness = 0.7; - bool _isAcOn = true; - bool _isVentOn = false; - - void _emergencyShutdown() { - setState(() { - _isLightOn = false; - _isAcOn = false; - _isVentOn = false; - _brightness = 0.0; - }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('⚠️ EMERGENCY SHUTDOWN ACTIVATED'), - backgroundColor: Colors.redAccent, - ), - ); - } - @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.transparent, - body: Stack( - children: [ - BlurBlob( - alignment: Alignment.bottomCenter, - translation: const Offset(0, 0.5), - color: const Color(0xFF38BDF8).withValues(alpha: 0.1), - size: 250, - ), - SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + return BlocProvider( + create: (context) => ControlCubit(), + child: Builder( + builder: (context) { + return BlocConsumer( + listener: (context, state) { + if (state.isEmergencyTriggered) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('⚠️ EMERGENCY SHUTDOWN ACTIVATED'), + backgroundColor: Colors.redAccent, + ), + ); + } + }, + builder: (context, state) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Stack( children: [ - const Center( - child: Text( - 'MANUAL CONTROL', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - letterSpacing: 2, - ), - ), - ), - const SizedBox(height: 40), - const Text( - 'Lighting System', - style: TextStyle(color: Colors.white70), - ), - const SizedBox(height: 12), - _buildLightingCard(), - const SizedBox(height: 32), - const Text( - 'Climate Control', - style: TextStyle(color: Colors.white70), + BlurBlob( + alignment: Alignment.bottomCenter, + translation: const Offset(0, 0.5), + color: const Color(0xFF38BDF8).withValues(alpha: 0.1), + size: 250, ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: ControlTile( - title: 'AC Unit', - icon: Icons.ac_unit, - isOn: _isAcOn, - onTap: () => setState(() => _isAcOn = !_isAcOn), - ), - ), - const SizedBox(width: 16), - Expanded( - child: ControlTile( - title: 'Ventilation', - icon: Icons.air, - isOn: _isVentOn, - onTap: () => setState(() => _isVentOn = !_isVentOn), + SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Center( + child: Text( + 'MANUAL CONTROL', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + ), + const SizedBox(height: 40), + const Text( + 'Lighting System', + style: TextStyle(color: Colors.white70), + ), + const SizedBox(height: 12), + _buildLightingCard(context, state), + const SizedBox(height: 32), + const Text( + 'Climate Control', + style: TextStyle(color: Colors.white70), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ControlTile( + title: 'AC Unit', + icon: Icons.ac_unit, + isOn: state.isAcOn, + onTap: () => context + .read() + .toggleAc(!state.isAcOn), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ControlTile( + title: 'Ventilation', + icon: Icons.air, + isOn: state.isVentOn, + onTap: () => context + .read() + .toggleVent(!state.isVentOn), + ), + ), + ], + ), + const SizedBox(height: 48), + PrimaryButton( + text: 'EMERGENCY SHUTDOWN', + onPressed: () => context + .read() + .emergencyShutdown(), + ), + ], ), ), - ], - ), - const SizedBox(height: 48), - PrimaryButton( - text: 'EMERGENCY SHUTDOWN', - onPressed: _emergencyShutdown, + ), ), ], ), - ), - ), - ), - ], + ); + }, + ); + }, ), ); } - Widget _buildLightingCard() { + Widget _buildLightingCard(BuildContext context, ControlState state) { return GlassCard( child: Column( children: [ ListTile( contentPadding: EdgeInsets.zero, leading: Icon( - _isLightOn ? Icons.light_mode : Icons.light_mode_outlined, - color: _isLightOn ? Colors.amber : Colors.white24, + state.isLightOn ? Icons.light_mode : Icons.light_mode_outlined, + color: state.isLightOn ? Colors.amber : Colors.white24, ), title: const Text('Main Office Light'), trailing: Switch( - value: _isLightOn, + value: state.isLightOn, activeThumbColor: const Color(0xFF38BDF8), - onChanged: (v) => setState(() => _isLightOn = v), + onChanged: (v) => context.read().toggleLight(v), ), ), Slider( - value: _brightness, + value: state.brightness, activeColor: const Color(0xFF38BDF8), - onChanged: - _isLightOn ? (v) => setState(() => _brightness = v) : null, + onChanged: state.isLightOn + ? (v) => context.read().setBrightness(v) + : null, ), ], ), diff --git a/lib/screens/home/dashboard_screen.dart b/lib/screens/home/dashboard_screen.dart index cff7497..a4aa339 100644 --- a/lib/screens/home/dashboard_screen.dart +++ b/lib/screens/home/dashboard_screen.dart @@ -1,158 +1,113 @@ -import 'dart:async'; import 'dart:math'; - -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/device_cubit.dart'; +import 'package:mobile_flutter_iot/cubits/mqtt_cubit.dart'; import 'package:mobile_flutter_iot/models/device_model.dart'; -import 'package:mobile_flutter_iot/providers/mqtt_provider.dart'; -import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; import 'package:mobile_flutter_iot/screens/home/add_device_screen.dart'; -import 'package:mobile_flutter_iot/widgets/api_device_list.dart'; -import 'package:mobile_flutter_iot/widgets/mqtt_section.dart'; -import 'package:provider/provider.dart'; -import 'package:shake/shake.dart'; +import 'package:mobile_flutter_iot/widgets/devices/api_device_list.dart'; +import 'package:mobile_flutter_iot/widgets/mqtt/mqtt_section.dart'; import 'package:shared_preferences/shared_preferences.dart'; -class DashboardScreen extends StatefulWidget { +class DashboardScreen extends StatelessWidget { const DashboardScreen({super.key}); - @override - State createState() => _DashboardScreenState(); -} - -class _DashboardScreenState extends State { - StreamSubscription>? _connectivitySubscription; - late ShakeDetector _shakeDetector; - bool _isFirstCheck = true; - int _listKey = 0; - - @override - void initState() { - super.initState(); - _initConnectivityMonitoring(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _safelyConnectMQTT(); - }); - - _shakeDetector = ShakeDetector.autoStart( - onPhoneShake: (_) => _handleShake(), - shakeThresholdGravity: 1.5, - ); - } - - void _initConnectivityMonitoring() { - _connectivitySubscription = Connectivity().onConnectivityChanged.listen( - (List results) { - if (!mounted) return; - - final bool hasNet = !results.contains(ConnectivityResult.none); - final mqtt = Provider.of(context, listen: false); - - if (!hasNet) { - _isFirstCheck = false; - _showBanner('OFFLINE: Check your Wi-Fi connection', Colors.redAccent); - mqtt.disconnect(); - } else { - if (!_isFirstCheck) { - _showBanner('ONLINE: Connection restored!', Colors.green); - setState(() => _listKey++); - } - _isFirstCheck = false; - - Future.delayed(const Duration(seconds: 2), () { - if (mounted) _safelyConnectMQTT(); - }); - } - }, - ); + Future _safelyConnectMQTT(BuildContext context) async { + final mqttCubit = context.read(); + if (mqttCubit.state.client == null) { + final prefs = await SharedPreferences.getInstance(); + final savedIp = prefs.getString('mqtt_ip') ?? '192.168.1.XXX'; + mqttCubit.initMqtt(savedIp, 'flutter_client_${Random().nextInt(100)}'); + } + mqttCubit.connect(); } - void _showBanner(String msg, Color color) { + void _showBanner(BuildContext context, String msg, Color color) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg, style: const TextStyle(color: Colors.white)), backgroundColor: color.withValues(alpha: 0.9), behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 2), ), ); } - void _safelyConnectMQTT() async { - final mqtt = Provider.of(context, listen: false); - if (mqtt.client == null) { - // Читаємо збережену IP-адресу - final prefs = await SharedPreferences.getInstance(); - final savedIp = prefs.getString('mqtt_ip') ?? '192.168.1.XXX'; - - mqtt.initMqtt(savedIp, 'flutter_client_${Random().nextInt(100)}'); - } - final connectivity = await Connectivity().checkConnectivity(); - if (!connectivity.contains(ConnectivityResult.none)) { - mqtt.connect(); - } - } - - void _handleShake() { - if (!mounted) return; - _showBanner('SHAKE: Data refreshed', const Color(0xFF38BDF8)); - setState(() => _listKey++); - } - - @override - void dispose() { - _shakeDetector.stopListening(); - _connectivitySubscription?.cancel(); - super.dispose(); - } - @override Widget build(BuildContext context) { - final mqtt = context.watch(); + WidgetsBinding.instance + .addPostFrameCallback((_) => _safelyConnectMQTT(context)); return Scaffold( backgroundColor: Colors.transparent, body: SafeArea( - child: RefreshIndicator( - onRefresh: () async => setState(() => _listKey++), - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildAppBar(), - MqttSection(mqtt: mqtt), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8), - child: Text( - 'CLOUD & LOCAL NODES', - style: TextStyle( - color: Colors.white30, - fontSize: 11, - letterSpacing: 1.5, + child: BlocConsumer( + listener: (context, state) { + if (state is DeviceLoaded && state.alertMessage != null) { + final color = + state.isError ? Colors.redAccent : const Color(0xFF4ADE80); + _showBanner(context, state.alertMessage!, color); + } + }, + builder: (context, state) { + return BlocBuilder( + builder: (context, mqttState) { + return RefreshIndicator( + onRefresh: () => context.read().loadDevices(), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAppBar(), + MqttSection(mqttState: mqttState), + const Padding( + padding: + EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Text( + 'CLOUD & LOCAL NODES', + style: TextStyle( + color: Colors.white30, + fontSize: 11, + letterSpacing: 1.5, + ), + ), + ), + if (state is DeviceLoading || state is DeviceInitial) + const Center( + child: Padding( + padding: EdgeInsets.all(40), + child: CircularProgressIndicator( + color: Color(0xFF38BDF8), + ), + ), + ) + else if (state is DeviceLoaded) + ApiDeviceList( + devices: state.devices, + mqttIp: mqttState.client?.server, + ), + const SizedBox(height: 80), + ], ), ), - ), - ApiDeviceList( - key: ValueKey(_listKey), - mqttIp: mqtt.client?.server, - ), - const SizedBox(height: 80), - ], - ), - ), + ); + }, + ); + }, ), ), floatingActionButton: FloatingActionButton( onPressed: () async { final result = await Navigator.push( context, - MaterialPageRoute(builder: (context) => const AddDeviceScreen()), + MaterialPageRoute( + builder: (context) => const AddDeviceScreen(), + ), ); - if (result != null && mounted) { - await LocalUserRepository().saveDevices([result]); - setState(() => _listKey++); + if (result != null && context.mounted) { + context.read().addOrUpdateDeviceLocally(result); } }, backgroundColor: const Color(0xFF38BDF8), @@ -173,3 +128,147 @@ class _DashboardScreenState extends State { ); } } + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'dart:math'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/device/device_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/device/device_event.dart'; +// import 'package:mobile_flutter_iot/blocs/device/device_state.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_event.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_state.dart'; +// import 'package:mobile_flutter_iot/models/device_model.dart'; +// import 'package:mobile_flutter_iot/screens/home/add_device_screen.dart'; +// import 'package:mobile_flutter_iot/widgets/devices/api_device_list.dart'; +// import 'package:mobile_flutter_iot/widgets/mqtt/mqtt_section.dart'; +// import 'package:shared_preferences/shared_preferences.dart'; + +// class DashboardScreen extends StatelessWidget { +// const DashboardScreen({super.key}); + +// Future _safelyConnectMQTT(BuildContext context) async { +// final mqttBloc = context.read(); // ЗМІНЕНО +// if (mqttBloc.state.client == null) { +// final prefs = await SharedPreferences.getInstance(); +// final savedIp = prefs.getString('mqtt_ip') ?? '192.168.1.XXX'; + +// mqttBloc.add(MqttInitializeRequested( +// savedIp, 'flutter_client_${Random().nextInt(100)}')); +// } +// mqttBloc.add(MqttConnectRequested()); // ЗМІНЕНО НА EVENT +// } + +// void _showBanner(BuildContext context, String msg, Color color) { +// ScaffoldMessenger.of(context).hideCurrentSnackBar(); +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text(msg, style: const TextStyle(color: Colors.white)), +// backgroundColor: color.withValues(alpha: 0.9), +// behavior: SnackBarBehavior.floating, +// duration: const Duration(seconds: 2), +// ), +// ); +// } + +// @override +// Widget build(BuildContext context) { +// WidgetsBinding.instance +// .addPostFrameCallback((_) => _safelyConnectMQTT(context)); + +// return Scaffold( +// backgroundColor: Colors.transparent, +// body: SafeArea( +// child: BlocConsumer( +// listener: (context, state) { +// if (state is DeviceError) { +// _showBanner(context, state.message, Colors.redAccent); +// } else if (state is DeviceLoaded && state.isOffline) { +// _showBanner(context, 'Offline Mode: Local Cache', Colors.orange); +// } +// }, +// builder: (context, state) { +// return BlocBuilder( +// builder: (context, mqttState) { +// return RefreshIndicator( +// onRefresh: () async => +// context.read().add(DeviceFetchRequested()), +// child: SingleChildScrollView( +// physics: const AlwaysScrollableScrollPhysics(), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// _buildAppBar(), +// MqttSection(mqttState: mqttState), +// const Padding( +// padding: +// EdgeInsets.symmetric(horizontal: 20, vertical: 8), +// child: Text( +// 'CLOUD & LOCAL NODES', +// style: TextStyle( +// color: Colors.white30, +// fontSize: 11, +// letterSpacing: 1.5, +// ), +// ), +// ), +// if (state is DeviceLoading || state is DeviceInitial) +// const Center( +// child: Padding( +// padding: EdgeInsets.all(40), +// child: CircularProgressIndicator( +// color: Color(0xFF38BDF8), +// ), +// ), +// ) +// else if (state is DeviceLoaded) +// ApiDeviceList( +// devices: state.devices, +// mqttIp: mqttState.client?.server, +// ), +// const SizedBox(height: 80), +// ], +// ), +// ), +// ); +// }, +// ); +// }, +// ), +// ), +// floatingActionButton: FloatingActionButton( +// onPressed: () async { +// final result = await Navigator.push( +// context, +// MaterialPageRoute( +// builder: (context) => const AddDeviceScreen(), +// ), +// ); +// if (result != null && context.mounted) { +// context.read().add(DeviceFetchRequested()); +// } +// }, +// 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), +// ), +// ); +// } +// } diff --git a/lib/screens/home/details_screen.dart b/lib/screens/home/details_screen.dart index 6858c13..475ef01 100644 --- a/lib/screens/home/details_screen.dart +++ b/lib/screens/home/details_screen.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/providers/mqtt_provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/details_cubit.dart'; import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; import 'package:mobile_flutter_iot/services/api_service.dart'; -import 'package:mobile_flutter_iot/widgets/glass_card.dart'; -import 'package:mobile_flutter_iot/widgets/sensor_chart.dart'; -import 'package:provider/provider.dart'; +import 'package:mobile_flutter_iot/utils/details_dialogs.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +import 'package:mobile_flutter_iot/widgets/devices/mini_stat_card.dart'; +import 'package:mobile_flutter_iot/widgets/devices/sensor_chart.dart'; class SensorArguments { final String id; @@ -26,403 +28,464 @@ class SensorArguments { }); } -class DetailsScreen extends StatefulWidget { +class DetailsScreen extends StatelessWidget { const DetailsScreen({super.key}); - @override - State createState() => _DetailsScreenState(); -} - -class _DetailsScreenState extends State { - bool _isManualControlOn = true; - String? _currentValue; - String? _customIp; - - final _userRepository = LocalUserRepository(); - final _apiService = ApiService(); - bool _isSavingSnapshot = false; - - Future _saveSnapshot(SensorArguments args) async { - setState(() => _isSavingSnapshot = true); - - final valueToSave = _currentValue ?? args.value; - final success = await _apiService.saveLog(args.id, valueToSave); - - if (!mounted) return; - - setState(() => _isSavingSnapshot = false); - - if (success) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Snapshot saved to cloud database! 📸'), - backgroundColor: Color(0xFF4ADE80), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to save snapshot. Check connection.'), - backgroundColor: Colors.orange, - ), - ); - } - } - - Future _deleteDevice(String deviceId) async { - final confirm = await showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - backgroundColor: const Color(0xFF1E293B), - title: - const Text('Delete Sensor?', style: TextStyle(color: Colors.white)), - content: const Text( - 'This will permanently remove the device from the cloud.', - style: TextStyle(color: Colors.white70), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext, false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(dialogContext, true), - child: - const Text('DELETE', style: TextStyle(color: Colors.redAccent)), - ), - ], - ), - ); - - if (confirm != true || !mounted) return; - - final success = await _apiService.deleteDevice(deviceId); - - if (!mounted) return; - - if (success) { - final devices = await _userRepository.getDevices(); - devices.removeWhere((d) => d.id == deviceId); - await _userRepository.saveDevices(devices); - - if (!mounted) return; - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Device deleted from cloud!'), - backgroundColor: Colors.redAccent, - ), - ); - Navigator.pop(context); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to delete (Check connection)'), - backgroundColor: Colors.orange, - ), - ); - } - } - - Future _editIpAddress(SensorArguments args) async { - final mqtt = Provider.of(context, listen: false); - final controller = TextEditingController(text: _customIp ?? args.ipAddress); - - final newIp = await showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - backgroundColor: const Color(0xFF1E293B), - title: const Text('Edit Device IP / Broker'), - content: TextField( - controller: controller, - autofocus: true, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( - hintText: 'e.g. 192.168.1.XXX', - labelText: 'Target IP Address', - labelStyle: TextStyle(color: Colors.white38), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white24), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(dialogContext, controller.text), - child: const Text( - 'Update & Reconnect', - style: TextStyle(color: Color(0xFF4ADE80)), - ), - ), - ], - ), - ); - - if (!mounted) return; - - if (newIp != null && newIp.isNotEmpty) { - setState(() => _customIp = newIp); - mqtt.disconnect(); - mqtt.initMqtt(newIp, 'flutter_client_reconnect'); - mqtt.connect(); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Reconnecting to $newIp...'), - backgroundColor: Colors.blueGrey, - ), - ); - } - } - - Future _editValue(SensorArguments args) async { - final controller = TextEditingController(text: _currentValue ?? args.value); - - final newValue = await showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - backgroundColor: const Color(0xFF1E293B), - title: Text('Edit ${args.title} Value'), - content: TextField( - controller: controller, - autofocus: true, - style: const TextStyle(color: Colors.white), - decoration: - const InputDecoration(hintText: 'Enter new value (e.g. 25.5°C)'), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(dialogContext, controller.text), - child: - const Text('Save', style: TextStyle(color: Color(0xFF38BDF8))), - ), - ], - ), - ); - - if (!mounted) return; - - 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 _apiService.updateDevice(devices[index]); - await _userRepository.saveDevices(devices); - - if (!mounted) return; - 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(); + return const Scaffold( + body: Center(child: Text('Sensor data not found.')), + ); } final args = rawArgs; - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.transparent, - title: Text( - '${args.title.toUpperCase()} ANALYSIS', - style: const TextStyle(fontSize: 16), - ), - actions: [ - IconButton( - icon: const Icon( - Icons.delete_sweep_outlined, - color: Colors.redAccent, - ), - onPressed: () => _deleteDevice(args.id), - ), - ], + return BlocProvider( + create: (context) => DetailsCubit( + apiService: context.read(), + userRepository: context.read(), ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - GlassCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Builder( + builder: (context) { + return BlocConsumer( + listener: (context, state) { + if (state.alertMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.alertMessage!), + backgroundColor: + state.isError ? Colors.orange : const Color(0xFF4ADE80), + ), + ); + } + if (state.isDeleted) Navigator.pop(context); + }, + builder: (context, state) { + final displayValue = state.currentValue ?? args.value; + final displayIp = state.customIp ?? args.ipAddress; + + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + title: Text( + '${args.title.toUpperCase()} ANALYSIS', + style: const TextStyle(fontSize: 16), + ), + actions: [ + IconButton( + icon: const Icon( + Icons.delete_sweep_outlined, + color: Colors.redAccent, + ), + onPressed: () => + DetailsDialogs.showDeleteDevice(context, args.id), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( children: [ - Text( - '${args.title} History', - style: const TextStyle(color: Colors.white70), + GlassCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${args.title} History', + style: const TextStyle(color: Colors.white70), + ), + if (state.isSavingSnapshot) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFF38BDF8), + ), + ) + else + IconButton( + icon: const Icon( + Icons.camera_alt_outlined, + color: Color(0xFF38BDF8), + size: 20, + ), + onPressed: () => context + .read() + .saveSnapshot(args.id, displayValue), + ), + ], + ), + const SizedBox(height: 10), + const SensorChart(), + ], + ), ), - // НОВЕ: Кнопка Snapshot - if (_isSavingSnapshot) - const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Color(0xFF38BDF8), + const SizedBox(height: 20), + GestureDetector( + onTap: () => DetailsDialogs.showEditIpAddress( + context, + args, + state.customIp, + ), + child: GlassCard( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 15, ), - ) - else - IconButton( - icon: const Icon( - Icons.camera_alt_outlined, - color: Color(0xFF38BDF8), - size: 20, + child: Row( + children: [ + const Icon( + Icons.lan_outlined, + color: Colors.white38, + ), + const SizedBox(width: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'DEVICE IP / BROKER', + style: TextStyle( + fontSize: 10, + color: Colors.white38, + ), + ), + Text( + displayIp, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Spacer(), + const Icon( + Icons.edit_note, + color: Color(0xFF38BDF8), + size: 20, + ), + ], ), - tooltip: 'Save Snapshot to Database', - onPressed: () => _saveSnapshot(args), - ), - ], - ), - const SizedBox(height: 10), - const SensorChart(), - ], - ), - ), - const SizedBox(height: 20), - GestureDetector( - onTap: () => _editIpAddress(args), - child: GlassCard( - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 15), - child: Row( - children: [ - const Icon(Icons.lan_outlined, color: Colors.white38), - const SizedBox(width: 15), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'DEVICE IP / BROKER', - style: TextStyle(fontSize: 10, color: Colors.white38), ), - Text( - _customIp ?? args.ipAddress, - style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => DetailsDialogs.showEditValue( + context, + args, + state.currentValue, + ), + child: MiniStatCard( + label: 'Current Value', + value: displayValue, + icon: args.icon, + color: args.color, + isEditable: true, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: MiniStatCard( + label: 'Status', + value: args.status, + icon: Icons.check_circle_outline, + color: const Color(0xFF4ADE80), + ), + ), + ], + ), + const SizedBox(height: 20), + GlassCard( + child: ListTile( + leading: Icon( + state.isManualControlOn + ? args.icon + : Icons.power_off_outlined, + color: state.isManualControlOn + ? args.color + : Colors.white24, + ), + title: Text('Manual ${args.title} Control'), + subtitle: Text( + state.isManualControlOn + ? 'System Active' + : 'System Paused', + ), + trailing: Switch( + value: state.isManualControlOn, + onChanged: (v) => context + .read() + .toggleManualControl(v), + ), ), - ], - ), - const Spacer(), - const Icon( - Icons.edit_note, - color: Color(0xFF38BDF8), - size: 20, - ), - ], - ), - ), - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - 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', - args.status, - Icons.check_circle_outline, - const Color(0xFF4ADE80), + ), + ], ), ), - ], - ), - const SizedBox(height: 20), - GlassCard( - child: ListTile( - 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: _isManualControlOn, - onChanged: (v) => setState(() => _isManualControlOn = v), - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildMiniStat( - String label, - String value, - IconData icon, - Color color, { - bool isEditable = false, - }) { - return GlassCard( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - 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, - style: const TextStyle(fontSize: 12, color: Colors.white54), - ), - Text( - value, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ], + ); + }, + ); + }, ), ); } } -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.', - style: TextStyle(color: Colors.white54), - ), - ), - ); - } -} +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/details/details_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/details/details_event.dart'; +// import 'package:mobile_flutter_iot/blocs/details/details_state.dart'; +// import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +// import 'package:mobile_flutter_iot/services/api_service.dart'; +// import 'package:mobile_flutter_iot/utils/details_dialogs.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +// import 'package:mobile_flutter_iot/widgets/devices/mini_stat_card.dart'; +// import 'package:mobile_flutter_iot/widgets/devices/sensor_chart.dart'; + +// class SensorArguments { +// final String id; +// final String title; +// final String value; +// final IconData icon; +// final Color color; +// final String status; +// final String ipAddress; + +// SensorArguments({ +// required this.id, +// required this.title, +// required this.value, +// required this.icon, +// required this.color, +// this.status = 'Stable', +// this.ipAddress = '192.168.1.XXX', +// }); +// } + +// class DetailsScreen extends StatelessWidget { +// const DetailsScreen({super.key}); + +// @override +// Widget build(BuildContext context) { +// final Object? rawArgs = ModalRoute.of(context)?.settings.arguments; +// if (rawArgs == null || rawArgs is! SensorArguments) { +// return const Scaffold( +// body: Center(child: Text('Sensor data not found.')), +// ); +// } +// final args = rawArgs; + +// return BlocProvider( +// create: (context) => DetailsBloc( +// apiService: context.read(), +// userRepository: context.read(), +// ), +// child: Builder( +// builder: (context) { +// return BlocConsumer( +// listener: (context, state) { +// if (state.alertMessage != null) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text(state.alertMessage!), +// backgroundColor: +// state.isError ? Colors.orange : const Color(0xFF4ADE80), +// ), +// ); +// } +// if (state.isDeleted) Navigator.pop(context); +// }, +// builder: (context, state) { +// final displayValue = state.currentValue ?? args.value; +// final displayIp = state.customIp ?? args.ipAddress; + +// return Scaffold( +// appBar: AppBar( +// backgroundColor: Colors.transparent, +// title: Text( +// '${args.title.toUpperCase()} ANALYSIS', +// style: const TextStyle(fontSize: 16), +// ), +// actions: [ +// IconButton( +// icon: const Icon( +// Icons.delete_sweep_outlined, +// color: Colors.redAccent, +// ), +// onPressed: () => +// DetailsDialogs.showDeleteDevice(context, args.id), +// ), +// ], +// ), +// body: SingleChildScrollView( +// padding: const EdgeInsets.all(20), +// child: Column( +// children: [ +// GlassCard( +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Text( +// '${args.title} History', +// style: const TextStyle(color: Colors.white70), +// ), +// if (state.isSavingSnapshot) +// const SizedBox( +// width: 20, +// height: 20, +// child: CircularProgressIndicator( +// strokeWidth: 2, +// color: Color(0xFF38BDF8), +// ), +// ) +// else +// IconButton( +// icon: const Icon( +// Icons.camera_alt_outlined, +// color: Color(0xFF38BDF8), +// size: 20, +// ), +// onPressed: () => context +// .read() +// .add(DetailsSaveSnapshotRequested( +// args.id, displayValue)), +// ), +// ], +// ), +// const SizedBox(height: 10), +// const SensorChart(), +// ], +// ), +// ), +// const SizedBox(height: 20), +// GestureDetector( +// onTap: () => DetailsDialogs.showEditIpAddress( +// context, +// args, +// state.customIp, +// ), +// child: GlassCard( +// padding: const EdgeInsets.symmetric( +// horizontal: 20, +// vertical: 15, +// ), +// child: Row( +// children: [ +// const Icon( +// Icons.lan_outlined, +// color: Colors.white38, +// ), +// const SizedBox(width: 15), +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// const Text( +// 'DEVICE IP / BROKER', +// style: TextStyle( +// fontSize: 10, +// color: Colors.white38, +// ), +// ), +// Text( +// displayIp, +// style: const TextStyle( +// fontWeight: FontWeight.bold, +// ), +// ), +// ], +// ), +// const Spacer(), +// const Icon( +// Icons.edit_note, +// color: Color(0xFF38BDF8), +// size: 20, +// ), +// ], +// ), +// ), +// ), +// const SizedBox(height: 20), +// Row( +// children: [ +// Expanded( +// child: GestureDetector( +// onTap: () => DetailsDialogs.showEditValue( +// context, +// args, +// state.currentValue, +// ), +// child: MiniStatCard( +// label: 'Current Value', +// value: displayValue, +// icon: args.icon, +// color: args.color, +// isEditable: true, +// ), +// ), +// ), +// const SizedBox(width: 16), +// Expanded( +// child: MiniStatCard( +// label: 'Status', +// value: args.status, +// icon: Icons.check_circle_outline, +// color: const Color(0xFF4ADE80), +// ), +// ), +// ], +// ), +// const SizedBox(height: 20), +// GlassCard( +// child: ListTile( +// leading: Icon( +// state.isManualControlOn +// ? args.icon +// : Icons.power_off_outlined, +// color: state.isManualControlOn +// ? args.color +// : Colors.white24, +// ), +// title: Text('Manual ${args.title} Control'), +// subtitle: Text( +// state.isManualControlOn +// ? 'System Active' +// : 'System Paused', +// ), +// trailing: Switch( +// value: state.isManualControlOn, +// // ЗМІНЕНО НА EVENT +// onChanged: (v) => context +// .read() +// .add(DetailsToggleManualControlRequested(v)), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// }, +// ); +// }, +// ), +// ); +// } +// } diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 78f13b2..d78ac0d 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -1,84 +1,94 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/widgets/blur_blob.dart'; -import 'package:mobile_flutter_iot/widgets/fan_widget.dart'; -import 'package:mobile_flutter_iot/widgets/glass_card.dart'; -import 'package:mobile_flutter_iot/widgets/tech_node.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/home_cubit.dart'; +import 'package:mobile_flutter_iot/widgets/common/blur_blob.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +import 'package:mobile_flutter_iot/widgets/devices/fan_widget.dart'; +import 'package:mobile_flutter_iot/widgets/devices/tech_node.dart'; -class HomeScreen extends StatefulWidget { +class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); - @override - State createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State { - bool _isSystemOn = true; - - void _toggleSystemPower() { - setState(() => _isSystemOn = !_isSystemOn); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(_isSystemOn ? 'SYSTEM INITIALIZED' : 'SYSTEM SHUTDOWN'), - backgroundColor: - _isSystemOn ? const Color(0xFF38BDF8) : Colors.redAccent, - duration: const Duration(seconds: 1), - ), - ); - } - @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.transparent, - body: Stack( - children: [ - BlurBlob( - alignment: Alignment.topLeft, - translation: const Offset(-0.2, -0.3), - color: const Color(0xFF4ADE80).withValues(alpha: 0.08), - size: 280, - ), - BlurBlob( - alignment: Alignment.bottomRight, - translation: const Offset(0.3, 0.2), - color: const Color(0xFF38BDF8).withValues(alpha: 0.1), - size: 320, - ), - SafeArea( - child: Column( - children: [ - const SizedBox(height: 20), - const Text( - 'SYSTEM SCHEMATIC', - style: TextStyle( - letterSpacing: 4, - fontWeight: FontWeight.bold, - color: Colors.white70, + return BlocProvider( + create: (context) => HomeCubit(), + child: Builder( + builder: (context) { + return BlocConsumer( + listener: (context, state) { + if (state.alertMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.alertMessage!), + backgroundColor: state.isSystemOn + ? const Color(0xFF38BDF8) + : Colors.redAccent, + duration: const Duration(seconds: 1), ), - ), - Expanded( - child: Center( - child: GlassCard( - width: 320, - height: 500, - child: Stack( - alignment: Alignment.center, + ); + } + }, + builder: (context, state) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Stack( + children: [ + BlurBlob( + alignment: Alignment.topLeft, + translation: const Offset(-0.2, -0.3), + color: const Color(0xFF4ADE80).withValues(alpha: 0.08), + size: 280, + ), + BlurBlob( + alignment: Alignment.bottomRight, + translation: const Offset(0.3, 0.2), + color: const Color(0xFF38BDF8).withValues(alpha: 0.1), + size: 320, + ), + SafeArea( + child: Column( children: [ - _buildConnectionLine(), - _buildVentilationUnit(), - _buildIlluminationUnit(), - _buildPowerButton(), + const SizedBox(height: 20), + const Text( + 'SYSTEM SCHEMATIC', + style: TextStyle( + letterSpacing: 4, + fontWeight: FontWeight.bold, + color: Colors.white70, + ), + ), + Expanded( + child: Center( + child: GlassCard( + width: 320, + height: 500, + child: Stack( + alignment: Alignment.center, + children: [ + _buildConnectionLine(), + _buildVentilationUnit(state.isSystemOn), + _buildIlluminationUnit(state.isSystemOn), + _buildPowerButton( + context, + state.isSystemOn, + ), + ], + ), + ), + ), + ), + _buildStatusText(state.isSystemOn), + const SizedBox(height: 20), ], ), ), - ), + ], ), - _buildStatusText(), - const SizedBox(height: 20), - ], - ), - ), - ], + ); + }, + ); + }, ), ); } @@ -91,49 +101,48 @@ class _HomeScreenState extends State { ); } - Widget _buildVentilationUnit() { + Widget _buildVentilationUnit(bool isSystemOn) { return Positioned( top: 40, child: TechNode( label: 'VENTILATION UNIT', - accentColor: _isSystemOn ? const Color(0xFF38BDF8) : Colors.transparent, - child: _isSystemOn + accentColor: isSystemOn ? const Color(0xFF38BDF8) : Colors.transparent, + child: isSystemOn ? const RotatingFan(size: 100) : const Icon(Icons.cyclone, size: 100, color: Colors.white10), ), ); } - Widget _buildIlluminationUnit() { + Widget _buildIlluminationUnit(bool isSystemOn) { return Positioned( bottom: 60, child: TechNode( label: 'ILLUMINATION', - accentColor: _isSystemOn ? Colors.yellow : Colors.transparent, + accentColor: isSystemOn ? Colors.yellow : Colors.transparent, child: Icon( Icons.lightbulb, size: 70, - color: _isSystemOn ? Colors.yellow : Colors.white10, + color: isSystemOn ? Colors.yellow : Colors.white10, ), ), ); } - Widget _buildPowerButton() { + Widget _buildPowerButton(BuildContext context, bool isSystemOn) { return Positioned( bottom: 20, right: 20, child: GestureDetector( - onTap: _toggleSystemPower, + onTap: () => context.read().toggleSystemPower(), child: AnimatedContainer( duration: const Duration(milliseconds: 300), padding: const EdgeInsets.all(8), decoration: BoxDecoration( shape: BoxShape.circle, - color: _isSystemOn - ? Colors.red.withValues(alpha: 0.1) - : Colors.white10, - boxShadow: _isSystemOn + color: + isSystemOn ? Colors.red.withValues(alpha: 0.1) : Colors.white10, + boxShadow: isSystemOn ? [ BoxShadow( color: Colors.red.withValues(alpha: 0.2), @@ -144,7 +153,7 @@ class _HomeScreenState extends State { ), child: Icon( Icons.power_settings_new, - color: _isSystemOn ? Colors.redAccent : Colors.white38, + color: isSystemOn ? Colors.redAccent : Colors.white38, size: 28, ), ), @@ -152,14 +161,256 @@ class _HomeScreenState extends State { ); } - Widget _buildStatusText() { + Widget _buildStatusText(bool isSystemOn) { return Text( - _isSystemOn ? 'LIVE SYSTEM PREVIEW' : 'SYSTEM OFFLINE', + isSystemOn ? 'LIVE SYSTEM PREVIEW' : 'SYSTEM OFFLINE', style: TextStyle( letterSpacing: 2, - color: _isSystemOn ? Colors.white24 : Colors.red.withValues(alpha: 0.3), + color: isSystemOn ? Colors.white24 : Colors.red.withValues(alpha: 0.3), fontSize: 10, ), ); } } + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:iot_flashlight/iot_flashlight.dart'; // ІМПОРТ НАШОГО ПЛАГІНУ +// import 'package:mobile_flutter_iot/cubits/home_cubit.dart'; +// import 'package:mobile_flutter_iot/widgets/common/blur_blob.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +// import 'package:mobile_flutter_iot/widgets/devices/fan_widget.dart'; +// import 'package:mobile_flutter_iot/widgets/devices/tech_node.dart'; + +// class HomeScreen extends StatelessWidget { +// const HomeScreen({super.key}); + +// @override +// Widget build(BuildContext context) { +// return BlocProvider( +// create: (context) => HomeCubit(), +// child: Builder( +// builder: (context) { +// return BlocConsumer( +// listener: (context, state) { +// if (state.alertMessage != null) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text(state.alertMessage!), +// backgroundColor: state.isSystemOn +// ? const Color(0xFF38BDF8) +// : Colors.redAccent, +// duration: const Duration(seconds: 1), +// ), +// ); +// } +// }, +// builder: (context, state) { +// return Scaffold( +// backgroundColor: Colors.transparent, +// body: Stack( +// children: [ +// BlurBlob( +// alignment: Alignment.topLeft, +// translation: const Offset(-0.2, -0.3), +// color: const Color(0xFF4ADE80).withValues(alpha: 0.08), +// size: 280, +// ), +// BlurBlob( +// alignment: Alignment.bottomRight, +// translation: const Offset(0.3, 0.2), +// color: const Color(0xFF38BDF8).withValues(alpha: 0.1), +// size: 320, +// ), +// SafeArea( +// child: Column( +// children: [ +// const SizedBox(height: 20), +// const Text( +// 'SYSTEM SCHEMATIC', +// style: TextStyle( +// letterSpacing: 4, +// fontWeight: FontWeight.bold, +// color: Colors.white70, +// ), +// ), +// Expanded( +// child: Center( +// child: GlassCard( +// width: 320, +// height: 500, +// child: Stack( +// alignment: Alignment.center, +// children: [ +// _buildConnectionLine(), +// _buildVentilationUnit(state.isSystemOn), +// // ПЕРЕДАЄМО CONTEXT ДЛЯ ДІАЛОГУ ПОМИЛКИ +// _buildIlluminationUnit( +// context, state.isSystemOn), +// _buildPowerButton( +// context, +// state.isSystemOn, +// ), +// ], +// ), +// ), +// ), +// ), +// _buildStatusText(state.isSystemOn), +// const SizedBox(height: 20), +// ], +// ), +// ), +// ], +// ), +// ); +// }, +// ); +// }, +// ), +// ); +// } + +// Widget _buildConnectionLine() { +// return Positioned( +// top: 100, +// bottom: 100, +// child: Container(width: 1, color: Colors.white10), +// ); +// } + +// Widget _buildVentilationUnit(bool isSystemOn) { +// return Positioned( +// top: 40, +// child: TechNode( +// label: 'VENTILATION UNIT', +// accentColor: isSystemOn ? const Color(0xFF38BDF8) : Colors.transparent, +// child: isSystemOn +// ? const RotatingFan(size: 100) +// : const Icon(Icons.cyclone, size: 100, color: Colors.white10), +// ), +// ); +// } + +// // ОНОВЛЕНИЙ ВУЗОЛ ОСВІТЛЕННЯ З ПАСХАЛКОЮ +// Widget _buildIlluminationUnit(BuildContext context, bool isSystemOn) { +// return Positioned( +// bottom: 60, +// child: GestureDetector( +// // СЕКРЕТНИЙ ФУНКЦІОНАЛ: Подвійний тап вмикає реальний спалах +// onDoubleTap: () async { +// try { +// final isOn = await IotFlashlight.toggle(); + +// if (context.mounted) { +// ScaffoldMessenger.of(context).hideCurrentSnackBar(); +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text( +// isOn +// ? '🔦 HARDWARE OVERRIDE: OPTICAL EMITTER ACTIVE' +// : '🔦 HARDWARE OVERRIDE: OPTICAL EMITTER OFFLINE', +// style: const TextStyle( +// fontWeight: FontWeight.bold, letterSpacing: 1), +// ), +// backgroundColor: +// isOn ? Colors.amber.shade700 : Colors.blueGrey, +// duration: const Duration(milliseconds: 1500), +// ), +// ); +// } +// } catch (e) { +// // ОБРОБКА iOS ТА ІНШИХ ПЛАТФОРМ +// if (e.toString().contains('UNSUPPORTED_PLATFORM') && +// context.mounted) { +// showDialog( +// context: context, +// builder: (ctx) => AlertDialog( +// backgroundColor: const Color(0xFF1E293B), +// title: const Row( +// children: [ +// Icon(Icons.warning_amber_rounded, color: Colors.orange), +// SizedBox(width: 10), +// Text('Platform Warning', +// style: TextStyle(color: Colors.white, fontSize: 16)), +// ], +// ), +// content: const Text( +// 'Hardware flashlight control is currently only supported +//on native Android nodes.', +// style: TextStyle(color: Colors.white70), +// ), +// actions: [ +// TextButton( +// onPressed: () => Navigator.pop(ctx), +// child: const Text('ACKNOWLEDGE', +// style: TextStyle(color: Color(0xFF38BDF8))), +// ), +// ], +// ), +// ); +// } +// } +// }, +// child: TechNode( +// label: 'ILLUMINATION', +// accentColor: isSystemOn ? Colors.yellow : Colors.transparent, +// child: Icon( +// Icons.lightbulb, +// size: 70, +// color: isSystemOn ? Colors.yellow : Colors.white10, +// ), +// ), +// ), +// ); +// } + +// Widget _buildPowerButton(BuildContext context, bool isSystemOn) { +// return Positioned( +// bottom: 20, +// right: 20, +// child: GestureDetector( +// onTap: () => context.read().toggleSystemPower(), +// child: AnimatedContainer( +// duration: const Duration(milliseconds: 300), +// padding: const EdgeInsets.all(8), +// decoration: BoxDecoration( +// shape: BoxShape.circle, +// color: +// isSystemOn ? Colors.red.withValues(alpha: 0.1) : Colors.white10, +// boxShadow: isSystemOn +// ? [ +// BoxShadow( +// color: Colors.red.withValues(alpha: 0.2), +// blurRadius: 10, +// ), +// ] +// : [], +// ), +// child: Icon( +// Icons.power_settings_new, +// color: isSystemOn ? Colors.redAccent : Colors.white38, +// size: 28, +// ), +// ), +// ), +// ); +// } + +// Widget _buildStatusText(bool isSystemOn) { +// return Text( +// isSystemOn ? 'LIVE SYSTEM PREVIEW' : 'SYSTEM OFFLINE', +// style: TextStyle( +// letterSpacing: 2, +// color: isSystemOn ? Colors.white24 : Colors.red.withValues(alpha: 0.3), +// fontSize: 10, +// ), +// ); +// } +// } diff --git a/lib/screens/main/main_wrapper.dart b/lib/screens/main/main_wrapper.dart index 8c44b4e..4556a7d 100644 --- a/lib/screens/main/main_wrapper.dart +++ b/lib/screens/main/main_wrapper.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/profile/profile_screen.dart'; import 'package:mobile_flutter_iot/screens/home/alerts_screen.dart'; import 'package:mobile_flutter_iot/screens/home/control_screen.dart'; import 'package:mobile_flutter_iot/screens/home/dashboard_screen.dart'; import 'package:mobile_flutter_iot/screens/home/home_screen.dart'; +import 'package:mobile_flutter_iot/screens/profile/profile_screen.dart'; class MainWrapper extends StatefulWidget { const MainWrapper({super.key}); diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart new file mode 100644 index 0000000..58d9bdd --- /dev/null +++ b/lib/screens/profile/profile_screen.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/auth_cubit.dart'; +import 'package:mobile_flutter_iot/cubits/profile_cubit.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; +import 'package:mobile_flutter_iot/widgets/profile/emergency_wrapper.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ProfileCubit( + apiService: context.read(), + userRepository: context.read(), + ), + child: Builder( + builder: (context) { + return BlocConsumer( + listener: (context, state) { + if (state.alertMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.alertMessage!), + backgroundColor: + state.isError ? Colors.orange : const Color(0xFF4ADE80), + ), + ); + } + if (state.isAccountDeleted) { + context.read().logout(); + Navigator.pushNamedAndRemoveUntil( + context, + '/login', + (route) => false, + ); + } + }, + builder: (context, state) { + final authState = context.watch().state; + final isLoggedIn = authState is AuthAuthenticated; + + return EmergencyOverrideWrapper( + profileState: state, + isLoggedIn: isLoggedIn, + ); + }, + ); + }, + ), + ); + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/cubits/auth_cubit.dart'; +// import 'package:mobile_flutter_iot/cubits/profile_cubit.dart'; +// import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +// import 'package:mobile_flutter_iot/services/api_service.dart'; +// import 'package:mobile_flutter_iot/widgets/profile/emergency_wrapper.dart'; // НОВИЙ ІМПОРТ + +// class ProfileScreen extends StatelessWidget { +// const ProfileScreen({super.key}); + +// @override +// Widget build(BuildContext context) { +// return BlocProvider( +// create: (context) => ProfileCubit( +// apiService: context.read(), +// userRepository: context.read(), +// ), +// child: Builder( +// builder: (context) { +// return BlocConsumer( +// listener: (context, state) { +// if (state.alertMessage != null) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text(state.alertMessage!), +// backgroundColor: +// state.isError ? Colors.orange : const Color(0xFF4ADE80), +// ), +// ); +// } +// if (state.isAccountDeleted) { +// context.read().logout(); +// Navigator.pushNamedAndRemoveUntil( +// context, '/login', (route) => false); +// } +// }, +// builder: (context, state) { +// final authState = context.watch().state; +// final isLoggedIn = authState is AuthAuthenticated; + +// return EmergencyOverrideWrapper( +// profileState: state, +// isLoggedIn: isLoggedIn, +// ); +// }, +// ); +// }, +// ), +// ); +// } +// } diff --git a/lib/utils/details_dialogs.dart b/lib/utils/details_dialogs.dart new file mode 100644 index 0000000..ac047b4 --- /dev/null +++ b/lib/utils/details_dialogs.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/details_cubit.dart'; +import 'package:mobile_flutter_iot/cubits/mqtt_cubit.dart'; +import 'package:mobile_flutter_iot/screens/home/details_screen.dart'; + +class DetailsDialogs { + static Future showDeleteDevice( + BuildContext context, + String deviceId, + ) async { + final confirm = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + title: + const Text('Delete Sensor?', style: TextStyle(color: Colors.white)), + content: const Text( + 'This will permanently remove the device from the cloud.', + style: TextStyle(color: Colors.white70), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext, true), + child: const Text( + 'DELETE', + style: TextStyle( + color: Colors.redAccent, + ), + ), + ), + ], + ), + ); + + if (confirm == true && context.mounted) { + context.read().deleteDevice(deviceId); + } + } + + static Future showEditIpAddress( + BuildContext context, + SensorArguments args, + String? currentCustomIp, + ) async { + final mqttCubit = context.read(); + final controller = + TextEditingController(text: currentCustomIp ?? args.ipAddress); + + final newIp = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + title: const Text('Edit Device IP / Broker'), + content: TextField( + controller: controller, + autofocus: true, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + hintText: 'e.g. 192.168.1.XXX', + labelText: 'Target IP Address', + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white24), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext, controller.text), + child: const Text( + 'Update & Reconnect', + style: TextStyle(color: Color(0xFF4ADE80)), + ), + ), + ], + ), + ); + + if (newIp != null && newIp.isNotEmpty && context.mounted) { + context.read().updateIp(newIp); + mqttCubit.disconnect(); + mqttCubit.initMqtt(newIp, 'flutter_client_reconnect'); + mqttCubit.connect(); + } + } + + static Future showEditValue( + BuildContext context, + SensorArguments args, + String? currentValue, + ) async { + final controller = TextEditingController(text: currentValue ?? args.value); + + final newValue = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + title: Text('Edit ${args.title} Value'), + content: TextField( + controller: controller, + autofocus: true, + style: const TextStyle(color: Colors.white), + decoration: + const InputDecoration(hintText: 'Enter new value (e.g. 25.5°C)'), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext, controller.text), + child: + const Text('Save', style: TextStyle(color: Color(0xFF38BDF8))), + ), + ], + ), + ); + + if (newValue != null && newValue.isNotEmpty && context.mounted) { + context.read().updateValue(args.id, newValue); + } + } +} diff --git a/lib/utils/login_form.dart b/lib/utils/login_form.dart new file mode 100644 index 0000000..a7dbb41 --- /dev/null +++ b/lib/utils/login_form.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/auth_cubit.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_input.dart'; +import 'package:mobile_flutter_iot/widgets/common/primary_button.dart'; + +class LoginForm extends StatefulWidget { + final bool isLoading; + const LoginForm({required this.isLoading, super.key}); + + @override + State createState() => _LoginFormState(); +} + +class _LoginFormState extends State { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + + void _handleLogin() { + final email = _emailController.text.trim(); + final password = _passwordController.text.trim(); + + if (email.isEmpty || password.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please fill in all fields'), + backgroundColor: Colors.redAccent, + duration: Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + return; + } + + context.read().login(email, password); + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GlassCard( + child: Column( + children: [ + GlassInput( + hintText: 'System ID / Email', + icon: Icons.alternate_email, + controller: _emailController, + ), + const SizedBox(height: 16), + GlassInput( + hintText: 'Password', + icon: Icons.fingerprint, + isPassword: true, + controller: _passwordController, + ), + const SizedBox(height: 24), + if (widget.isLoading) + const CircularProgressIndicator(color: Color(0xFF38BDF8)) + else + PrimaryButton( + text: 'INITIALIZE LOGIN', + onPressed: _handleLogin, + ), + const SizedBox(height: 12), + TextButton( + onPressed: () => Navigator.pushNamed(context, '/register'), + child: const Text( + "Don't have an account? Register", + style: TextStyle(color: Colors.white38, fontSize: 12), + ), + ), + ], + ), + ); + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_event.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_input.dart'; +// import 'package:mobile_flutter_iot/widgets/common/primary_button.dart'; + +// class LoginForm extends StatefulWidget { +// final bool isLoading; +// const LoginForm({required this.isLoading, super.key}); + +// @override +// State createState() => _LoginFormState(); +// } + +// class _LoginFormState extends State { +// final _emailController = TextEditingController(); +// final _passwordController = TextEditingController(); + +// void _handleLogin() { +// final email = _emailController.text.trim(); +// final password = _passwordController.text.trim(); + +// if (email.isEmpty || password.isEmpty) { +// ScaffoldMessenger.of(context).showSnackBar( +// const SnackBar( +// content: Text('Please fill in all fields'), +// backgroundColor: Colors.redAccent, +// duration: Duration(seconds: 2), +// behavior: SnackBarBehavior.floating, +// ), +// ); +// return; +// } + +// context.read().add( +// AuthLoginRequested(email: email, password: password), +// ); +// } + +// @override +// void dispose() { +// _emailController.dispose(); +// _passwordController.dispose(); +// super.dispose(); +// } + +// @override +// Widget build(BuildContext context) { +// return GlassCard( +// child: Column( +// children: [ +// GlassInput( +// hintText: 'System ID / Email', +// icon: Icons.alternate_email, +// controller: _emailController, +// ), +// const SizedBox(height: 16), +// GlassInput( +// hintText: 'Password', +// icon: Icons.fingerprint, +// isPassword: true, +// controller: _passwordController, +// ), +// const SizedBox(height: 24), +// if (widget.isLoading) +// const CircularProgressIndicator(color: Color(0xFF38BDF8)) +// else +// PrimaryButton( +// text: 'INITIALIZE LOGIN', +// onPressed: _handleLogin, +// ), +// const SizedBox(height: 12), +// TextButton( +// onPressed: () => Navigator.pushNamed(context, '/register'), +// child: const Text( +// "Don't have an account? Register", +// style: TextStyle(color: Colors.white38, fontSize: 12), +// ), +// ), +// ], +// ), +// ); +// } +// } diff --git a/lib/utils/mqtt_dialogs.dart b/lib/utils/mqtt_dialogs.dart new file mode 100644 index 0000000..33caf24 --- /dev/null +++ b/lib/utils/mqtt_dialogs.dart @@ -0,0 +1,303 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/cubits/mqtt_cubit.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MqttDialogs { + static Future showEditIpAddress( + BuildContext context, + MqttCubit mqttCubit, + MqttState state, + ) async { + final prefs = await SharedPreferences.getInstance(); + if (!context.mounted) return; + + final currentIp = + state.client?.server ?? prefs.getString('mqtt_ip') ?? '192.168.1.XXX'; + final controller = TextEditingController(text: currentIp); + + final newIp = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + title: + const Text('Set Broker IP', style: TextStyle(color: Colors.white)), + content: TextField( + controller: controller, + autofocus: true, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + hintText: 'e.g. 192.168.1.100', + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white24), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, controller.text.trim()), + child: const Text( + 'Save & Reconnect', + style: TextStyle(color: Color(0xFF4ADE80)), + ), + ), + ], + ), + ); + + if (newIp != null && newIp.isNotEmpty && context.mounted) { + await prefs.setString('mqtt_ip', newIp); + if (!context.mounted) return; + + mqttCubit.disconnect(); + mqttCubit.initMqtt(newIp, 'flutter_client_${Random().nextInt(100)}'); + mqttCubit.connect(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Reconnecting to $newIp...'), + backgroundColor: Colors.blueGrey, + ), + ); + } + } + + static Future showSetLockPolicy( + BuildContext context, + MqttCubit mqttCubit, + MqttState state, + ) async { + final startController = + TextEditingController(text: state.startLockHour.toString()); + final endController = + TextEditingController(text: state.endLockHour.toString()); + + await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + title: const Text( + 'Admin Security Policy', + style: TextStyle(color: Colors.white), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Set hours when LED control is disabled.' + 'Actions will be logged to the server.', + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextField( + controller: startController, + keyboardType: TextInputType.number, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + labelText: 'Lock From (Hour)', + labelStyle: TextStyle(color: Colors.white54), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextField( + controller: endController, + keyboardType: TextInputType.number, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + labelText: 'Unlock At (Hour)', + labelStyle: TextStyle(color: Colors.white54), + ), + ), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + final start = int.tryParse(startController.text) ?? 22; + final end = int.tryParse(endController.text) ?? 6; + mqttCubit.updateLockHours(start, end); + Navigator.pop(context); + }, + child: const Text( + 'Save Policy', + style: TextStyle(color: Colors.redAccent), + ), + ), + ], + ), + ); + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'dart:math'; +// import 'package:flutter/material.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_event.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_state.dart'; +// import 'package:shared_preferences/shared_preferences.dart'; + +// class MqttDialogs { +// static Future showEditIpAddress( +// BuildContext context, +// MqttBloc mqttBloc, +// MqttState state, +// ) async { +// final prefs = await SharedPreferences.getInstance(); +// if (!context.mounted) return; + +// final currentIp = +// state.client?.server ?? prefs.getString('mqtt_ip') ?? '192.168.1.XXX'; +// final controller = TextEditingController(text: currentIp); + +// final newIp = await showDialog( +// context: context, +// builder: (context) => AlertDialog( +// backgroundColor: const Color(0xFF1E293B), +// title: +// const Text('Set Broker IP', style: TextStyle(color: Colors.white)), +// content: TextField( +// controller: controller, +// autofocus: true, +// style: const TextStyle(color: Colors.white), +// decoration: const InputDecoration( +// hintText: 'e.g. 192.168.1.100', +// enabledBorder: UnderlineInputBorder( +// borderSide: BorderSide(color: Colors.white24), +// ), +// ), +// ), +// actions: [ +// TextButton( +// onPressed: () => Navigator.pop(context), +// child: const Text('Cancel'), +// ), +// TextButton( +// onPressed: () => Navigator.pop(context, controller.text.trim()), +// child: const Text( +// 'Save & Reconnect', +// style: TextStyle(color: Color(0xFF4ADE80)), +// ), +// ), +// ], +// ), +// ); + +// if (newIp != null && newIp.isNotEmpty && context.mounted) { +// await prefs.setString('mqtt_ip', newIp); +// if (!context.mounted) return; + +// mqttBloc.add(MqttDisconnectRequested()); +// mqttBloc.add(MqttInitializeRequested( +// newIp, 'flutter_client_${Random().nextInt(100)}')); +// mqttBloc.add(MqttConnectRequested()); + +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text('Reconnecting to $newIp...'), +// backgroundColor: Colors.blueGrey, +// ), +// ); +// } +// } + +// static Future showSetLockPolicy( +// BuildContext context, +// MqttBloc mqttBloc, +// MqttState state, +// ) async { +// final startController = +// TextEditingController(text: state.startLockHour.toString()); +// final endController = +// TextEditingController(text: state.endLockHour.toString()); + +// await showDialog( +// context: context, +// builder: (context) => AlertDialog( +// backgroundColor: const Color(0xFF1E293B), +// title: const Text( +// 'Admin Security Policy', +// style: TextStyle(color: Colors.white), +// ), +// content: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// const Text( +// 'Set hours when LED control is disabled.' +// 'Actions will be logged to the server.', +// style: TextStyle(color: Colors.white70, fontSize: 12), +// ), +// const SizedBox(height: 16), +// Row( +// children: [ +// Expanded( +// child: TextField( +// controller: startController, +// keyboardType: TextInputType.number, +// style: const TextStyle(color: Colors.white), +// decoration: const InputDecoration( +// labelText: 'Lock From (Hour)', +// labelStyle: TextStyle(color: Colors.white54), +// ), +// ), +// ), +// const SizedBox(width: 16), +// Expanded( +// child: TextField( +// controller: endController, +// keyboardType: TextInputType.number, +// style: const TextStyle(color: Colors.white), +// decoration: const InputDecoration( +// labelText: 'Unlock At (Hour)', +// labelStyle: TextStyle(color: Colors.white54), +// ), +// ), +// ), +// ], +// ), +// ], +// ), +// actions: [ +// TextButton( +// onPressed: () => Navigator.pop(context), +// child: const Text('Cancel'), +// ), +// TextButton( +// onPressed: () { +// final start = int.tryParse(startController.text) ?? 22; +// final end = int.tryParse(endController.text) ?? 6; +// // ВИКЛИКАЄМО ПОДІЮ BLoC +// mqttBloc.add(MqttUpdateLockHoursRequested(start, end)); +// Navigator.pop(context); +// }, +// child: const Text( +// 'Save Policy', +// style: TextStyle(color: Colors.redAccent), +// ), +// ), +// ], +// ), +// ); +// } +// } diff --git a/lib/utils/profile_dialogs.dart b/lib/utils/profile_dialogs.dart new file mode 100644 index 0000000..6a70b80 --- /dev/null +++ b/lib/utils/profile_dialogs.dart @@ -0,0 +1,263 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/auth_cubit.dart'; +import 'package:mobile_flutter_iot/cubits/profile_cubit.dart'; + +class ProfileDialogs { + static void showLogout(BuildContext context) { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: const Text( + 'System Termination', + style: TextStyle(color: Colors.white), + ), + content: const Text('Are you sure you want to logout?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () { + Navigator.pop(dialogContext); + context.read().logout(); + Navigator.pushNamedAndRemoveUntil( + context, + '/login', + (route) => false, + ); + }, + child: + const Text('LOGOUT', style: TextStyle(color: Colors.redAccent)), + ), + ], + ), + ); + } + + static void showDeleteAccount(BuildContext context) async { + final confirm = await showDialog( + context: context, + builder: (dialogContext) => 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, cloud data,' + 'and local profile. Continue?', + style: TextStyle(color: Colors.white70), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: + const Text('CANCEL', style: TextStyle(color: Colors.white38)), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFF87171).withValues(alpha: 0.2), + foregroundColor: const Color(0xFFF87171), + side: const BorderSide(color: Color(0xFFF87171)), + ), + onPressed: () => Navigator.pop(dialogContext, true), + child: const Text('CONFIRM PURGE'), + ), + ], + ), + ); + + if (confirm == true && context.mounted) { + context.read().deleteAccount(); + } + } + + static void showEditField( + BuildContext context, + String fieldTitle, + String currentValue, + ) async { + final controller = TextEditingController(text: currentValue); + final cubit = context.read(); + + final newValue = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + title: Text( + 'Update $fieldTitle', + style: const TextStyle(color: Colors.white), + ), + content: TextField( + controller: controller, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: 'Enter new $fieldTitle', + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white24), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext, controller.text), + child: + const Text('Save', style: TextStyle(color: Color(0xFF38BDF8))), + ), + ], + ), + ); + + if (newValue != null && newValue.isNotEmpty) { + cubit.updateField(fieldTitle, newValue); + } + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_event.dart'; +// import 'package:mobile_flutter_iot/blocs/profile/profile_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/profile/profile_event.dart'; + +// class ProfileDialogs { +// static void showLogout(BuildContext context) { +// showDialog( +// context: context, +// builder: (dialogContext) => AlertDialog( +// backgroundColor: const Color(0xFF1E293B), +// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), +// title: const Text( +// 'System Termination', +// style: TextStyle(color: Colors.white), +// ), +// content: const Text('Are you sure you want to logout?'), +// actions: [ +// TextButton( +// onPressed: () => Navigator.pop(dialogContext), +// child: const Text('CANCEL'), +// ), +// TextButton( +// onPressed: () { +// Navigator.pop(dialogContext); +// context.read().add(AuthLogoutRequested()); +// Navigator.pushNamedAndRemoveUntil( +// context, +// '/login', +// (route) => false, +// ); +// }, +// child: +// const Text('LOGOUT', style: TextStyle(color: Colors.redAccent)), +// ), +// ], +// ), +// ); +// } + +// static void showDeleteAccount(BuildContext context) async { +// final confirm = await showDialog( +// context: context, +// builder: (dialogContext) => 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, cloud data,' +// 'and local profile. Continue?', +// style: TextStyle(color: Colors.white70), +// ), +// actions: [ +// TextButton( +// onPressed: () => Navigator.pop(dialogContext, false), +// child: +// const Text('CANCEL', style: TextStyle(color: Colors.white38)), +// ), +// ElevatedButton( +// style: ElevatedButton.styleFrom( +// backgroundColor: const Color(0xFFF87171).withValues(alpha: 0.2), +// foregroundColor: const Color(0xFFF87171), +// side: const BorderSide(color: Color(0xFFF87171)), +// ), +// onPressed: () => Navigator.pop(dialogContext, true), +// child: const Text('CONFIRM PURGE'), +// ), +// ], +// ), +// ); + +// if (confirm == true && context.mounted) { +// context.read().add(ProfileDeleteAccountRequested()); +// } +// } + +// static void showEditField( +// BuildContext context, +// String fieldTitle, +// String currentValue, +// ) async { +// final controller = TextEditingController(text: currentValue); +// final bloc = context.read(); + +// final newValue = await showDialog( +// context: context, +// builder: (dialogContext) => AlertDialog( +// backgroundColor: const Color(0xFF1E293B), +// title: Text( +// 'Update $fieldTitle', +// style: const TextStyle(color: Colors.white), +// ), +// content: TextField( +// controller: controller, +// style: const TextStyle(color: Colors.white), +// decoration: InputDecoration( +// hintText: 'Enter new $fieldTitle', +// enabledBorder: const UnderlineInputBorder( +// borderSide: BorderSide(color: Colors.white24), +// ), +// ), +// ), +// actions: [ +// TextButton( +// onPressed: () => Navigator.pop(dialogContext), +// child: const Text('Cancel'), +// ), +// TextButton( +// onPressed: () => Navigator.pop(dialogContext, controller.text), +// child: +// const Text('Save', style: TextStyle(color: Color(0xFF38BDF8))), +// ), +// ], +// ), +// ); + +// if (newValue != null && newValue.isNotEmpty) { +// bloc.add(ProfileUpdateFieldRequested(fieldTitle, newValue)); +// } +// } +// } diff --git a/lib/utils/register_form.dart b/lib/utils/register_form.dart new file mode 100644 index 0000000..a50ae3c --- /dev/null +++ b/lib/utils/register_form.dart @@ -0,0 +1,268 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/auth_cubit.dart'; +import 'package:mobile_flutter_iot/models/user_model.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_input.dart'; +import 'package:mobile_flutter_iot/widgets/common/primary_button.dart'; + +class RegisterForm extends StatefulWidget { + final bool isLoading; + const RegisterForm({required this.isLoading, super.key}); + + @override + State createState() => _RegisterFormState(); +} + +class _RegisterFormState extends State { + final _nameController = TextEditingController(); + final _emailController = TextEditingController(); + final _deptController = TextEditingController(); + final _passwordController = TextEditingController(); + + String? _nameError, _emailError, _deptError, _passwordError; + + void _handleRegister() { + setState(() { + _nameError = _emailError = _deptError = _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 = 'Required'); + hasError = true; + } + if (email.isEmpty || !email.contains('@')) { + setState(() => _emailError = 'Invalid email'); + hasError = true; + } + if (dept.isEmpty) { + setState(() => _deptError = 'Required'); + hasError = true; + } + if (password.length < 6) { + setState(() => _passwordError = 'Min 6 chars'); + hasError = true; + } + + if (hasError) return; + + final newUser = UserModel( + fullName: name, + email: email, + password: password, + department: dept, + ); + + context.read().register(newUser); + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _deptController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GlassCard( + child: Column( + children: [ + GlassInput( + hintText: 'Full Name', + icon: Icons.person_outline, + controller: _nameController, + errorText: _nameError, + ), + const SizedBox(height: 16), + GlassInput( + hintText: 'Email Address', + icon: Icons.alternate_email, + controller: _emailController, + errorText: _emailError, + ), + const SizedBox(height: 16), + GlassInput( + hintText: 'Department (e.g., KSA)', + icon: Icons.business_center_outlined, + controller: _deptController, + errorText: _deptError, + ), + const SizedBox(height: 16), + GlassInput( + hintText: 'Access Password', + icon: Icons.lock_open_rounded, + isPassword: true, + controller: _passwordController, + errorText: _passwordError, + ), + const SizedBox(height: 32), + if (widget.isLoading) + const CircularProgressIndicator(color: Color(0xFF4ADE80)) + else + PrimaryButton( + text: 'INITIALIZE ACCOUNT', + onPressed: _handleRegister, + ), + const SizedBox(height: 16), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text( + 'ALREADY HAVE A KEY? RETURN', + style: TextStyle( + color: Colors.white24, + fontSize: 10, + letterSpacing: 1, + ), + ), + ), + ], + ), + ); + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/auth/auth_event.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_input.dart'; +// import 'package:mobile_flutter_iot/widgets/common/primary_button.dart'; + +// class RegisterForm extends StatefulWidget { +// final bool isLoading; +// const RegisterForm({required this.isLoading, super.key}); + +// @override +// State createState() => _RegisterFormState(); +// } + +// class _RegisterFormState extends State { +// final _nameController = TextEditingController(); +// final _emailController = TextEditingController(); +// final _deptController = TextEditingController(); +// final _passwordController = TextEditingController(); + +// String? _nameError, _emailError, _deptError, _passwordError; + +// void _handleRegister() { +// setState(() { +// _nameError = _emailError = _deptError = _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 = 'Required'); +// hasError = true; +// } +// if (email.isEmpty || !email.contains('@')) { +// setState(() => _emailError = 'Invalid email'); +// hasError = true; +// } +// if (dept.isEmpty) { +// setState(() => _deptError = 'Required'); +// hasError = true; +// } +// if (password.length < 6) { +// setState(() => _passwordError = 'Min 6 chars'); +// hasError = true; +// } + +// if (hasError) return; + +// context.read().add( +// AuthRegisterRequested( +// name: name, +// email: email, +// password: password, +// ), +// ); +// } + +// @override +// void dispose() { +// _nameController.dispose(); +// _emailController.dispose(); +// _deptController.dispose(); +// _passwordController.dispose(); +// super.dispose(); +// } + +// @override +// Widget build(BuildContext context) { +// return GlassCard( +// child: Column( +// children: [ +// GlassInput( +// hintText: 'Full Name', +// icon: Icons.person_outline, +// controller: _nameController, +// errorText: _nameError, +// ), +// const SizedBox(height: 16), +// GlassInput( +// hintText: 'Email Address', +// icon: Icons.alternate_email, +// controller: _emailController, +// errorText: _emailError, +// ), +// const SizedBox(height: 16), +// GlassInput( +// hintText: 'Department (e.g., KSA)', +// icon: Icons.business_center_outlined, +// controller: _deptController, +// errorText: _deptError, +// ), +// const SizedBox(height: 16), +// GlassInput( +// hintText: 'Access Password', +// icon: Icons.lock_open_rounded, +// isPassword: true, +// controller: _passwordController, +// errorText: _passwordError, +// ), +// const SizedBox(height: 32), +// if (widget.isLoading) +// const CircularProgressIndicator(color: Color(0xFF4ADE80)) +// else +// PrimaryButton( +// text: 'INITIALIZE ACCOUNT', +// onPressed: _handleRegister, +// ), +// const SizedBox(height: 16), +// TextButton( +// onPressed: () => Navigator.pop(context), +// child: const Text( +// 'ALREADY HAVE A KEY? RETURN', +// style: TextStyle( +// color: Colors.white24, +// fontSize: 10, +// letterSpacing: 1, +// ), +// ), +// ), +// ], +// ), +// ); +// } +// } diff --git a/lib/widgets/api_device_list.dart b/lib/widgets/api_device_list.dart deleted file mode 100644 index d1d2abe..0000000 --- a/lib/widgets/api_device_list.dart +++ /dev/null @@ -1,124 +0,0 @@ -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/services/api_service.dart'; -import 'package:mobile_flutter_iot/widgets/workspace_card.dart'; - -class ApiDeviceList extends StatefulWidget { - final String? mqttIp; - const ApiDeviceList({this.mqttIp, super.key}); - - @override - State createState() => _ApiDeviceListState(); -} - -class _ApiDeviceListState extends State { - final ApiService _apiService = ApiService(); - final LocalUserRepository _cache = LocalUserRepository(); - - late Future> _devicesFuture; - - @override - void initState() { - super.initState(); - _refreshData(); - } - - void _refreshData() { - setState(() { - _devicesFuture = _fetchAndSyncDevices(); - }); - } - - Future> _fetchAndSyncDevices() async { - try { - final cloudDevices = await _apiService.fetchDevices(); - if (cloudDevices != null && cloudDevices.isNotEmpty) { - await _cache.saveDevices(cloudDevices); - return cloudDevices; - } - } catch (e) { - debugPrint('API Error, falling back to cache: $e'); - } - return await _cache.getDevices(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: _devicesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Padding( - padding: EdgeInsets.all(32), - child: Center( - child: CircularProgressIndicator(color: Color(0xFF38BDF8)), - ), - ); - } - - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center( - child: Padding( - padding: EdgeInsets.all(32), - child: Text( - 'No devices found.\nAdd one manually or connect ESP.', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.white30), - ), - ), - ); - } - - final devices = snapshot.data!; - return ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - itemCount: devices.length, - separatorBuilder: (_, __) => const SizedBox(height: 16), - itemBuilder: (context, index) { - final device = devices[index]; - return GestureDetector( - onTap: () async { - await Navigator.pushNamed( - context, - '/details', - arguments: SensorArguments( - id: device.id, - title: device.title, - value: device.value, - icon: device.icon, - color: device.color, - ipAddress: widget.mqttIp ?? 'No IP', - ), - ); - _refreshData(); - }, - onLongPress: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AddDeviceScreen(device: device), - ), - ); - _refreshData(); - }, - child: WorkspaceCard( - id: device.id, - title: device.title, - value: device.value, - status: device.status, - subtitle: 'Cloud / Local', - icon: device.icon, - accentColor: device.color, - ), - ); - }, - ); - }, - ); - } -} diff --git a/lib/widgets/blur_blob.dart b/lib/widgets/common/blur_blob.dart similarity index 100% rename from lib/widgets/blur_blob.dart rename to lib/widgets/common/blur_blob.dart diff --git a/lib/widgets/glass_card.dart b/lib/widgets/common/glass_card.dart similarity index 100% rename from lib/widgets/glass_card.dart rename to lib/widgets/common/glass_card.dart diff --git a/lib/widgets/glass_input.dart b/lib/widgets/common/glass_input.dart similarity index 100% rename from lib/widgets/glass_input.dart rename to lib/widgets/common/glass_input.dart diff --git a/lib/widgets/indicator.dart b/lib/widgets/common/indicator.dart similarity index 100% rename from lib/widgets/indicator.dart rename to lib/widgets/common/indicator.dart diff --git a/lib/widgets/primary_button.dart b/lib/widgets/common/primary_button.dart similarity index 100% rename from lib/widgets/primary_button.dart rename to lib/widgets/common/primary_button.dart diff --git a/lib/widgets/workspace_card.dart b/lib/widgets/common/workspace_card.dart similarity index 97% rename from lib/widgets/workspace_card.dart rename to lib/widgets/common/workspace_card.dart index 004cfb8..67fddb0 100644 --- a/lib/widgets/workspace_card.dart +++ b/lib/widgets/common/workspace_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.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/common/glass_card.dart'; class WorkspaceCard extends StatelessWidget { final String id; diff --git a/lib/widgets/alert_card.dart b/lib/widgets/devices/alert_card.dart similarity index 97% rename from lib/widgets/alert_card.dart rename to lib/widgets/devices/alert_card.dart index b1d90bd..49e8faa 100644 --- a/lib/widgets/alert_card.dart +++ b/lib/widgets/devices/alert_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/widgets/glass_card.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; class AlertCard extends StatelessWidget { final String title; diff --git a/lib/widgets/devices/api_device_list.dart b/lib/widgets/devices/api_device_list.dart new file mode 100644 index 0000000..4eb235b --- /dev/null +++ b/lib/widgets/devices/api_device_list.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/device_cubit.dart'; +import 'package:mobile_flutter_iot/models/device_model.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/common/workspace_card.dart'; + +class ApiDeviceList extends StatelessWidget { + final List devices; + final String? mqttIp; + + const ApiDeviceList({required this.devices, this.mqttIp, super.key}); + + @override + Widget build(BuildContext context) { + if (devices.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(32), + child: Text( + 'No devices found.\nAdd one manually or connect ESP.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white30), + ), + ), + ); + } + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: devices.length, + separatorBuilder: (_, __) => const SizedBox(height: 16), + itemBuilder: (context, index) { + final device = devices[index]; + + return GestureDetector( + onTap: () async { + await Navigator.pushNamed( + context, + '/details', + arguments: SensorArguments( + id: device.id, + title: device.title, + value: device.value, + icon: device.icon, + color: device.color, + ipAddress: mqttIp ?? 'No IP', + ), + ); + if (context.mounted) { + context.read().loadDevices(); + } + }, + onLongPress: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddDeviceScreen(device: device), + ), + ); + if (context.mounted) { + context.read().loadDevices(); + } + }, + child: WorkspaceCard( + id: device.id, + title: device.title, + value: device.value, + status: device.status, + subtitle: 'Cloud / Local', + icon: device.icon, + accentColor: device.color, + ), + ); + }, + ); + } +} diff --git a/lib/widgets/devices/color_picker.dart b/lib/widgets/devices/color_picker.dart new file mode 100644 index 0000000..169e402 --- /dev/null +++ b/lib/widgets/devices/color_picker.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/add_device_cubit.dart'; + +class ColorPicker extends StatelessWidget { + final AddDeviceState state; + final List colors; + + const ColorPicker({ + required this.state, + required this.colors, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: colors.map((color) { + return GestureDetector( + onTap: () => context.read().selectColor(color), + child: Container( + width: 45, + height: 45, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + shape: BoxShape.circle, + border: Border.all( + color: + state.selectedColor == color ? color : Colors.transparent, + width: 2, + ), + ), + child: Center( + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + ), + ), + ); + }).toList(), + ); + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/add_device/add_device_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/add_device/add_device_event.dart'; +// import 'package:mobile_flutter_iot/blocs/add_device/add_device_state.dart'; + +// class ColorPicker extends StatelessWidget { +// final AddDeviceState state; +// final List colors; + +// const ColorPicker({ +// required this.state, +// required this.colors, +// super.key, +// }); + +// @override +// Widget build(BuildContext context) { +// return Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: colors.map((color) { +// return GestureDetector( +// onTap: () => +// context.read().add(AddDeviceColorChanged(color)), +// child: Container( +// width: 45, +// height: 45, +// decoration: BoxDecoration( +// color: color.withValues(alpha: 0.2), +// shape: BoxShape.circle, +// border: Border.all( +// color: +// state.selectedColor == color ? color : Colors.transparent, +// width: 2, +// ), +// ), +// child: Center( +// child: Container( +// width: 20, +// height: 20, +// decoration: BoxDecoration(color: color, shape: BoxShape.circle), +// ), +// ), +// ), +// ); +// }).toList(), +// ); +// } +// } diff --git a/lib/widgets/control_title.dart b/lib/widgets/devices/control_title.dart similarity index 93% rename from lib/widgets/control_title.dart rename to lib/widgets/devices/control_title.dart index 96415e4..8ec2da6 100644 --- a/lib/widgets/control_title.dart +++ b/lib/widgets/devices/control_title.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/widgets/glass_card.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; class ControlTile extends StatelessWidget { final String title; diff --git a/lib/widgets/fan_widget.dart b/lib/widgets/devices/fan_widget.dart similarity index 100% rename from lib/widgets/fan_widget.dart rename to lib/widgets/devices/fan_widget.dart diff --git a/lib/widgets/devices/icon_picker.dart b/lib/widgets/devices/icon_picker.dart new file mode 100644 index 0000000..271a282 --- /dev/null +++ b/lib/widgets/devices/icon_picker.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/add_device_cubit.dart'; + +class IconPicker extends StatelessWidget { + final AddDeviceState state; + final List icons; + + const IconPicker({ + required this.state, + required this.icons, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 16, + children: icons.map((icon) { + return GestureDetector( + onTap: () => context.read().selectIcon(icon), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: state.selectedIcon == icon + ? state.selectedColor.withValues(alpha: 0.2) + : Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: state.selectedIcon == icon + ? state.selectedColor + : Colors.transparent, + ), + ), + child: Icon( + icon, + color: state.selectedIcon == icon + ? state.selectedColor + : Colors.white38, + ), + ), + ); + }).toList(), + ); + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/add_device/add_device_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/add_device/add_device_event.dart'; +// import 'package:mobile_flutter_iot/blocs/add_device/add_device_state.dart'; + +// class IconPicker extends StatelessWidget { +// final AddDeviceState state; +// final List icons; + +// const IconPicker({ +// required this.state, +// required this.icons, +// super.key, +// }); + +// @override +// Widget build(BuildContext context) { +// return Wrap( +// spacing: 16, +// children: icons.map((icon) { +// return GestureDetector( +// onTap: () => +// context.read().add(AddDeviceIconChanged(icon)), +// child: Container( +// padding: const EdgeInsets.all(12), +// decoration: BoxDecoration( +// color: state.selectedIcon == icon +// ? state.selectedColor.withValues(alpha: 0.2) +// : Colors.white.withValues(alpha: 0.05), +// borderRadius: BorderRadius.circular(12), +// border: Border.all( +// color: state.selectedIcon == icon +// ? state.selectedColor +// : Colors.transparent, +// ), +// ), +// child: Icon( +// icon, +// color: state.selectedIcon == icon +// ? state.selectedColor +// : Colors.white38, +// ), +// ), +// ); +// }).toList(), +// ); +// } +// } diff --git a/lib/widgets/devices/mini_stat_card.dart b/lib/widgets/devices/mini_stat_card.dart new file mode 100644 index 0000000..bc81b22 --- /dev/null +++ b/lib/widgets/devices/mini_stat_card.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; + +class MiniStatCard extends StatelessWidget { + final String label; + final String value; + final IconData icon; + final Color color; + final bool isEditable; + + const MiniStatCard({ + required this.label, + required this.value, + required this.icon, + required this.color, + this.isEditable = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + return GlassCard( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + 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, + style: const TextStyle(fontSize: 12, color: Colors.white54), + ), + Text( + value, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/sensor_chart.dart b/lib/widgets/devices/sensor_chart.dart similarity index 100% rename from lib/widgets/sensor_chart.dart rename to lib/widgets/devices/sensor_chart.dart diff --git a/lib/widgets/tech_node.dart b/lib/widgets/devices/tech_node.dart similarity index 100% rename from lib/widgets/tech_node.dart rename to lib/widgets/devices/tech_node.dart diff --git a/lib/widgets/mqtt/mqtt_control_card.dart b/lib/widgets/mqtt/mqtt_control_card.dart new file mode 100644 index 0000000..3f4d188 --- /dev/null +++ b/lib/widgets/mqtt/mqtt_control_card.dart @@ -0,0 +1,220 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/mqtt_cubit.dart'; +import 'package:mobile_flutter_iot/utils/mqtt_dialogs.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; + +class MqttControlCard extends StatelessWidget { + final MqttState mqttState; + + const MqttControlCard({required this.mqttState, super.key}); + + @override + Widget build(BuildContext context) { + final mqttCubit = context.read(); + final bool isLocked = mqttCubit.isTimeRestricted(); + final String lockTimeStr = + '${mqttState.startLockHour.toString().padLeft(2, '0')}:00 - ' + '${mqttState.endLockHour.toString().padLeft(2, '0')}:00'; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: GlassCard( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: ListTile( + contentPadding: EdgeInsets.zero, + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isLocked + ? Colors.redAccent.withValues(alpha: 0.1) + : mqttState.isLedOn + ? Colors.yellow.withValues(alpha: 0.1) + : Colors.white.withValues(alpha: 0.05), + shape: BoxShape.circle, + ), + child: Icon( + isLocked + ? Icons.lock_clock + : mqttState.isLedOn + ? Icons.lightbulb + : Icons.lightbulb_outline, + color: isLocked + ? Colors.redAccent + : mqttState.isLedOn + ? Colors.yellow + : Colors.white24, + ), + ), + title: Row( + children: [ + Text( + isLocked ? 'LED System (Locked)' : 'Smart LED System', + style: + const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + const Spacer(), + IconButton( + icon: + const Icon(Icons.security, size: 16, color: Colors.white38), + onPressed: () => MqttDialogs.showSetLockPolicy( + context, + mqttCubit, + mqttState, + ), + ), + ], + ), + subtitle: Text( + isLocked + ? 'Restricted hours ($lockTimeStr)' + : mqttState.isLedOn + ? 'Active (ON)' + : 'Inactive (OFF)', + style: TextStyle( + fontSize: 11, + color: isLocked + ? Colors.redAccent.withValues(alpha: 0.7) + : Colors.white38, + ), + ), + trailing: Switch( + value: !isLocked && mqttState.isLedOn, + activeThumbColor: Colors.yellow, + onChanged: (bool value) { + if (isLocked) { + mqttCubit.toggleLed(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('ACCESS DENIED: System lock policy active'), + backgroundColor: Colors.redAccent, + ), + ); + } else { + mqttCubit.toggleLed(); + } + }, + ), + ), + ), + ); + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_event.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_state.dart'; +// import 'package:mobile_flutter_iot/utils/mqtt_dialogs.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; + +// class MqttControlCard extends StatelessWidget { +// final MqttState mqttState; + +// const MqttControlCard({required this.mqttState, super.key}); + +// bool _isTimeRestricted(int start, int end) { +// final now = DateTime.now().hour; +// if (start <= end) return now >= start && now < end; +// return now >= start || now < end; +// } + +// @override +// Widget build(BuildContext context) { +// final mqttBloc = context.read(); // ЗМІНЕНО +// final bool isLocked = +// _isTimeRestricted(mqttState.startLockHour, mqttState.endLockHour); +// final String lockTimeStr = +// '${mqttState.startLockHour.toString().padLeft(2, '0')}:00 - ' +// '${mqttState.endLockHour.toString().padLeft(2, '0')}:00'; + +// return Padding( +// padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), +// child: GlassCard( +// padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), +// child: ListTile( +// contentPadding: EdgeInsets.zero, +// leading: Container( +// padding: const EdgeInsets.all(8), +// decoration: BoxDecoration( +// color: isLocked +// ? Colors.redAccent.withValues(alpha: 0.1) +// : mqttState.isLedOn +// ? Colors.yellow.withValues(alpha: 0.1) +// : Colors.white.withValues(alpha: 0.05), +// shape: BoxShape.circle, +// ), +// child: Icon( +// isLocked +// ? Icons.lock_clock +// : mqttState.isLedOn +// ? Icons.lightbulb +// : Icons.lightbulb_outline, +// color: isLocked +// ? Colors.redAccent +// : mqttState.isLedOn +// ? Colors.yellow +// : Colors.white24, +// ), +// ), +// title: Row( +// children: [ +// Text( +// isLocked ? 'LED System (Locked)' : 'Smart LED System', +// style: +// const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), +// ), +// const Spacer(), +// IconButton( +// icon: +// const Icon(Icons.security, size: 16, color: Colors.white38), +// onPressed: () => MqttDialogs.showSetLockPolicy( +// context, +// mqttBloc, +// mqttState, +// ), +// ), +// ], +// ), +// subtitle: Text( +// isLocked +// ? 'Restricted hours ($lockTimeStr)' +// : mqttState.isLedOn +// ? 'Active (ON)' +// : 'Inactive (OFF)', +// style: TextStyle( +// fontSize: 11, +// color: isLocked +// ? Colors.redAccent.withValues(alpha: 0.7) +// : Colors.white38, +// ), +// ), +// trailing: Switch( +// value: !isLocked && mqttState.isLedOn, +// activeThumbColor: Colors.yellow, +// onChanged: (bool value) { +// if (isLocked) { +// mqttBloc.add(MqttToggleHardwareRequested(!mqttState.isLedOn)); +// ScaffoldMessenger.of(context).showSnackBar( +// const SnackBar( +// content: Text('ACCESS DENIED: System lock policy active'), +// backgroundColor: Colors.redAccent, +// ), +// ); +// } else { +// mqttBloc.add(MqttToggleHardwareRequested(!mqttState.isLedOn)); +// } +// }, +// ), +// ), +// ), +// ); +// } +// } diff --git a/lib/widgets/mqtt/mqtt_section.dart b/lib/widgets/mqtt/mqtt_section.dart new file mode 100644 index 0000000..5e79998 --- /dev/null +++ b/lib/widgets/mqtt/mqtt_section.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/mqtt_cubit.dart'; +import 'package:mobile_flutter_iot/screens/home/details_screen.dart'; +import 'package:mobile_flutter_iot/utils/mqtt_dialogs.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +import 'package:mobile_flutter_iot/widgets/common/indicator.dart'; +import 'package:mobile_flutter_iot/widgets/common/workspace_card.dart'; +import 'package:mobile_flutter_iot/widgets/mqtt/mqtt_control_card.dart'; + +class MqttSection extends StatelessWidget { + final MqttState mqttState; + const MqttSection({required this.mqttState, super.key}); + + @override + Widget build(BuildContext context) { + final bool isConnected = mqttState.status == MqttStatus.connected; + + return Column( + children: [ + _buildStatusCard(context, isConnected), + if (isConnected) ...[ + _buildMqttLiveNode(context), + MqttControlCard(mqttState: mqttState), + ], + ], + ); + } + + Widget _buildStatusCard(BuildContext context, bool isConnected) { + final statusColor = + isConnected ? const Color(0xFF4ADE80) : const Color(0xFFF87171); + + return Padding( + padding: const EdgeInsets.all(16), + child: GlassCard( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), + child: Row( + children: [ + SystemPulseIndicator(color: statusColor), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'SYSTEM ENGINE', + style: TextStyle(fontSize: 10, color: Colors.white38), + ), + Text( + isConnected ? 'ONLINE' : 'MQTT DISCONNECTED', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Spacer(), + GestureDetector( + onTap: () => MqttDialogs.showEditIpAddress( + context, + context.read(), + mqttState, + ), + child: Row( + children: [ + Text( + mqttState.client?.server ?? 'No IP', + style: const TextStyle(fontSize: 10, color: Colors.white24), + ), + const SizedBox(width: 4), + const Icon(Icons.edit, size: 10, color: Colors.white24), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildMqttLiveNode(BuildContext context) { + final double currentAqi = + double.tryParse(mqttState.airQuality.toString()) ?? 0.0; + final bool isSensorOffline = currentAqi == 0.0; + + final String displayValue = + isSensorOffline ? 'No Data' : '${mqttState.airQuality} AQI'; + final String displayStatus = isSensorOffline ? 'SENSOR OFFLINE' : 'LIVE'; + final Color displayColor = + isSensorOffline ? Colors.white24 : const Color(0xFF38BDF8); + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + '/details', + arguments: SensorArguments( + id: 'ESP_AIR_01', + title: 'Air Quality', + value: displayValue, + icon: Icons.air_rounded, + color: displayColor, + status: displayStatus, + ipAddress: mqttState.client?.server ?? '192.168.1.XXX', + ), + ), + child: WorkspaceCard( + id: 'ESP_AIR_01', + title: 'Air Quality (ESP8266)', + value: displayValue, + status: displayStatus, + subtitle: + isSensorOffline ? 'Check hardware power' : 'Real-time MQTT data', + icon: Icons.air_rounded, + accentColor: displayColor, + ), + ), + ); + } +} + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// // ЗМІНЕНО ІМПОРТИ +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/mqtt/mqtt_state.dart'; +// import 'package:mobile_flutter_iot/screens/home/details_screen.dart'; +// import 'package:mobile_flutter_iot/utils/mqtt_dialogs.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +// import 'package:mobile_flutter_iot/widgets/common/indicator.dart'; +// import 'package:mobile_flutter_iot/widgets/common/workspace_card.dart'; +// import 'package:mobile_flutter_iot/widgets/mqtt/mqtt_control_card.dart'; + +// class MqttSection extends StatelessWidget { +// final MqttState mqttState; +// const MqttSection({required this.mqttState, super.key}); + +// @override +// Widget build(BuildContext context) { +// final bool isConnected = mqttState.status == MqttStatus.connected; + +// return Column( +// children: [ +// _buildStatusCard(context, isConnected), +// if (isConnected) ...[ +// _buildMqttLiveNode(context), +// MqttControlCard(mqttState: mqttState), +// ], +// ], +// ); +// } + +// Widget _buildStatusCard(BuildContext context, bool isConnected) { +// final statusColor = +// isConnected ? const Color(0xFF4ADE80) : const Color(0xFFF87171); + +// return Padding( +// padding: const EdgeInsets.all(16), +// child: GlassCard( +// padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), +// child: Row( +// children: [ +// SystemPulseIndicator(color: statusColor), +// const SizedBox(width: 12), +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// const Text( +// 'SYSTEM ENGINE', +// style: TextStyle(fontSize: 10, color: Colors.white38), +// ), +// Text( +// isConnected ? 'ONLINE' : 'MQTT DISCONNECTED', +// style: const TextStyle( +// fontSize: 11, +// fontWeight: FontWeight.bold, +// ), +// ), +// ], +// ), +// const Spacer(), +// GestureDetector( +// onTap: () => MqttDialogs.showEditIpAddress( +// context, +// context.read(), // ЗМІНЕНО +// mqttState, +// ), +// child: Row( +// children: [ +// Text( +// mqttState.client?.server ?? 'No IP', +// style: const TextStyle(fontSize: 10, color: Colors.white24), +// ), +// const SizedBox(width: 4), +// const Icon(Icons.edit, size: 10, color: Colors.white24), +// ], +// ), +// ), +// ], +// ), +// ), +// ); +// } + +// Widget _buildMqttLiveNode(BuildContext context) { +// final double currentAqi = +// double.tryParse(mqttState.airQuality.toString()) ?? 0.0; +// final bool isSensorOffline = currentAqi == 0.0; + +// final String displayValue = +// isSensorOffline ? 'No Data' : '${mqttState.airQuality} AQI'; +// final String displayStatus = isSensorOffline ? 'SENSOR OFFLINE' : 'LIVE'; +// final Color displayColor = +// isSensorOffline ? Colors.white24 : const Color(0xFF38BDF8); + +// return Padding( +// padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), +// child: GestureDetector( +// onTap: () => Navigator.pushNamed( +// context, +// '/details', +// arguments: SensorArguments( +// id: 'ESP_AIR_01', +// title: 'Air Quality', +// value: displayValue, +// icon: Icons.air_rounded, +// color: displayColor, +// status: displayStatus, +// ipAddress: mqttState.client?.server ?? '192.168.1.XXX', +// ), +// ), +// child: WorkspaceCard( +// id: 'ESP_AIR_01', +// title: 'Air Quality (ESP8266)', +// value: displayValue, +// status: displayStatus, +// subtitle: +// isSensorOffline ? 'Check hardware power' : 'Real-time MQTT data', +// icon: Icons.air_rounded, +// accentColor: displayColor, +// ), +// ), +// ); +// } +// } diff --git a/lib/widgets/mqtt_section.dart b/lib/widgets/mqtt_section.dart deleted file mode 100644 index 0a2615f..0000000 --- a/lib/widgets/mqtt_section.dart +++ /dev/null @@ -1,335 +0,0 @@ -import 'dart:math'; -import 'package:flutter/material.dart'; -import 'package:mobile_flutter_iot/providers/mqtt_provider.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:shared_preferences/shared_preferences.dart'; - -class MqttSection extends StatelessWidget { - final MqttProvider mqtt; - const MqttSection({required this.mqtt, super.key}); - - Future _editIpAddress(BuildContext context) async { - final prefs = await SharedPreferences.getInstance(); - if (!context.mounted) return; - - final currentIp = - mqtt.client?.server ?? prefs.getString('mqtt_ip') ?? '192.168.1.XXX'; - final controller = TextEditingController(text: currentIp); - - final newIp = await showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: const Color(0xFF1E293B), - title: - const Text('Set Broker IP', style: TextStyle(color: Colors.white)), - content: TextField( - controller: controller, - autofocus: true, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( - hintText: 'e.g. 192.168.1.100', - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white24), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(context, controller.text.trim()), - child: const Text( - 'Save & Reconnect', - style: TextStyle(color: Color(0xFF4ADE80)), - ), - ), - ], - ), - ); - - if (newIp != null && newIp.isNotEmpty && context.mounted) { - await prefs.setString('mqtt_ip', newIp); - if (!context.mounted) return; - - mqtt.disconnect(); - mqtt.initMqtt(newIp, 'flutter_client_${Random().nextInt(100)}'); - mqtt.connect(); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Reconnecting to $newIp...'), - backgroundColor: Colors.blueGrey, - ), - ); - } - } - - Future _setLockPolicy(BuildContext context) async { - final startController = - TextEditingController(text: mqtt.startLockHour.toString()); - final endController = - TextEditingController(text: mqtt.endLockHour.toString()); - - await showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: const Color(0xFF1E293B), - title: const Text( - 'Admin Security Policy', - style: TextStyle(color: Colors.white), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Set hours when LED control is disabled.' - 'Actions will be logged to the server.', - style: TextStyle(color: Colors.white70, fontSize: 12), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: TextField( - controller: startController, - keyboardType: TextInputType.number, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( - labelText: 'Lock From (Hour)', - labelStyle: TextStyle(color: Colors.white54), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: TextField( - controller: endController, - keyboardType: TextInputType.number, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( - labelText: 'Unlock At (Hour)', - labelStyle: TextStyle(color: Colors.white54), - ), - ), - ), - ], - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - final start = int.tryParse(startController.text) ?? 22; - final end = int.tryParse(endController.text) ?? 6; - mqtt.updateLockHours(start, end); - Navigator.pop(context); - }, - child: const Text( - 'Save Policy', - style: TextStyle(color: Colors.redAccent), - ), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - final bool isConnected = mqtt.status == MqttStatus.connected; - - return Column( - children: [ - _buildStatusCard(context, isConnected), - if (isConnected) ...[ - _buildMqttLiveNode(context), - _buildMqttControlNode(context), - ], - ], - ); - } - - Widget _buildStatusCard(BuildContext context, bool isConnected) { - final statusColor = - isConnected ? const Color(0xFF4ADE80) : const Color(0xFFF87171); - - return Padding( - padding: const EdgeInsets.all(16), - child: GlassCard( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), - child: Row( - children: [ - SystemPulseIndicator(color: statusColor), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'SYSTEM ENGINE', - style: TextStyle(fontSize: 10, color: Colors.white38), - ), - Text( - isConnected ? 'ONLINE' : 'MQTT DISCONNECTED', - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Spacer(), - GestureDetector( - onTap: () => _editIpAddress(context), - child: Row( - children: [ - Text( - mqtt.client?.server ?? 'No IP', - style: const TextStyle(fontSize: 10, color: Colors.white24), - ), - const SizedBox(width: 4), - const Icon(Icons.edit, size: 10, color: Colors.white24), - ], - ), - ), - ], - ), - ), - ); - } - - Widget _buildMqttLiveNode(BuildContext context) { - final double currentAqi = - double.tryParse(mqtt.airQuality.toString()) ?? 0.0; - final bool isSensorOffline = currentAqi == 0.0; - - final String displayValue = - isSensorOffline ? 'No Data' : '${mqtt.airQuality} AQI'; - final String displayStatus = isSensorOffline ? 'SENSOR OFFLINE' : 'LIVE'; - final Color displayColor = - isSensorOffline ? Colors.white24 : const Color(0xFF38BDF8); - - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), - child: GestureDetector( - onTap: () => Navigator.pushNamed( - context, - '/details', - arguments: SensorArguments( - id: 'ESP_AIR_01', - title: 'Air Quality', - value: displayValue, - icon: Icons.air_rounded, - color: displayColor, - status: displayStatus, - ipAddress: mqtt.client?.server ?? '192.168.1.XXX', - ), - ), - child: WorkspaceCard( - id: 'ESP_AIR_01', - title: 'Air Quality (ESP8266)', - value: displayValue, - status: displayStatus, - subtitle: - isSensorOffline ? 'Check hardware power' : 'Real-time MQTT data', - icon: Icons.air_rounded, - accentColor: displayColor, - ), - ), - ); - } - - Widget _buildMqttControlNode(BuildContext context) { - final bool isLocked = mqtt.isTimeRestricted(); - - final String lockTimeStr = - '${mqtt.startLockHour.toString().padLeft(2, '0')}:00 - ' - '${mqtt.endLockHour.toString().padLeft(2, '0')}:00'; - - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: GlassCard( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: ListTile( - contentPadding: EdgeInsets.zero, - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: isLocked - ? Colors.redAccent.withValues(alpha: 0.1) - : mqtt.isLedOn - ? Colors.yellow.withValues(alpha: 0.1) - : Colors.white.withValues(alpha: 0.05), - shape: BoxShape.circle, - ), - child: Icon( - isLocked - ? Icons.lock_clock - : mqtt.isLedOn - ? Icons.lightbulb - : Icons.lightbulb_outline, - color: isLocked - ? Colors.redAccent - : mqtt.isLedOn - ? Colors.yellow - : Colors.white24, - ), - ), - title: Row( - children: [ - Text( - isLocked ? 'LED System (Locked)' : 'Smart LED System', - style: - const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - ), - const Spacer(), - IconButton( - icon: - const Icon(Icons.security, size: 16, color: Colors.white38), - onPressed: () => _setLockPolicy(context), - ), - ], - ), - subtitle: Text( - isLocked - ? 'Restricted hours ($lockTimeStr)' - : mqtt.isLedOn - ? 'Active (ON)' - : 'Inactive (OFF)', - style: TextStyle( - fontSize: 11, - color: isLocked - ? Colors.redAccent.withValues(alpha: 0.7) - : Colors.white38, - ), - ), - trailing: Switch( - value: !isLocked && mqtt.isLedOn, - activeThumbColor: Colors.yellow, - onChanged: (bool value) { - if (isLocked) { - mqtt.toggleLed(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('ACCESS DENIED: System lock policy active'), - backgroundColor: Colors.redAccent, - ), - ); - } else { - mqtt.toggleLed(); - } - }, - ), - ), - ), - ); - } -} diff --git a/lib/widgets/profile/emergency_wrapper.dart b/lib/widgets/profile/emergency_wrapper.dart new file mode 100644 index 0000000..9490f52 --- /dev/null +++ b/lib/widgets/profile/emergency_wrapper.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:iot_flashlight/iot_flashlight.dart'; +import 'package:mobile_flutter_iot/cubits/profile_cubit.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; +import 'package:mobile_flutter_iot/widgets/profile/override_background.dart'; +import 'package:mobile_flutter_iot/widgets/profile/profile_header.dart'; +import 'package:mobile_flutter_iot/widgets/profile/profile_settings.dart'; +import 'package:mobile_flutter_iot/widgets/profile/warning_dialog.dart'; + +class EmergencyOverrideWrapper extends StatefulWidget { + final ProfileState profileState; + final bool isLoggedIn; + + const EmergencyOverrideWrapper({ + required this.profileState, + required this.isLoggedIn, + super.key, + }); + + @override + State createState() => + _EmergencyOverrideWrapperState(); +} + +class _EmergencyOverrideWrapperState extends State { + int _tapCount = 0; + DateTime? _lastTapTime; + bool _isOverrideActive = false; + + void _handleSecretTap() async { + final now = DateTime.now(); + if (_lastTapTime == null || + now.difference(_lastTapTime!) > const Duration(milliseconds: 500)) { + _tapCount = 1; + } else { + _tapCount++; + } + _lastTapTime = now; + + if (_tapCount == 5) { + _tapCount = 0; + try { + final isOn = await IotFlashlight.toggle(); + + debugPrint('\n========================================='); + debugPrint('🔦 [IOT_FLASHLIGHT_PLUGIN] Triggered!'); + debugPrint('- Hardware Flashlight State: ${isOn ? "ON" : "OFF"}'); + debugPrint('=========================================\n'); + + if (mounted) { + context.read().saveLog( + 'HARDWARE_OVERRIDE', + 'Physical optical emitter toggled.' + 'State: ${isOn ? "ON" : "OFF"}', + ); + } + + setState(() => _isOverrideActive = isOn); + + if (mounted) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + isOn + ? '🔦 OVERRIDE: HARDWARE LIGHT ACTIVE ' + : '✅ SYSTEM NORMALIZED: LIGHT OFF', + style: const TextStyle( + fontWeight: FontWeight.bold, + letterSpacing: 1, + ), + ), + backgroundColor: + isOn ? Colors.redAccent : const Color(0xFF4ADE80), + duration: const Duration(seconds: 2), + ), + ); + } + } catch (e) { + if (e.toString().contains('UNSUPPORTED_PLATFORM') && mounted) { + PlatformWarningDialog.show(context); + } else { + debugPrint('🔦 [FLASHLIGHT ERROR]: $e'); + } + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Stack( + children: [ + OverrideBackground(isOverrideActive: _isOverrideActive), + CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverAppBar( + expandedHeight: 100, + pinned: true, + backgroundColor: Colors.transparent, + flexibleSpace: FlexibleSpaceBar( + centerTitle: true, + title: Text( + _isOverrideActive ? 'SYSTEM OVERRIDE' : 'USER PROFILE', + style: TextStyle( + letterSpacing: 2, + fontSize: 16, + fontWeight: FontWeight.bold, + color: + _isOverrideActive ? Colors.redAccent : Colors.white, + ), + ), + ), + ), + SliverPadding( + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 20), + sliver: SliverList( + delegate: SliverChildListDelegate([ + GestureDetector( + onTap: _handleSecretTap, + behavior: HitTestBehavior.opaque, + child: ProfileHeader(state: widget.profileState), + ), + const SizedBox(height: 32), + ProfileSettings( + state: widget.profileState, + isLoggedIn: widget.isLoggedIn, + ), + const SizedBox(height: 40), + ]), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/profile/override_background.dart b/lib/widgets/profile/override_background.dart new file mode 100644 index 0000000..2635708 --- /dev/null +++ b/lib/widgets/profile/override_background.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/widgets/common/blur_blob.dart'; + +class OverrideBackground extends StatelessWidget { + final bool isOverrideActive; + + const OverrideBackground({required this.isOverrideActive, super.key}); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + final colorTop = isOverrideActive + ? Colors.redAccent.withValues(alpha: 0.15) + : const Color(0xFF38BDF8).withValues(alpha: 0.1); + + final colorBottom = isOverrideActive + ? Colors.orange.withValues(alpha: 0.15) + : const Color(0xFF4ADE80).withValues(alpha: 0.08); + + return Stack( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 500), + child: BlurBlob( + alignment: Alignment.topRight, + translation: const Offset(0.3, -0.3), + color: colorTop, + size: size.width * 0.7, + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 500), + child: BlurBlob( + alignment: Alignment.bottomLeft, + translation: const Offset(-0.3, 0.3), + color: colorBottom, + size: size.width * 0.8, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/profile/profile_header.dart b/lib/widgets/profile/profile_header.dart new file mode 100644 index 0000000..80bdb9f --- /dev/null +++ b/lib/widgets/profile/profile_header.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_flutter_iot/cubits/profile_cubit.dart'; +import 'package:mobile_flutter_iot/utils/profile_dialogs.dart'; + +class ProfileHeader extends StatelessWidget { + final ProfileState state; + const ProfileHeader({required this.state, super.key}); + + @override + Widget build(BuildContext context) { + final name = state.user?.fullName ?? 'Loading...'; + final dept = state.user?.department ?? 'Unknown Department'; + final email = state.user?.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), + ], + ), + ), + child: const CircleAvatar( + radius: 55, + backgroundColor: Color(0xFF0F172A), + child: CircleAvatar( + radius: 51, + backgroundColor: Color(0xFF1E293B), + child: Icon(Icons.person_rounded, size: 50, color: Colors.white), + ), + ), + ), + const SizedBox(height: 20), + GestureDetector( + onTap: () => ProfileDialogs.showEditField(context, 'Name', name), + child: Text( + name, + style: const TextStyle(fontSize: 26, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 4), + Text( + dept.toUpperCase(), + style: const TextStyle( + color: Color(0xFF4ADE80), + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () => ProfileDialogs.showEditField(context, 'Email', email), + 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), + ), + ), + ), + ], + ); + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:mobile_flutter_iot/blocs/profile/profile_state.dart'; +// import 'package:mobile_flutter_iot/utils/profile_dialogs.dart'; + +// class ProfileHeader extends StatelessWidget { +// final ProfileState state; +// const ProfileHeader({required this.state, super.key}); + +// @override +// Widget build(BuildContext context) { +// final name = state.user?.fullName ?? 'Loading...'; +// final dept = state.user?.department ?? 'Unknown Department'; +// final email = state.user?.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), +// ], +// ), +// ), +// child: const CircleAvatar( +// radius: 55, +// backgroundColor: Color(0xFF0F172A), +// child: CircleAvatar( +// radius: 51, +// backgroundColor: Color(0xFF1E293B), +// child: Icon(Icons.person_rounded, size: 50, color: Colors.white), +// ), +// ), +// ), +// const SizedBox(height: 20), +// GestureDetector( +// onTap: () => ProfileDialogs.showEditField(context, 'Name', name), +// child: Text( +// name, +// style: const TextStyle(fontSize: 26, fontWeight: FontWeight.bold), +// ), +// ), +// const SizedBox(height: 4), +// Text( +// dept.toUpperCase(), +// style: const TextStyle( +// color: Color(0xFF4ADE80), +// fontSize: 12, +// fontWeight: FontWeight.bold, +// ), +// ), +// const SizedBox(height: 12), +// GestureDetector( +// onTap: () => ProfileDialogs.showEditField(context, 'Email', email), +// 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), +// ), +// ), +// ), +// ], +// ); +// } +// } diff --git a/lib/widgets/profile_item.dart b/lib/widgets/profile/profile_item.dart similarity index 100% rename from lib/widgets/profile_item.dart rename to lib/widgets/profile/profile_item.dart diff --git a/lib/widgets/profile_option.dart b/lib/widgets/profile/profile_option.dart similarity index 100% rename from lib/widgets/profile_option.dart rename to lib/widgets/profile/profile_option.dart diff --git a/lib/widgets/profile/profile_settings.dart b/lib/widgets/profile/profile_settings.dart new file mode 100644 index 0000000..8fced55 --- /dev/null +++ b/lib/widgets/profile/profile_settings.dart @@ -0,0 +1,317 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_flutter_iot/cubits/profile_cubit.dart'; +import 'package:mobile_flutter_iot/utils/profile_dialogs.dart'; +import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +import 'package:mobile_flutter_iot/widgets/profile/profile_item.dart'; + +class ProfileSettings extends StatelessWidget { + final ProfileState state; + final bool isLoggedIn; + + const ProfileSettings({ + required this.state, + required this.isLoggedIn, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + GlassCard( + child: Row( + children: [ + Icon( + isLoggedIn + ? Icons.verified_user_rounded + : Icons.shield_moon_outlined, + color: + isLoggedIn ? const Color(0xFF4ADE80) : Colors.orangeAccent, + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Security Protocol', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + isLoggedIn + ? 'Persistent Session: Active' + : 'Session Encryption: Local Only', + style: const TextStyle(fontSize: 11, color: Colors.white38), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 24), + const Text( + 'PREFERENCES', + style: TextStyle( + color: Colors.white24, + fontSize: 11, + letterSpacing: 1.5, + ), + ), + const SizedBox(height: 12), + GlassCard( + child: Column( + children: [ + ProfileMenuItem( + icon: Icons.notifications_active_outlined, + title: 'System Alerts', + isSwitch: true, + value: state.notifications, + onChanged: (v) => + context.read().toggleNotifications(v), + ), + const Divider(color: Colors.white10), + ProfileMenuItem( + icon: Icons.palette_outlined, + title: 'OLED Dark Mode', + isSwitch: true, + value: state.darkMode, + onChanged: (v) => + context.read().toggleDarkMode(v), + ), + const Divider(color: Colors.white10), + ProfileMenuItem( + icon: Icons.hub_outlined, + title: 'Department Unit', + trailingText: state.user?.department ?? '...', + onTap: () => ProfileDialogs.showEditField( + context, + 'Department', + state.user?.department ?? '', + ), + ), + ], + ), + ), + const SizedBox(height: 32), + const Text( + 'ACCOUNT ACTIONS', + style: TextStyle( + color: Colors.white24, + fontSize: 11, + letterSpacing: 1.5, + ), + ), + const SizedBox(height: 12), + GlassCard( + padding: EdgeInsets.zero, + child: ListTile( + onTap: () => ProfileDialogs.showLogout(context), + 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, + ), + ), + ), + const SizedBox(height: 16), + GestureDetector( + onTap: () => ProfileDialogs.showDeleteAccount(context), + 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, + ), + ), + ), + ), + ), + ], + ); + } +} + +/* ============================================================ +[ADVANCED ARCHITECTURE OPTION: BLoC IMPLEMENTATION] +Щоб переключити екран на BLoC, розкоментуйте код нижче +та закоментуйте секцію з Cubit вище. +============================================================*/ + +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/profile/profile_bloc.dart'; +// import 'package:mobile_flutter_iot/blocs/profile/profile_event.dart'; +// import 'package:mobile_flutter_iot/blocs/profile/profile_state.dart'; +// import 'package:mobile_flutter_iot/utils/profile_dialogs.dart'; +// import 'package:mobile_flutter_iot/widgets/common/glass_card.dart'; +// import 'package:mobile_flutter_iot/widgets/profile/profile_item.dart'; + +// class ProfileSettings extends StatelessWidget { +// final ProfileState state; +// final bool isLoggedIn; + +// const ProfileSettings({ +// required this.state, +// required this.isLoggedIn, +// super.key, +// }); + +// @override +// Widget build(BuildContext context) { +// return Column( +// children: [ +// GlassCard( +// child: Row( +// children: [ +// Icon( +// isLoggedIn +// ? Icons.verified_user_rounded +// : Icons.shield_moon_outlined, +// color: +// isLoggedIn ? const Color(0xFF4ADE80) : Colors.orangeAccent, +// ), +// const SizedBox(width: 16), +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// const Text( +// 'Security Protocol', +// style: TextStyle(fontWeight: FontWeight.bold), +// ), +// Text( +// isLoggedIn +// ? 'Persistent Session: Active' +// : 'Session Encryption: Local Only', +// style: const TextStyle(fontSize: 11, color: Colors.white38), +// ), +// ], +// ), +// ], +// ), +// ), +// const SizedBox(height: 24), +// const Text( +// 'PREFERENCES', +// style: TextStyle( +// color: Colors.white24, +// fontSize: 11, +// letterSpacing: 1.5, +// ), +// ), +// const SizedBox(height: 12), +// GlassCard( +// child: Column( +// children: [ +// ProfileMenuItem( +// icon: Icons.notifications_active_outlined, +// title: 'System Alerts', +// isSwitch: true, +// +// value: false, +// onChanged: (v) => context +// .read() +// .add(ProfileToggleNotifications(v)), +// ), +// const Divider(color: Colors.white10), +// ProfileMenuItem( +// icon: Icons.palette_outlined, +// title: 'OLED Dark Mode', +// isSwitch: true, +// value: true, // Тимчасова заглушка +// onChanged: (v) => +// context.read().add(ProfileToggleDarkMode(v)), +// ), +// const Divider(color: Colors.white10), +// ProfileMenuItem( +// icon: Icons.hub_outlined, +// title: 'Department Unit', +// trailingText: state.user?.department ?? '...', +// onTap: () => ProfileDialogs.showEditField( +// context, +// 'Department', +// state.user?.department ?? '', +// ), +// ), +// ], +// ), +// ), +// const SizedBox(height: 32), +// const Text( +// 'ACCOUNT ACTIONS', +// style: TextStyle( +// color: Colors.white24, +// fontSize: 11, +// letterSpacing: 1.5, +// ), +// ), +// const SizedBox(height: 12), +// GlassCard( +// padding: EdgeInsets.zero, +// child: ListTile( +// onTap: () => ProfileDialogs.showLogout(context), +// 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, +// ), +// ), +// ), +// const SizedBox(height: 16), +// GestureDetector( +// onTap: () => ProfileDialogs.showDeleteAccount(context), +// 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/widgets/profile/warning_dialog.dart b/lib/widgets/profile/warning_dialog.dart new file mode 100644 index 0000000..c6c7d5a --- /dev/null +++ b/lib/widgets/profile/warning_dialog.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class PlatformWarningDialog { + static void show(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: const Color(0xFF1E293B), + title: const Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Colors.orange), + SizedBox(width: 10), + Text( + 'Platform Warning', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + content: const Text( + 'Hardware flashlight control is currently only supported' + 'on native Android nodes.', + style: TextStyle(color: Colors.white70), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text( + 'ACKNOWLEDGE', + style: TextStyle(color: Color(0xFF38BDF8)), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 660e5f2..f353b00 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: a48653a82055a900b88cd35f92429f068c5a8057ae9b136d197b3d56c57efb81 + url: "https://pub.dev" + source: hosted + version: "9.2.0" boolean_selector: dependency: transitive description: @@ -113,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" event_bus: dependency: transitive description: @@ -150,6 +166,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 + url: "https://pub.dev" + source: hosted + version: "9.1.1" flutter_lints: dependency: "direct dev" description: @@ -240,6 +264,15 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + iot_flashlight: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: "2df1880c52084089fd1158bbd089d2228a4f0de1" + url: "https://github.com/Art-Invis/iot_flashlight_plugin.git" + source: git + version: "0.0.1" js: dependency: transitive description: @@ -654,5 +687,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.3 <4.0.0" + dart: ">=3.11.0 <4.0.0" flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 424f4ca..3162557 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,7 +46,14 @@ dependencies: provider: ^6.1.1 dio: ^5.4.1 - + flutter_bloc: ^9.1.1 + equatable: ^2.0.8 + + iot_flashlight: + git: + url: https://github.com/Art-Invis/iot_flashlight_plugin.git + ref: main + dev_dependencies: flutter_test: sdk: flutter diff --git a/test/widget_test.dart b/test/widget_test.dart index 7187fc6..be96db3 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,23 +5,41 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile_flutter_iot/cubits/auth_cubit.dart'; +import 'package:mobile_flutter_iot/cubits/mqtt_cubit.dart'; import 'package:mobile_flutter_iot/main.dart'; -import 'package:mobile_flutter_iot/providers/auth_provider.dart'; -import 'package:mobile_flutter_iot/providers/mqtt_provider.dart'; -import 'package:provider/provider.dart'; +import 'package:mobile_flutter_iot/repository/local_user_repository.dart'; +import 'package:mobile_flutter_iot/services/api_service.dart'; void main() { testWidgets('Auth screens load test', (WidgetTester tester) async { - final authProvider = AuthProvider(); + final apiService = ApiService(); + final localUserRepository = LocalUserRepository(); await tester.pumpWidget( - MultiProvider( + MultiRepositoryProvider( providers: [ - ChangeNotifierProvider.value(value: authProvider), - ChangeNotifierProvider(create: (_) => MqttProvider()), + RepositoryProvider.value(value: apiService), + RepositoryProvider.value(value: localUserRepository), ], - child: const SmartWorkspaceApp(), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => AuthCubit( + apiService: context.read(), + userRepository: context.read(), + ), + ), + BlocProvider( + create: (context) => MqttCubit( + apiService: context.read(), + ), + ), + ], + child: const SmartWorkspaceApp(initialRoute: '/login'), + ), ), );