Merge pull request #57 from SyncrowIOT/routines_issues

Routines issues
This commit is contained in:
Abdullah
2024-12-03 01:04:12 +03:00
committed by GitHub
11 changed files with 657 additions and 391 deletions

File diff suppressed because it is too large Load Diff

View File

@ -206,3 +206,5 @@ class FetchDevicesInRoutine extends RoutineEvent {}
class ResetRoutineState extends RoutineEvent {} class ResetRoutineState extends RoutineEvent {}
class ClearFunctions extends RoutineEvent {} class ClearFunctions extends RoutineEvent {}
class ResetErrorMessage extends RoutineEvent {}

View File

@ -22,7 +22,7 @@ class RoutineState extends Equatable {
final String? automationId; final String? automationId;
final bool? isUpdate; final bool? isUpdate;
final List<AllDevicesModel> devices; final List<AllDevicesModel> devices;
final String? automationActionExecutor; // final String? automationActionExecutor;
final bool routineTab; final bool routineTab;
final bool createRoutineView; final bool createRoutineView;
@ -48,7 +48,7 @@ class RoutineState extends Equatable {
this.automationId, this.automationId,
this.isUpdate, this.isUpdate,
this.devices = const [], this.devices = const [],
this.automationActionExecutor, // this.automationActionExecutor,
this.routineTab = false, this.routineTab = false,
this.createRoutineView = false}); this.createRoutineView = false});
@ -73,7 +73,7 @@ class RoutineState extends Equatable {
String? automationId, String? automationId,
bool? isUpdate, bool? isUpdate,
List<AllDevicesModel>? devices, List<AllDevicesModel>? devices,
String? automationActionExecutor, // String? automationActionExecutor,
TextEditingController? nameController, TextEditingController? nameController,
bool? routineTab, bool? routineTab,
bool? createRoutineView, bool? createRoutineView,
@ -88,18 +88,21 @@ class RoutineState extends Equatable {
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
routineName: routineName ?? this.routineName, routineName: routineName ?? this.routineName,
selectedIcon: selectedIcon ?? this.selectedIcon, selectedIcon: selectedIcon ?? this.selectedIcon,
loadScenesErrorMessage: loadScenesErrorMessage ?? this.loadScenesErrorMessage, loadScenesErrorMessage:
loadAutomationErrorMessage: loadAutomationErrorMessage ?? this.loadAutomationErrorMessage, loadScenesErrorMessage ?? this.loadScenesErrorMessage,
loadAutomationErrorMessage:
loadAutomationErrorMessage ?? this.loadAutomationErrorMessage,
searchText: searchText ?? this.searchText, searchText: searchText ?? this.searchText,
isTabToRun: isTabToRun ?? this.isTabToRun, isTabToRun: isTabToRun ?? this.isTabToRun,
isAutomation: isAutomation ?? this.isAutomation, isAutomation: isAutomation ?? this.isAutomation,
selectedAutomationOperator: selectedAutomationOperator ?? this.selectedAutomationOperator, selectedAutomationOperator:
selectedAutomationOperator ?? this.selectedAutomationOperator,
effectiveTime: effectiveTime ?? this.effectiveTime, effectiveTime: effectiveTime ?? this.effectiveTime,
sceneId: sceneId ?? this.sceneId, sceneId: sceneId ?? this.sceneId,
automationId: automationId ?? this.automationId, automationId: automationId ?? this.automationId,
isUpdate: isUpdate ?? this.isUpdate, isUpdate: isUpdate ?? this.isUpdate,
devices: devices ?? this.devices, devices: devices ?? this.devices,
automationActionExecutor: automationActionExecutor ?? this.automationActionExecutor, // automationActionExecutor: automationActionExecutor ?? this.automationActionExecutor,
routineTab: routineTab ?? this.routineTab, routineTab: routineTab ?? this.routineTab,
createRoutineView: createRoutineView ?? this.createRoutineView); createRoutineView: createRoutineView ?? this.createRoutineView);
} }
@ -126,7 +129,7 @@ class RoutineState extends Equatable {
automationId, automationId,
isUpdate, isUpdate,
devices, devices,
automationActionExecutor, // automationActionExecutor,
routineTab, routineTab,
createRoutineView createRoutineView
]; ];

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.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/pages/routiens/widgets/dialog_footer.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SaveRoutineHelper { class SaveRoutineHelper {
static Future<void> showSaveRoutineDialog(BuildContext context) async { static Future<void> showSaveRoutineDialog(BuildContext context) async {
@ -98,18 +101,29 @@ class SaveRoutineHelper {
final functions = final functions =
state.selectedFunctions[item['uniqueCustomId']] ?? []; state.selectedFunctions[item['uniqueCustomId']] ?? [];
return ListTile( return ListTile(
leading: SvgPicture.asset( leading: item['type'] == 'tap_to_run'
item['imagePath'], ? Image.memory(
width: 22, base64Decode(item['icon']),
height: 22, 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( subtitle: Wrap(
children: functions children: functions
.map((f) => Text( .map((f) => Text(
'${f.operationName}: ${f.value}, ', '${f.operationName}: ${f.value}, ',
style: const TextStyle( style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.grayColor, fontSize: 8), color: ColorsManager.grayColor, fontSize: 8),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 3, maxLines: 3,
@ -124,17 +138,17 @@ class SaveRoutineHelper {
], ],
), ),
), ),
if (state.errorMessage != null || state.errorMessage!.isNotEmpty) // if (state.errorMessage != null || state.errorMessage!.isNotEmpty)
Padding( // Padding(
padding: const EdgeInsets.all(8.0), // padding: const EdgeInsets.all(8.0),
child: Text( // child: Text(
state.errorMessage!, // state.errorMessage!,
style: const TextStyle(color: Colors.red), // style: const TextStyle(color: Colors.red),
), // ),
), // ),
DialogFooter( DialogFooter(
onCancel: () => Navigator.pop(context), onCancel: () => Navigator.pop(context),
onConfirm: () { onConfirm: () async {
if (state.isAutomation) { if (state.isAutomation) {
if (state.automationId != null) { if (state.automationId != null) {
context.read<RoutineBloc>().add(const UpdateAutomation()); context.read<RoutineBloc>().add(const UpdateAutomation());
@ -148,10 +162,9 @@ class SaveRoutineHelper {
context.read<RoutineBloc>().add(const CreateSceneEvent()); context.read<RoutineBloc>().add(const CreateSceneEvent());
} }
} }
if (context.read<RoutineBloc>().state.errorMessage == null || // if (state.errorMessage == null || state.errorMessage!.isEmpty) {
context.read<RoutineBloc>().state.errorMessage!.isEmpty) { Navigator.pop(context);
Navigator.pop(context); // }
}
}, },
isConfirmEnabled: true, isConfirmEnabled: true,
), ),

View File

@ -137,11 +137,13 @@ class ConditionExpr {
class AutomationAction { class AutomationAction {
String entityId; String entityId;
String? actionType;
String actionExecutor; String actionExecutor;
ExecutorProperty? executorProperty; ExecutorProperty? executorProperty;
AutomationAction({ AutomationAction({
required this.entityId, required this.entityId,
this.actionType,
required this.actionExecutor, required this.actionExecutor,
this.executorProperty, this.executorProperty,
}); });
@ -150,12 +152,15 @@ class AutomationAction {
return { return {
'entityId': entityId, 'entityId': entityId,
'actionExecutor': actionExecutor, 'actionExecutor': actionExecutor,
'executorProperty': executorProperty?.toMap(), if (executorProperty != null)
'executorProperty': executorProperty?.toMap(),
'actionType': actionType
}; };
} }
factory AutomationAction.fromMap(Map<String, dynamic> map) { factory AutomationAction.fromMap(Map<String, dynamic> map) {
return AutomationAction( return AutomationAction(
actionType: map['actionType'],
entityId: map['entityId'] ?? '', entityId: map['entityId'] ?? '',
actionExecutor: map['actionExecutor'] ?? '', actionExecutor: map['actionExecutor'] ?? '',
executorProperty: map['executorProperty'] != null executorProperty: map['executorProperty'] != null

View File

@ -95,10 +95,12 @@ class CreateSceneModel {
class CreateSceneAction { class CreateSceneAction {
String entityId; String entityId;
String? actionType;
String actionExecutor; String actionExecutor;
CreateSceneExecutorProperty? executorProperty; CreateSceneExecutorProperty? executorProperty;
CreateSceneAction({ CreateSceneAction({
this.actionType,
required this.entityId, required this.entityId,
required this.actionExecutor, required this.actionExecutor,
required this.executorProperty, required this.executorProperty,
@ -110,6 +112,7 @@ class CreateSceneAction {
CreateSceneExecutorProperty? executorProperty, CreateSceneExecutorProperty? executorProperty,
}) { }) {
return CreateSceneAction( return CreateSceneAction(
actionType: actionType ?? this.actionType,
entityId: entityId ?? this.entityId, entityId: entityId ?? this.entityId,
actionExecutor: actionExecutor ?? this.actionExecutor, actionExecutor: actionExecutor ?? this.actionExecutor,
executorProperty: executorProperty ?? this.executorProperty, executorProperty: executorProperty ?? this.executorProperty,
@ -125,6 +128,7 @@ class CreateSceneAction {
}; };
} else { } else {
return { return {
"actionType": actionType,
'entityId': entityId, 'entityId': entityId,
'actionExecutor': actionExecutor, 'actionExecutor': actionExecutor,
}; };
@ -133,6 +137,7 @@ class CreateSceneAction {
factory CreateSceneAction.fromMap(Map<String, dynamic> map) { factory CreateSceneAction.fromMap(Map<String, dynamic> map) {
return CreateSceneAction( return CreateSceneAction(
actionType: map['actionType'],
entityId: map['entityId'] ?? '', entityId: map['entityId'] ?? '',
actionExecutor: map['actionExecutor'] ?? '', actionExecutor: map['actionExecutor'] ?? '',
executorProperty: executorProperty:

View File

@ -35,6 +35,20 @@ class DraggableCard extends StatelessWidget {
final deviceFunctions = final deviceFunctions =
state.selectedFunctions[deviceData['uniqueCustomId']] ?? []; 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<Map<String, dynamic>>( return Draggable<Map<String, dynamic>>(
data: deviceData, data: deviceData,
feedback: Transform.rotate( feedback: Transform.rotate(
@ -79,17 +93,13 @@ class DraggableCard extends StatelessWidget {
), ),
), ),
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: imagePath.contains('.svg') child: deviceData['type'] == 'tap_to_run'
? SvgPicture.asset( ? Image.memory(
imagePath, base64Decode(deviceData['icon']),
) )
: imagePath.contains('.png') : SvgPicture.asset(
? Image.asset( imagePath,
imagePath, ),
)
: Image.memory(
base64Decode(imagePath),
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Padding( Padding(

View File

@ -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/pages/routiens/widgets/dialog_footer.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
class AutomationDialog extends StatelessWidget { class AutomationDialog extends StatefulWidget {
final String automationName; final String automationName;
final String automationId; final String automationId;
final String uniqueCustomId; final String uniqueCustomId;
final String? passedAutomationActionExecutor;
const AutomationDialog({ const AutomationDialog({
super.key, super.key,
required this.automationName, required this.automationName,
required this.automationId, required this.automationId,
required this.uniqueCustomId, required this.uniqueCustomId,
this.passedAutomationActionExecutor,
}); });
@override @override
Widget build(BuildContext context) { State<AutomationDialog> createState() => _AutomationDialogState();
return BlocBuilder<RoutineBloc, RoutineState>( }
builder: (context, state) {
final isEnabled = state.automationActionExecutor == 'rule_enable';
return Dialog( class _AutomationDialogState extends State<AutomationDialog> {
shape: String? selectedAutomationActionExecutor;
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container( @override
width: 400, void initState() {
padding: const EdgeInsets.all(16), super.initState();
child: Column( List<DeviceFunctionData>? functions = context
mainAxisSize: MainAxisSize.min, .read<RoutineBloc>()
children: [ .state
DialogHeader(automationName), .selectedFunctions[widget.uniqueCustomId];
const SizedBox(height: 16), for (DeviceFunctionData data in functions ?? []) {
ListTile( if (data.entityId == widget.automationId) {
leading: selectedAutomationActionExecutor = data.value;
SvgPicture.asset(Assets.acPower, width: 24, height: 24), }
title: const Text('Enable'), }
trailing: Radio<bool>( }
value: true,
groupValue: isEnabled, @override
onChanged: (bool? value) { Widget build(BuildContext context) {
if (value == true) { return Dialog(
context.read<RoutineBloc>().add( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
const SetAutomationActionExecutor( child: Container(
automationActionExecutor: 'rule_enable', width: 400,
), padding: const EdgeInsets.all(16),
); child: Column(
} mainAxisSize: MainAxisSize.min,
}, children: [
), DialogHeader(widget.automationName),
), const SizedBox(height: 16),
ListTile( ListTile(
leading: SvgPicture.asset(Assets.acPowerOff, leading: SvgPicture.asset(Assets.acPower, width: 24, height: 24),
width: 24, height: 24), title: const Text('Enable'),
title: const Text('Disable'), trailing: Radio<String?>(
trailing: Radio<bool>( value: 'rule_enable',
value: false, groupValue: selectedAutomationActionExecutor,
groupValue: isEnabled, onChanged: (String? value) {
onChanged: (bool? value) { setState(() {
if (value == false) { selectedAutomationActionExecutor = 'rule_enable';
context.read<RoutineBloc>().add( });
const SetAutomationActionExecutor( }),
automationActionExecutor: 'rule_disable',
),
);
}
},
),
),
const SizedBox(height: 16),
DialogFooter(
onConfirm: () {
context.read<RoutineBloc>().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,
),
],
), ),
), ListTile(
); leading:
}, SvgPicture.asset(Assets.acPowerOff, width: 24, height: 24),
title: const Text('Disable'),
trailing: Radio<String?>(
value: 'rule_disable',
groupValue: selectedAutomationActionExecutor,
onChanged: (String? value) {
setState(() {
selectedAutomationActionExecutor = 'rule_disable';
});
},
),
),
const SizedBox(height: 16),
DialogFooter(
onConfirm: () {
if (selectedAutomationActionExecutor != null) {
context.read<RoutineBloc>().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,
),
],
),
),
); );
} }
} }

View File

@ -43,6 +43,13 @@ class _RoutineSearchAndButtonsState extends State<RoutineSearchAndButtons> {
return Wrap( return Wrap(
runSpacing: 16, runSpacing: 16,
children: [ children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
state.errorMessage ?? '',
style: const TextStyle(color: Colors.red),
),
),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
@ -214,6 +221,7 @@ class _RoutineSearchAndButtonsState extends State<RoutineSearchAndButtons> {
} }
// final result = // final result =
// await // await
BlocProvider.of<RoutineBloc>(context).add(ResetErrorMessage());
SaveRoutineHelper.showSaveRoutineDialog(context); SaveRoutineHelper.showSaveRoutineDialog(context);
// if (result != null && result) { // if (result != null && result) {
// BlocProvider.of<RoutineBloc>(context).add( // BlocProvider.of<RoutineBloc>(context).add(
@ -341,6 +349,7 @@ class _RoutineSearchAndButtonsState extends State<RoutineSearchAndButtons> {
} }
// final result = // final result =
// await // await
BlocProvider.of<RoutineBloc>(context).add(ResetErrorMessage());
SaveRoutineHelper.showSaveRoutineDialog(context); SaveRoutineHelper.showSaveRoutineDialog(context);
// if (result != null && result) { // if (result != null && result) {
// BlocProvider.of<RoutineBloc>(context).add( // BlocProvider.of<RoutineBloc>(context).add(

View File

@ -87,8 +87,11 @@ class ThenContainer extends StatelessWidget {
...state.thenItems[index], ...state.thenItems[index],
'imagePath': 'imagePath':
Assets.automation, Assets.automation,
'title': state 'title':
.thenItems[index]['name'], state.thenItems[index]
['name'] ??
state.thenItems[index]
['title'],
})); }));
} }
return; return;
@ -149,6 +152,12 @@ class ThenContainer extends StatelessWidget {
} }
if (mutableData['type'] == 'automation') { if (mutableData['type'] == 'automation') {
int index = state.thenItems.indexWhere(
(item) => item['deviceId'] == mutableData['deviceId']);
if (index != -1) {
return;
}
final result = await showDialog<bool>( final result = await showDialog<bool>(
context: context, context: context,
builder: (BuildContext context) => AutomationDialog( builder: (BuildContext context) => AutomationDialog(
@ -169,9 +178,14 @@ class ThenContainer extends StatelessWidget {
} }
if (mutableData['type'] == 'tap_to_run' && state.isAutomation) { if (mutableData['type'] == 'tap_to_run' && state.isAutomation) {
int index = state.thenItems.indexWhere(
(item) => item['deviceId'] == mutableData['deviceId']);
if (index != -1) {
return;
}
context.read<RoutineBloc>().add(AddToThenContainer({ context.read<RoutineBloc>().add(AddToThenContainer({
...mutableData, ...mutableData,
'imagePath': Assets.logo, 'imagePath': mutableData['imagePath'] ?? Assets.logo,
'title': mutableData['name'], 'title': mutableData['name'],
})); }));

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/navigation_service.dart'; import 'package:syncrow_web/utils/navigation_service.dart';
class CustomSnackBar { 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) { static greenSnackBar(String message) {
final key = NavigationService.snackbarKey; final key = NavigationService.snackbarKey;
BuildContext? currentContext = key?.currentContext; BuildContext? currentContext = key?.currentContext;
@ -29,8 +59,10 @@ class CustomSnackBar {
), ),
Text( Text(
message, message,
style: Theme.of(currentContext).textTheme.bodySmall!.copyWith( style: Theme.of(currentContext)
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green), .textTheme
.bodySmall!
.copyWith(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green),
) )
]), ]),
); );