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 ClearFunctions extends RoutineEvent {}
class ResetErrorMessage extends RoutineEvent {}

View File

@ -22,7 +22,7 @@ class RoutineState extends Equatable {
final String? automationId;
final bool? isUpdate;
final List<AllDevicesModel> 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<AllDevicesModel>? 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
];

View File

@ -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<void> 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<RoutineBloc>().add(const UpdateAutomation());
@ -148,10 +162,9 @@ class SaveRoutineHelper {
context.read<RoutineBloc>().add(const CreateSceneEvent());
}
}
if (context.read<RoutineBloc>().state.errorMessage == null ||
context.read<RoutineBloc>().state.errorMessage!.isEmpty) {
Navigator.pop(context);
}
// if (state.errorMessage == null || state.errorMessage!.isEmpty) {
Navigator.pop(context);
// }
},
isConfirmEnabled: true,
),

View File

@ -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<String, dynamic> map) {
return AutomationAction(
actionType: map['actionType'],
entityId: map['entityId'] ?? '',
actionExecutor: map['actionExecutor'] ?? '',
executorProperty: map['executorProperty'] != null

View File

@ -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<String, dynamic> map) {
return CreateSceneAction(
actionType: map['actionType'],
entityId: map['entityId'] ?? '',
actionExecutor: map['actionExecutor'] ?? '',
executorProperty:

View File

@ -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<Map<String, dynamic>>(
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(

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/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<RoutineBloc, RoutineState>(
builder: (context, state) {
final isEnabled = state.automationActionExecutor == 'rule_enable';
State<AutomationDialog> 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<bool>(
value: true,
groupValue: isEnabled,
onChanged: (bool? value) {
if (value == true) {
context.read<RoutineBloc>().add(
const SetAutomationActionExecutor(
automationActionExecutor: 'rule_enable',
),
);
}
},
),
),
ListTile(
leading: SvgPicture.asset(Assets.acPowerOff,
width: 24, height: 24),
title: const Text('Disable'),
trailing: Radio<bool>(
value: false,
groupValue: isEnabled,
onChanged: (bool? value) {
if (value == false) {
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,
),
],
class _AutomationDialogState extends State<AutomationDialog> {
String? selectedAutomationActionExecutor;
@override
void initState() {
super.initState();
List<DeviceFunctionData>? functions = context
.read<RoutineBloc>()
.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<String?>(
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<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(
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<RoutineSearchAndButtons> {
}
// final result =
// await
BlocProvider.of<RoutineBloc>(context).add(ResetErrorMessage());
SaveRoutineHelper.showSaveRoutineDialog(context);
// if (result != null && result) {
// BlocProvider.of<RoutineBloc>(context).add(
@ -341,6 +349,7 @@ class _RoutineSearchAndButtonsState extends State<RoutineSearchAndButtons> {
}
// final result =
// await
BlocProvider.of<RoutineBloc>(context).add(ResetErrorMessage());
SaveRoutineHelper.showSaveRoutineDialog(context);
// if (result != null && result) {
// BlocProvider.of<RoutineBloc>(context).add(

View File

@ -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<bool>(
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<RoutineBloc>().add(AddToThenContainer({
...mutableData,
'imagePath': Assets.logo,
'imagePath': mutableData['imagePath'] ?? Assets.logo,
'title': mutableData['name'],
}));

View File

@ -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),
)
]),
);