diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 0b61e4e7..a86b49c5 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -13,6 +13,7 @@ import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; import 'package:syncrow_web/services/routines_api.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/snack_bar.dart'; import 'package:uuid/uuid.dart'; part 'routine_event.dart'; @@ -44,16 +45,17 @@ class RoutineBloc extends Bloc { on(_fetchDevices); on(_onUpdateScene); on(_onUpdateAutomation); - on(_onSetAutomationActionExecutor); on(_triggerSwitchTabsEvent); on(_createNewRoutineViewEvent); + on(_resetErrorMessage); } FutureOr _triggerSwitchTabsEvent( TriggerSwitchTabsEvent event, Emitter emit, ) { - emit(state.copyWith(routineTab: event.isRoutineTab, createRoutineView: false)); + emit(state.copyWith( + routineTab: event.isRoutineTab, createRoutineView: false)); add(ResetRoutineState()); if (event.isRoutineTab) { add(const LoadScenes(spaceId, communityId)); @@ -61,6 +63,13 @@ class RoutineBloc extends Bloc { } } + _resetErrorMessage( + ResetErrorMessage event, + Emitter emit, + ) { + emit(state.copyWith(errorMessage: '')); + } + FutureOr _createNewRoutineViewEvent( CreateNewRoutineViewEvent event, Emitter emit, @@ -72,8 +81,8 @@ class RoutineBloc extends Bloc { final updatedIfItems = List>.from(state.ifItems); // Find the index of the item in teh current itemsList - int index = - updatedIfItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + int index = updatedIfItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); // Replace the map if the index is valid if (index != -1) { updatedIfItems[index] = event.item; @@ -82,18 +91,21 @@ class RoutineBloc extends Bloc { } if (event.isTabToRun) { - emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: true, isAutomation: false)); + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: true, isAutomation: false)); } else { - emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: false, isAutomation: true)); + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: false, isAutomation: true)); } } - void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { + void _onAddToThenContainer( + AddToThenContainer event, Emitter emit) { final currentItems = List>.from(state.thenItems); // Find the index of the item in teh current itemsList - int index = - currentItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + int index = currentItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); // Replace the map if the index is valid if (index != -1) { currentItems[index] = event.item; @@ -104,22 +116,26 @@ class RoutineBloc extends Bloc { emit(state.copyWith(thenItems: currentItems)); } - void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter emit) { + void _onAddFunctionsToRoutine( + AddFunctionToRoutine event, Emitter emit) { try { if (event.functions.isEmpty) return; - List selectedFunction = List.from(event.functions); + List selectedFunction = + List.from(event.functions); Map> currentSelectedFunctions = Map>.from(state.selectedFunctions); if (currentSelectedFunctions.containsKey(event.uniqueCustomId)) { List currentFunctions = - List.from(currentSelectedFunctions[event.uniqueCustomId] ?? []); + List.from( + currentSelectedFunctions[event.uniqueCustomId] ?? []); List functionCode = []; for (int i = 0; i < selectedFunction.length; i++) { for (int j = 0; j < currentFunctions.length; j++) { - if (selectedFunction[i].functionCode == currentFunctions[j].functionCode) { + if (selectedFunction[i].functionCode == + currentFunctions[j].functionCode) { currentFunctions[j] = selectedFunction[i]; if (!functionCode.contains(currentFunctions[j].functionCode)) { functionCode.add(currentFunctions[j].functionCode); @@ -129,13 +145,15 @@ class RoutineBloc extends Bloc { } for (int i = 0; i < functionCode.length; i++) { - selectedFunction.removeWhere((code) => code.functionCode == functionCode[i]); + selectedFunction + .removeWhere((code) => code.functionCode == functionCode[i]); } - currentSelectedFunctions[event.uniqueCustomId] = List.from(currentFunctions) - ..addAll(selectedFunction); + currentSelectedFunctions[event.uniqueCustomId] = + List.from(currentFunctions)..addAll(selectedFunction); } else { - currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions); + currentSelectedFunctions[event.uniqueCustomId] = + List.from(event.functions); } emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); @@ -144,11 +162,13 @@ class RoutineBloc extends Bloc { } } - Future _onLoadScenes(LoadScenes event, Emitter emit) async { + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { - final scenes = await SceneApi.getScenesByUnitId(event.unitId, event.communityId); + final scenes = + await SceneApi.getScenesByUnitId(event.unitId, event.communityId); emit(state.copyWith( scenes: scenes, isLoading: false, @@ -163,7 +183,8 @@ class RoutineBloc extends Bloc { } } - Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { + Future _onLoadAutomation( + LoadAutomation event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { @@ -191,41 +212,45 @@ class RoutineBloc extends Bloc { } } - FutureOr _onSearchRoutines(SearchRoutines event, Emitter emit) async { + FutureOr _onSearchRoutines( + SearchRoutines event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); await Future.delayed(const Duration(seconds: 1)); emit(state.copyWith(isLoading: false, errorMessage: null)); emit(state.copyWith(searchText: event.query)); } - FutureOr _onAddSelectedIcon(AddSelectedIcon event, Emitter emit) { + FutureOr _onAddSelectedIcon( + AddSelectedIcon event, Emitter emit) { emit(state.copyWith(selectedIcon: event.icon)); } - bool _isFirstActionDelay(List> actions) { - if (actions.isEmpty) return false; - return actions.first['deviceId'] == 'delay'; - } + // bool _isFirstActionDelay(List> actions) { + // if (actions.isEmpty) return false; + // return actions.first['deviceId'] == 'delay'; + // } bool _isLastActionDelay(List> actions) { if (actions.isEmpty) return false; return actions.last['deviceId'] == 'delay'; } - Future _onCreateScene(CreateSceneEvent event, Emitter emit) async { + Future _onCreateScene( + CreateSceneEvent event, Emitter emit) async { try { // Check if first action is delay - if (_isFirstActionDelay(state.thenItems)) { - emit(state.copyWith( - errorMessage: 'Cannot have delay as the first action', - isLoading: false, - )); - return; - } + // if (_isFirstActionDelay(state.thenItems)) { + // emit(state.copyWith( + // errorMessage: 'Cannot have delay as the first action', + // isLoading: false, + // )); + // return; + // } if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'Cannot have delay as the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); return; @@ -238,6 +263,7 @@ class RoutineBloc extends Bloc { return functions.map((function) { if (function.functionCode == 'automation') { return CreateSceneAction( + actionType: 'automation', entityId: function.entityId, actionExecutor: function.value, executorProperty: null, @@ -257,6 +283,7 @@ class RoutineBloc extends Bloc { } return CreateSceneAction( + actionType: 'scene', entityId: function.entityId, actionExecutor: 'device_issue', executorProperty: CreateSceneExecutorProperty( @@ -296,27 +323,33 @@ class RoutineBloc extends Bloc { } } - Future _onCreateAutomation(CreateAutomationEvent event, Emitter emit) async { + Future _onCreateAutomation( + CreateAutomationEvent event, Emitter emit) async { try { if (state.routineName == null || state.routineName!.isEmpty) { emit(state.copyWith( errorMessage: 'Automation name is required', )); + CustomSnackBar.redSnackBar('Automation name is required'); return; } - if (_isFirstActionDelay(state.thenItems)) { - emit(state.copyWith( - errorMessage: 'Cannot have delay as the first action', - isLoading: false, - )); - return; - } + // if (_isFirstActionDelay(state.thenItems)) { + // emit(state.copyWith( + // errorMessage: 'Cannot have delay as the first action', + // isLoading: false, + // )); + // CustomSnackBar.redSnackBar('Cannot have delay as the first action'); + + // return; + // } if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'Cannot have delay as the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); + CustomSnackBar.redSnackBar('Cannot have delay as the last action'); return; } emit(state.copyWith(isLoading: true, errorMessage: null)); @@ -349,12 +382,25 @@ class RoutineBloc extends Bloc { } final actions = state.thenItems.expand((item) { + if (item['type'] == 'tap_to_run') { + return [ + AutomationAction( + actionType: 'scene', + entityId: item['deviceId'], + actionExecutor: 'rule_trigger', + ), + ]; + } + final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; + return functions.map((function) { if (function.functionCode == 'automation') { return AutomationAction( + actionType: 'automation', entityId: function.entityId, actionExecutor: function.value, + executorProperty: null, ); } @@ -402,26 +448,32 @@ class RoutineBloc extends Bloc { isLoading: false, errorMessage: result['message'], )); + CustomSnackBar.redSnackBar('Something went wrong'); } } catch (e) { emit(state.copyWith( isLoading: false, errorMessage: 'Something went wrong', )); + CustomSnackBar.redSnackBar('Something went wrong'); } } - FutureOr _onRemoveDragCard(RemoveDragCard event, Emitter emit) { + FutureOr _onRemoveDragCard( + RemoveDragCard event, Emitter emit) { if (event.isFromThen) { final thenItems = List>.from(state.thenItems); - final selectedFunctions = Map>.from(state.selectedFunctions); + final selectedFunctions = + Map>.from(state.selectedFunctions); thenItems.removeAt(event.index); selectedFunctions.remove(event.key); - emit(state.copyWith(thenItems: thenItems, selectedFunctions: selectedFunctions)); + emit(state.copyWith( + thenItems: thenItems, selectedFunctions: selectedFunctions)); } else { final ifItems = List>.from(state.ifItems); - final selectedFunctions = Map>.from(state.selectedFunctions); + final selectedFunctions = + Map>.from(state.selectedFunctions); ifItems.removeAt(event.index); selectedFunctions.remove(event.key); @@ -432,7 +484,8 @@ class RoutineBloc extends Bloc { isAutomation: false, isTabToRun: false)); } else { - emit(state.copyWith(ifItems: ifItems, selectedFunctions: selectedFunctions)); + emit(state.copyWith( + ifItems: ifItems, selectedFunctions: selectedFunctions)); } } } @@ -444,18 +497,23 @@ class RoutineBloc extends Bloc { )); } - FutureOr _onEffectiveTimeEvent(EffectiveTimePeriodEvent event, Emitter emit) { + FutureOr _onEffectiveTimeEvent( + EffectiveTimePeriodEvent event, Emitter emit) { emit(state.copyWith(effectiveTime: event.effectiveTime)); } - FutureOr _onSetRoutineName(SetRoutineName event, Emitter emit) { + FutureOr _onSetRoutineName( + SetRoutineName event, Emitter emit) { emit(state.copyWith( routineName: event.name, )); } - (List>, List>, Map>) - _createCardData( + ( + List>, + List>, + Map> + ) _createCardData( List actions, List? conditions, Map> currentFunctions, @@ -488,7 +546,8 @@ class RoutineBloc extends Bloc { 'deviceId': condition.entityId, 'title': matchingDevice.name ?? condition.entityId, 'productType': condition.entityType, - 'imagePath': matchingDevice.getDefaultIcon(condition.entityType), + 'imagePath': + matchingDevice.getDefaultIcon(condition.entityType), }; final functions = matchingDevice.functions; @@ -524,8 +583,11 @@ class RoutineBloc extends Bloc { final cardData = { 'entityId': action.entityId, 'uniqueCustomId': const Uuid().v4(), - 'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId, - 'title': action.actionExecutor == 'delay' ? 'Delay' : (matchingDevice.name ?? 'Device'), + 'deviceId': + action.actionExecutor == 'delay' ? 'delay' : action.entityId, + 'title': action.actionExecutor == 'delay' + ? 'Delay' + : (matchingDevice.name ?? 'Device'), 'productType': action.productType, 'imagePath': matchingDevice.getDefaultIcon(action.productType), }; @@ -568,7 +630,8 @@ class RoutineBloc extends Bloc { return (thenItems, ifItems, currentFunctions); } - Future _onGetSceneDetails(GetSceneDetails event, Emitter emit) async { + Future _onGetSceneDetails( + GetSceneDetails event, Emitter emit) async { try { emit(state.copyWith( isLoading: true, @@ -616,10 +679,12 @@ class RoutineBloc extends Bloc { if (!deviceCards.containsKey(deviceId)) { deviceCards[deviceId] = { 'entityId': action.entityId, - 'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId, - 'uniqueCustomId': action.type == 'automation' || action.actionExecutor == 'delay' - ? const Uuid().v4() - : action.entityId, + 'deviceId': + action.actionExecutor == 'delay' ? 'delay' : action.entityId, + 'uniqueCustomId': + action.type == 'automation' || action.actionExecutor == 'delay' + ? const Uuid().v4() + : action.entityId, 'title': action.actionExecutor == 'delay' ? 'Delay' : action.type == 'automation' @@ -653,8 +718,9 @@ class RoutineBloc extends Bloc { operationName: 'Automation', ), ); - emit(state.copyWith(automationActionExecutor: action.actionExecutor)); - } else if (action.executorProperty != null && action.actionExecutor != 'delay') { + // emit(state.copyWith(automationActionExecutor: action.actionExecutor)); + } else if (action.executorProperty != null && + action.actionExecutor != 'delay') { if (!updatedFunctions.containsKey(uniqueCustomId)) { updatedFunctions[uniqueCustomId] = []; } @@ -726,153 +792,8 @@ class RoutineBloc extends Bloc { } } - Future _onGetAutomationDetails( - GetAutomationDetails event, Emitter emit) async { - try { - emit(state.copyWith( - isLoading: true, - isUpdate: true, - isTabToRun: false, - automationId: event.automationId, - isAutomation: true, - ifItems: [], - thenItems: [], - )); - - final automationDetails = await SceneApi.getAutomationDetails(event.automationId); - - final List> thenItems; - final List> ifItems; - final Map> updatedFunctions = - Map>.from(state.selectedFunctions); - - ifItems = automationDetails.conditions?.map((condition) { - late AllDevicesModel? matchingDevice; - for (var device in state.devices) { - if (device.uuid == condition.entityId) { - matchingDevice = device; - break; - } - } - - final cardData = { - 'entityId': condition.entityId, - 'uniqueCustomId': const Uuid().v4(), - 'deviceId': condition.expr.statusCode == 'delay' ? 'delay' : condition.entityId, - 'title': condition.expr.statusCode == 'delay' - ? 'Delay' - : (matchingDevice?.name ?? 'Device'), - 'productType': condition.productType, - 'functions': matchingDevice?.functions ?? [], - 'imagePath': matchingDevice?.getDefaultIcon(condition.productType) ?? '', - 'device': matchingDevice, - }; - - final functions = matchingDevice?.functions ?? []; - for (var function in functions) { - if (function.code == condition.expr.statusCode) { - updatedFunctions[cardData['uniqueCustomId'].toString()] = [ - DeviceFunctionData( - entityId: condition.entityId, - functionCode: condition.expr.statusCode, - value: condition.expr.statusValue, - operationName: function.operationName, - ), - ]; - break; - } - } - - return cardData; - }).toList() ?? - []; - - // Create then items from actions - thenItems = automationDetails.actions.map((action) { - final matchingDevice = state.devices.firstWhere( - (device) => device.uuid == action.entityId, - orElse: () => AllDevicesModel( - uuid: action.entityId, - name: action.entityId, - productType: action.productType, - ), - ); - - final cardData = { - 'entityId': action.entityId, - 'uniqueCustomId': const Uuid().v4(), - 'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId, - 'title': action.actionExecutor == 'delay' ? 'Delay' : (matchingDevice.name ?? 'Device'), - 'productType': action.productType, - 'functions': matchingDevice.functions, - 'imagePath': matchingDevice.getDefaultIcon(action.productType), - 'device': matchingDevice, - }; - - if (action.executorProperty != null && action.actionExecutor != 'delay') { - final functions = matchingDevice.functions; - final functionCode = action.executorProperty!.functionCode; - for (var function in functions) { - if (function.code == functionCode) { - updatedFunctions[cardData['uniqueCustomId'].toString()] = [ - DeviceFunctionData( - entityId: action.entityId, - functionCode: functionCode ?? '', - value: action.executorProperty!.functionValue, - operationName: function.operationName, - ), - ]; - break; - } - } - } else if (action.actionExecutor == 'delay') { - final delayFunction = DelayFunction( - deviceId: action.entityId, - deviceName: 'Delay', - ); - updatedFunctions[cardData['uniqueCustomId'].toString()] = [ - DeviceFunctionData( - entityId: action.entityId, - functionCode: 'delay', - value: action.executorProperty?.delaySeconds ?? 0, - operationName: delayFunction.operationName, - ), - ]; - } else if (action.actionExecutor == 'rule_disable' || - action.actionExecutor == 'rule_enable') { - updatedFunctions[cardData['uniqueCustomId'].toString()] = [ - DeviceFunctionData( - entityId: action.entityId, - functionCode: 'automation', - value: action.actionExecutor, - operationName: action.name ?? 'Automation', - ), - ]; - } - - return cardData; - }).toList(); - - emit(state.copyWith( - isLoading: false, - routineName: automationDetails.name, - selectedAutomationOperator: automationDetails.decisionExpr, - effectiveTime: automationDetails.effectiveTime, - isAutomation: true, - thenItems: thenItems, - ifItems: ifItems, - selectedFunctions: updatedFunctions, - automationId: automationDetails.automationId, - )); - } catch (e) { - emit(state.copyWith( - isLoading: false, - errorMessage: 'Failed to load automation details: $e', - )); - } - } - - FutureOr _onResetRoutineState(ResetRoutineState event, Emitter emit) { + FutureOr _onResetRoutineState( + ResetRoutineState event, Emitter emit) { emit(state.copyWith( ifItems: [], thenItems: [], @@ -900,7 +821,8 @@ class RoutineBloc extends Bloc { if (state.isTabToRun) { SceneApi.deleteScene(unitUuid: spaceId, sceneId: state.sceneId ?? ''); } else { - SceneApi.deleteAutomation(unitUuid: spaceId, automationId: state.automationId ?? ''); + SceneApi.deleteAutomation( + unitUuid: spaceId, automationId: state.automationId ?? ''); } add(const LoadScenes(spaceId, communityId)); @@ -929,7 +851,8 @@ class RoutineBloc extends Bloc { // } // } - FutureOr _fetchDevices(FetchDevicesInRoutine event, Emitter emit) async { + FutureOr _fetchDevices( + FetchDevicesInRoutine event, Emitter emit) async { emit(state.copyWith(isLoading: true)); try { final devices = await DevicesManagementApi().fetchDevices(); @@ -940,21 +863,23 @@ class RoutineBloc extends Bloc { } } - FutureOr _onUpdateScene(UpdateScene event, Emitter emit) async { + FutureOr _onUpdateScene( + UpdateScene event, Emitter emit) async { try { // Check if first action is delay - if (_isFirstActionDelay(state.thenItems)) { - emit(state.copyWith( - errorMessage: 'Cannot have delay as the first action', - isLoading: false, - )); + // if (_isFirstActionDelay(state.thenItems)) { + // emit(state.copyWith( + // errorMessage: 'Cannot have delay as the first action', + // isLoading: false, + // )); - return; - } + // return; + // } if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'Cannot have delay as the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); return; @@ -967,6 +892,7 @@ class RoutineBloc extends Bloc { return functions.map((function) { if (function.functionCode == 'automation') { return CreateSceneAction( + actionType: 'automation', entityId: function.entityId, actionExecutor: function.value, executorProperty: null, @@ -1006,7 +932,8 @@ class RoutineBloc extends Bloc { actions: actions, ); - final result = await SceneApi.updateScene(createSceneModel, state.sceneId ?? ''); + final result = + await SceneApi.updateScene(createSceneModel, state.sceneId ?? ''); if (result['success']) { add(ResetRoutineState()); add(const LoadScenes(spaceId, communityId)); @@ -1025,72 +952,125 @@ class RoutineBloc extends Bloc { } } - FutureOr _onUpdateAutomation(UpdateAutomation event, Emitter emit) async { - if (_isFirstActionDelay(state.thenItems)) { - emit(state.copyWith( - errorMessage: 'Cannot have delay as the first action', - isLoading: false, - )); - return; - } - emit(state.copyWith(isLoading: true)); + FutureOr _onUpdateAutomation( + UpdateAutomation event, Emitter emit) async { try { - final conditions = state.ifItems - .map((item) { - final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; - if (functions.isEmpty) return null; + if (state.routineName == null || state.routineName!.isEmpty) { + emit(state.copyWith( + errorMessage: 'Automation name is required', + )); + return; + } + // if (_isFirstActionDelay(state.thenItems)) { + // emit(state.copyWith( + // errorMessage: 'Cannot have delay as the first action', + // isLoading: false, + // )); + // return; + // } - final function = functions.first; - return Condition( - code: state.ifItems.indexOf(item) + 1, + if (_isLastActionDelay(state.thenItems)) { + emit(state.copyWith( + errorMessage: 'Cannot have delay as the last action', + isLoading: false, + )); + return; + } + emit(state.copyWith(isLoading: true, errorMessage: null)); + + final conditions = state.ifItems.expand((item) { + final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; + return functions.map((function) { + return Condition( + code: state.selectedFunctions[item['uniqueCustomId']]!.indexOf( + function, + ) + + 1, + entityId: function.entityId, + entityType: 'device_report', + expr: ConditionExpr( + statusCode: function.functionCode, + comparator: function.condition ?? '==', + statusValue: function.value, + ), + ); + }); + }).toList(); + + if (conditions.isEmpty) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'At least one condition is required', + )); + return; + } + + final actions = state.thenItems.expand((item) { + if (item['type'] == 'tap_to_run' || item['type'] == 'scene') { + return [ + AutomationAction( + actionType: 'scene', + entityId: item['deviceId'], + actionExecutor: 'rule_trigger', + ), + ]; + } + + final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; + + return functions.map((function) { + if (function.functionCode == 'automation') { + return AutomationAction( + actionType: 'automation', entityId: function.entityId, - entityType: item['productType'], - expr: ConditionExpr( - statusCode: function.functionCode, - statusValue: function.value, - comparator: function.condition ?? '==', - ), + actionExecutor: function.value, + executorProperty: null, ); - }) - .whereType() - .toList(); + } - final actions = state.thenItems - .map((item) { - final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; - if (functions.isEmpty) return null; - - final function = functions.first; + if (item['deviceId'] == 'delay') { return AutomationAction( entityId: function.entityId, - actionExecutor: function.actionExecutor, + actionType: 'automation', + actionExecutor: 'delay', executorProperty: ExecutorProperty( - functionCode: function.functionCode, - functionValue: function.value, - delaySeconds: - function.functionCode == 'delay' ? (function.value as num).toInt() : null, + delaySeconds: int.tryParse(function.value.toString()) ?? 0, ), ); - }) - .whereType() - .toList(); + } + + return AutomationAction( + entityId: function.entityId, + actionExecutor: 'device_issue', + actionType: 'automation', + executorProperty: ExecutorProperty( + functionCode: function.functionCode, + functionValue: function.value, + ), + ); + }); + }).toList(); final createAutomationModel = CreateAutomationModel( spaceUuid: spaceId, automationName: state.routineName ?? '', decisionExpr: state.selectedAutomationOperator, - effectiveTime: state.effectiveTime ?? EffectiveTime(start: '', end: '', loops: ''), + effectiveTime: EffectiveTime( + start: state.effectiveTime?.start ?? '00:00', + end: state.effectiveTime?.end ?? '23:59', + loops: state.effectiveTime?.loops ?? '1111111', + ), conditions: conditions, actions: actions, ); - final result = - await SceneApi.updateAutomation(createAutomationModel, state.automationId ?? ''); + final result = await SceneApi.updateAutomation( + createAutomationModel, state.automationId ?? ''); if (result['success']) { add(ResetRoutineState()); - add(const LoadScenes(spaceId, communityId)); add(const LoadAutomation(spaceId)); + add(const LoadScenes(spaceId, communityId)); } else { emit(state.copyWith( isLoading: false, @@ -1100,13 +1080,198 @@ class RoutineBloc extends Bloc { } catch (e) { emit(state.copyWith( isLoading: false, - errorMessage: 'Failed to update automation: $e', + errorMessage: 'Something went wrong', )); } } - FutureOr _onSetAutomationActionExecutor( - SetAutomationActionExecutor event, Emitter emit) { - emit(state.copyWith(automationActionExecutor: event.automationActionExecutor)); + Future _onGetAutomationDetails( + GetAutomationDetails event, Emitter emit) async { + try { + emit(state.copyWith( + isLoading: true, + isUpdate: true, + isTabToRun: false, + automationId: event.automationId, + isAutomation: true, + ifItems: [], + thenItems: [], + )); + + final automationDetails = + await SceneApi.getAutomationDetails(event.automationId); + + final Map> deviceCards = {}; + final Map> updatedFunctions = + Map>.from(state.selectedFunctions); + + for (RoutineCondition condition in automationDetails.conditions ?? []) { + AllDevicesModel? matchingDevice = state.devices.firstWhere( + (device) => device.uuid == condition.entityId, + orElse: () => AllDevicesModel( + uuid: condition.entityId, + name: condition.entityId, + productType: condition.productType, + ), + ); + + final deviceId = condition.entityId; + + if (!deviceCards.containsKey(deviceId)) { + deviceCards[deviceId] = { + 'entityId': condition.entityId, + 'deviceId': condition.entityId, + 'uniqueCustomId': const Uuid().v4(), + 'title': matchingDevice.name ?? 'Device', + 'productType': condition.productType, + 'functions': matchingDevice.functions, + 'imagePath': matchingDevice.getDefaultIcon(condition.productType), + 'device': matchingDevice, + 'type': 'condition', + }; + } + + final cardData = deviceCards[deviceId]!; + final uniqueCustomId = cardData['uniqueCustomId'].toString(); + + if (!updatedFunctions.containsKey(uniqueCustomId)) { + updatedFunctions[uniqueCustomId] = []; + } + + final functions = matchingDevice.functions; + for (var function in functions) { + if (function.code == condition.expr.statusCode) { + updatedFunctions[uniqueCustomId]!.add( + DeviceFunctionData( + entityId: condition.entityId, + functionCode: condition.expr.statusCode, + value: condition.expr.statusValue, + operationName: function.operationName, + ), + ); + break; + } + } + } + + // Process actions (thenItems) + for (var action in automationDetails.actions) { + AllDevicesModel? matchingDevice = state.devices.firstWhere( + (device) => device.uuid == action.entityId, + orElse: () => AllDevicesModel( + uuid: action.entityId, + name: action.entityId, + productType: action.productType, + ), + ); + + final deviceId = action.actionExecutor == 'delay' + ? '${action.entityId}_delay' + : action.entityId; + + if (!deviceCards.containsKey(deviceId)) { + deviceCards[deviceId] = { + 'entityId': action.entityId, + 'deviceId': + action.actionExecutor == 'delay' ? 'delay' : action.entityId, + 'uniqueCustomId': const Uuid().v4(), + 'title': action.actionExecutor == 'delay' + ? 'Delay' + : (action.type == 'scene' || action.type == 'automation') + ? action.name + : (matchingDevice.name ?? 'Device'), + 'productType': action.productType, + 'functions': matchingDevice.functions, + 'imagePath': action.actionExecutor == 'delay' + ? Assets.delay + : action.type == 'automation' + ? Assets.automation + : matchingDevice.getDefaultIcon(action.productType), + 'device': matchingDevice, + 'type': action.type == 'scene' + ? 'scene' + : action.type == 'automation' + ? 'automation' + : 'action', + }; + } + + final cardData = deviceCards[deviceId]!; + final uniqueCustomId = cardData['uniqueCustomId'].toString(); + + if (!updatedFunctions.containsKey(uniqueCustomId)) { + updatedFunctions[uniqueCustomId] = []; + } + + if (action.executorProperty != null && + action.actionExecutor != 'delay') { + final functions = matchingDevice.functions; + final functionCode = action.executorProperty!.functionCode; + for (var function in functions) { + if (function.code == functionCode) { + updatedFunctions[uniqueCustomId]!.add( + DeviceFunctionData( + entityId: action.entityId, + functionCode: functionCode ?? '', + value: action.executorProperty!.functionValue, + operationName: function.operationName, + ), + ); + break; + } + } + } else if (action.actionExecutor == 'delay') { + final delayFunction = DelayFunction( + deviceId: action.entityId, + deviceName: 'Delay', + ); + updatedFunctions[uniqueCustomId]!.add( + DeviceFunctionData( + entityId: action.entityId, + functionCode: 'delay', + value: action.executorProperty?.delaySeconds ?? 0, + operationName: delayFunction.operationName, + ), + ); + } else if (action.actionExecutor == 'rule_disable' || + action.actionExecutor == 'rule_enable') { + updatedFunctions[uniqueCustomId]!.add( + DeviceFunctionData( + entityId: action.entityId, + functionCode: 'automation', + value: action.actionExecutor, + operationName: action.name ?? 'Automation', + ), + ); + } + } + + final ifItems = deviceCards.values + .where((card) => card['type'] == 'condition') + .toList(); + final thenItems = deviceCards.values + .where((card) => + card['type'] == 'action' || + card['type'] == 'automation' || + card['type'] == 'scene') + .toList(); + + emit(state.copyWith( + isLoading: false, + routineName: automationDetails.name, + selectedAutomationOperator: automationDetails.decisionExpr, + effectiveTime: automationDetails.effectiveTime, + isAutomation: true, + thenItems: thenItems, + ifItems: ifItems, + selectedFunctions: updatedFunctions, + automationId: automationDetails.automationId, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Failed to load automation details: $e', + )); + } } } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart index 1f703c92..f3d35eb8 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -206,3 +206,5 @@ class FetchDevicesInRoutine extends RoutineEvent {} class ResetRoutineState extends RoutineEvent {} class ClearFunctions extends RoutineEvent {} + +class ResetErrorMessage extends RoutineEvent {} diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart index e7dce718..4dc26a01 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart @@ -22,7 +22,7 @@ class RoutineState extends Equatable { final String? automationId; final bool? isUpdate; final List devices; - final String? automationActionExecutor; + // final String? automationActionExecutor; final bool routineTab; final bool createRoutineView; @@ -48,7 +48,7 @@ class RoutineState extends Equatable { this.automationId, this.isUpdate, this.devices = const [], - this.automationActionExecutor, + // this.automationActionExecutor, this.routineTab = false, this.createRoutineView = false}); @@ -73,7 +73,7 @@ class RoutineState extends Equatable { String? automationId, bool? isUpdate, List? devices, - String? automationActionExecutor, + // String? automationActionExecutor, TextEditingController? nameController, bool? routineTab, bool? createRoutineView, @@ -88,18 +88,21 @@ class RoutineState extends Equatable { errorMessage: errorMessage ?? this.errorMessage, routineName: routineName ?? this.routineName, selectedIcon: selectedIcon ?? this.selectedIcon, - loadScenesErrorMessage: loadScenesErrorMessage ?? this.loadScenesErrorMessage, - loadAutomationErrorMessage: loadAutomationErrorMessage ?? this.loadAutomationErrorMessage, + loadScenesErrorMessage: + loadScenesErrorMessage ?? this.loadScenesErrorMessage, + loadAutomationErrorMessage: + loadAutomationErrorMessage ?? this.loadAutomationErrorMessage, searchText: searchText ?? this.searchText, isTabToRun: isTabToRun ?? this.isTabToRun, isAutomation: isAutomation ?? this.isAutomation, - selectedAutomationOperator: selectedAutomationOperator ?? this.selectedAutomationOperator, + selectedAutomationOperator: + selectedAutomationOperator ?? this.selectedAutomationOperator, effectiveTime: effectiveTime ?? this.effectiveTime, sceneId: sceneId ?? this.sceneId, automationId: automationId ?? this.automationId, isUpdate: isUpdate ?? this.isUpdate, devices: devices ?? this.devices, - automationActionExecutor: automationActionExecutor ?? this.automationActionExecutor, + // automationActionExecutor: automationActionExecutor ?? this.automationActionExecutor, routineTab: routineTab ?? this.routineTab, createRoutineView: createRoutineView ?? this.createRoutineView); } @@ -126,7 +129,7 @@ class RoutineState extends Equatable { automationId, isUpdate, devices, - automationActionExecutor, + // automationActionExecutor, routineTab, createRoutineView ]; diff --git a/lib/pages/routiens/helper/save_routine_helper.dart b/lib/pages/routiens/helper/save_routine_helper.dart index 3bd5a5d2..6a725145 100644 --- a/lib/pages/routiens/helper/save_routine_helper.dart +++ b/lib/pages/routiens/helper/save_routine_helper.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -6,6 +8,7 @@ import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SaveRoutineHelper { static Future showSaveRoutineDialog(BuildContext context) async { @@ -98,18 +101,29 @@ class SaveRoutineHelper { final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; return ListTile( - leading: SvgPicture.asset( - item['imagePath'], - width: 22, - height: 22, + leading: item['type'] == 'tap_to_run' + ? Image.memory( + base64Decode(item['icon']), + width: 22, + height: 22, + ) + : SvgPicture.asset( + item['imagePath'], + width: 22, + height: 22, + ), + title: Text( + item['title'], + style: context.textTheme.bodySmall?.copyWith( + fontSize: 14, + color: ColorsManager.grayColor, + ), ), - title: - Text(item['title'], style: const TextStyle(fontSize: 14)), subtitle: Wrap( children: functions .map((f) => Text( '${f.operationName}: ${f.value}, ', - style: const TextStyle( + style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.grayColor, fontSize: 8), overflow: TextOverflow.ellipsis, maxLines: 3, @@ -124,17 +138,17 @@ class SaveRoutineHelper { ], ), ), - if (state.errorMessage != null || state.errorMessage!.isNotEmpty) - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - state.errorMessage!, - style: const TextStyle(color: Colors.red), - ), - ), + // if (state.errorMessage != null || state.errorMessage!.isNotEmpty) + // Padding( + // padding: const EdgeInsets.all(8.0), + // child: Text( + // state.errorMessage!, + // style: const TextStyle(color: Colors.red), + // ), + // ), DialogFooter( onCancel: () => Navigator.pop(context), - onConfirm: () { + onConfirm: () async { if (state.isAutomation) { if (state.automationId != null) { context.read().add(const UpdateAutomation()); @@ -148,10 +162,9 @@ class SaveRoutineHelper { context.read().add(const CreateSceneEvent()); } } - if (context.read().state.errorMessage == null || - context.read().state.errorMessage!.isEmpty) { - Navigator.pop(context); - } + // if (state.errorMessage == null || state.errorMessage!.isEmpty) { + Navigator.pop(context); + // } }, isConfirmEnabled: true, ), diff --git a/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart b/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart index 295328e4..467c492c 100644 --- a/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart +++ b/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart @@ -137,11 +137,13 @@ class ConditionExpr { class AutomationAction { String entityId; + String? actionType; String actionExecutor; ExecutorProperty? executorProperty; AutomationAction({ required this.entityId, + this.actionType, required this.actionExecutor, this.executorProperty, }); @@ -150,12 +152,15 @@ class AutomationAction { return { 'entityId': entityId, 'actionExecutor': actionExecutor, - 'executorProperty': executorProperty?.toMap(), + if (executorProperty != null) + 'executorProperty': executorProperty?.toMap(), + 'actionType': actionType }; } factory AutomationAction.fromMap(Map map) { return AutomationAction( + actionType: map['actionType'], entityId: map['entityId'] ?? '', actionExecutor: map['actionExecutor'] ?? '', executorProperty: map['executorProperty'] != null diff --git a/lib/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart b/lib/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart index c669aa9a..81ee1096 100644 --- a/lib/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart +++ b/lib/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart @@ -95,10 +95,12 @@ class CreateSceneModel { class CreateSceneAction { String entityId; + String? actionType; String actionExecutor; CreateSceneExecutorProperty? executorProperty; CreateSceneAction({ + this.actionType, required this.entityId, required this.actionExecutor, required this.executorProperty, @@ -110,6 +112,7 @@ class CreateSceneAction { CreateSceneExecutorProperty? executorProperty, }) { return CreateSceneAction( + actionType: actionType ?? this.actionType, entityId: entityId ?? this.entityId, actionExecutor: actionExecutor ?? this.actionExecutor, executorProperty: executorProperty ?? this.executorProperty, @@ -125,6 +128,7 @@ class CreateSceneAction { }; } else { return { + "actionType": actionType, 'entityId': entityId, 'actionExecutor': actionExecutor, }; @@ -133,6 +137,7 @@ class CreateSceneAction { factory CreateSceneAction.fromMap(Map map) { return CreateSceneAction( + actionType: map['actionType'], entityId: map['entityId'] ?? '', actionExecutor: map['actionExecutor'] ?? '', executorProperty: diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 1eddf841..e26d3d12 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -35,6 +35,20 @@ class DraggableCard extends StatelessWidget { final deviceFunctions = state.selectedFunctions[deviceData['uniqueCustomId']] ?? []; + int index = state.thenItems.indexWhere( + (item) => item['uniqueCustomId'] == deviceData['uniqueCustomId']); + + if (index != -1) { + return _buildCardContent(context, deviceFunctions, padding: padding); + } + + int ifIndex = state.ifItems.indexWhere( + (item) => item['uniqueCustomId'] == deviceData['uniqueCustomId']); + + if (ifIndex != -1) { + return _buildCardContent(context, deviceFunctions, padding: padding); + } + return Draggable>( data: deviceData, feedback: Transform.rotate( @@ -79,17 +93,13 @@ class DraggableCard extends StatelessWidget { ), ), padding: const EdgeInsets.all(8), - child: imagePath.contains('.svg') - ? SvgPicture.asset( - imagePath, + child: deviceData['type'] == 'tap_to_run' + ? Image.memory( + base64Decode(deviceData['icon']), ) - : imagePath.contains('.png') - ? Image.asset( - imagePath, - ) - : Image.memory( - base64Decode(imagePath), - ), + : SvgPicture.asset( + imagePath, + ), ), const SizedBox(height: 8), Padding( diff --git a/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart index 16a181b6..84ed8fdd 100644 --- a/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart @@ -7,98 +7,106 @@ import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -class AutomationDialog extends StatelessWidget { +class AutomationDialog extends StatefulWidget { final String automationName; final String automationId; final String uniqueCustomId; + final String? passedAutomationActionExecutor; const AutomationDialog({ super.key, required this.automationName, required this.automationId, required this.uniqueCustomId, + this.passedAutomationActionExecutor, }); @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final isEnabled = state.automationActionExecutor == 'rule_enable'; + State createState() => _AutomationDialogState(); +} - return Dialog( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Container( - width: 400, - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - DialogHeader(automationName), - const SizedBox(height: 16), - ListTile( - leading: - SvgPicture.asset(Assets.acPower, width: 24, height: 24), - title: const Text('Enable'), - trailing: Radio( - value: true, - groupValue: isEnabled, - onChanged: (bool? value) { - if (value == true) { - context.read().add( - const SetAutomationActionExecutor( - automationActionExecutor: 'rule_enable', - ), - ); - } - }, - ), - ), - ListTile( - leading: SvgPicture.asset(Assets.acPowerOff, - width: 24, height: 24), - title: const Text('Disable'), - trailing: Radio( - value: false, - groupValue: isEnabled, - onChanged: (bool? value) { - if (value == false) { - context.read().add( - const SetAutomationActionExecutor( - automationActionExecutor: 'rule_disable', - ), - ); - } - }, - ), - ), - const SizedBox(height: 16), - DialogFooter( - onConfirm: () { - context.read().add( - AddFunctionToRoutine( - [ - DeviceFunctionData( - entityId: automationId, - functionCode: 'automation', - value: state.automationActionExecutor, - operationName: 'Automation', - ), - ], - uniqueCustomId, - ), - ); - Navigator.of(context).pop(true); - }, - onCancel: () => Navigator.of(context).pop(false), - isConfirmEnabled: true, - dialogWidth: 400, - ), - ], +class _AutomationDialogState extends State { + String? selectedAutomationActionExecutor; + + @override + void initState() { + super.initState(); + List? functions = context + .read() + .state + .selectedFunctions[widget.uniqueCustomId]; + for (DeviceFunctionData data in functions ?? []) { + if (data.entityId == widget.automationId) { + selectedAutomationActionExecutor = data.value; + } + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Container( + width: 400, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DialogHeader(widget.automationName), + const SizedBox(height: 16), + ListTile( + leading: SvgPicture.asset(Assets.acPower, width: 24, height: 24), + title: const Text('Enable'), + trailing: Radio( + value: 'rule_enable', + groupValue: selectedAutomationActionExecutor, + onChanged: (String? value) { + setState(() { + selectedAutomationActionExecutor = 'rule_enable'; + }); + }), ), - ), - ); - }, + ListTile( + leading: + SvgPicture.asset(Assets.acPowerOff, width: 24, height: 24), + title: const Text('Disable'), + trailing: Radio( + value: 'rule_disable', + groupValue: selectedAutomationActionExecutor, + onChanged: (String? value) { + setState(() { + selectedAutomationActionExecutor = 'rule_disable'; + }); + }, + ), + ), + const SizedBox(height: 16), + DialogFooter( + onConfirm: () { + if (selectedAutomationActionExecutor != null) { + context.read().add( + AddFunctionToRoutine( + [ + DeviceFunctionData( + entityId: widget.automationId, + functionCode: 'automation', + value: selectedAutomationActionExecutor, + operationName: 'Automation', + ), + ], + widget.uniqueCustomId, + ), + ); + } + Navigator.of(context).pop(true); + }, + onCancel: () => Navigator.of(context).pop(), + isConfirmEnabled: selectedAutomationActionExecutor != null, + dialogWidth: 400, + ), + ], + ), + ), ); } } diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart index 4f4165d5..f10694b6 100644 --- a/lib/pages/routiens/widgets/routine_search_and_buttons.dart +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -43,6 +43,13 @@ class _RoutineSearchAndButtonsState extends State { return Wrap( runSpacing: 16, children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + state.errorMessage ?? '', + style: const TextStyle(color: Colors.red), + ), + ), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -214,6 +221,7 @@ class _RoutineSearchAndButtonsState extends State { } // final result = // await + BlocProvider.of(context).add(ResetErrorMessage()); SaveRoutineHelper.showSaveRoutineDialog(context); // if (result != null && result) { // BlocProvider.of(context).add( @@ -341,6 +349,7 @@ class _RoutineSearchAndButtonsState extends State { } // final result = // await + BlocProvider.of(context).add(ResetErrorMessage()); SaveRoutineHelper.showSaveRoutineDialog(context); // if (result != null && result) { // BlocProvider.of(context).add( diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index d959c372..8a12e443 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -87,8 +87,11 @@ class ThenContainer extends StatelessWidget { ...state.thenItems[index], 'imagePath': Assets.automation, - 'title': state - .thenItems[index]['name'], + 'title': + state.thenItems[index] + ['name'] ?? + state.thenItems[index] + ['title'], })); } return; @@ -149,6 +152,12 @@ class ThenContainer extends StatelessWidget { } if (mutableData['type'] == 'automation') { + int index = state.thenItems.indexWhere( + (item) => item['deviceId'] == mutableData['deviceId']); + if (index != -1) { + return; + } + final result = await showDialog( context: context, builder: (BuildContext context) => AutomationDialog( @@ -169,9 +178,14 @@ class ThenContainer extends StatelessWidget { } if (mutableData['type'] == 'tap_to_run' && state.isAutomation) { + int index = state.thenItems.indexWhere( + (item) => item['deviceId'] == mutableData['deviceId']); + if (index != -1) { + return; + } context.read().add(AddToThenContainer({ ...mutableData, - 'imagePath': Assets.logo, + 'imagePath': mutableData['imagePath'] ?? Assets.logo, 'title': mutableData['name'], })); diff --git a/lib/utils/snack_bar.dart b/lib/utils/snack_bar.dart index d50a4250..0a312e5a 100644 --- a/lib/utils/snack_bar.dart +++ b/lib/utils/snack_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/navigation_service.dart'; class CustomSnackBar { @@ -11,6 +12,35 @@ class CustomSnackBar { } } + static redSnackBar(String message) { + final key = NavigationService.snackbarKey; + BuildContext? currentContext = key?.currentContext; + if (key != null && currentContext != null) { + final snackBar = SnackBar( + padding: const EdgeInsets.all(16), + backgroundColor: ColorsManager.red, + content: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + const Icon( + Icons.check_circle, + color: ColorsManager.whiteColors, + size: 32, + ), + const SizedBox( + width: 8, + ), + Text( + message, + style: Theme.of(currentContext) + .textTheme + .bodySmall! + .copyWith(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green), + ) + ]), + ); + key.currentState?.showSnackBar(snackBar); + } + } + static greenSnackBar(String message) { final key = NavigationService.snackbarKey; BuildContext? currentContext = key?.currentContext; @@ -29,8 +59,10 @@ class CustomSnackBar { ), Text( message, - style: Theme.of(currentContext).textTheme.bodySmall!.copyWith( - fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green), + style: Theme.of(currentContext) + .textTheme + .bodySmall! + .copyWith(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green), ) ]), );