Implemented side tree to devices and rountines screen

This commit is contained in:
Abdullah Alassaf
2025-01-04 17:45:15 +03:00
parent 0341844ea9
commit a98f7e77a3
88 changed files with 1551 additions and 1202 deletions

View File

@ -0,0 +1,118 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_event.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_state.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
class EffectPeriodBloc extends Bloc<EffectPeriodEvent, EffectPeriodState> {
final daysMap = {
'Sun': 'S',
'Mon': 'M',
'Tue': 'T',
'Wed': 'W',
'Thu': 'T',
'Fri': 'F',
'Sat': 'S',
};
EffectPeriodBloc() : super(EffectPeriodState.initial()) {
on<InitialEffectPeriodEvent>(_initialEvent);
on<SetPeriod>(_onSetPeriod);
on<ToggleDay>(_onToggleDay);
on<SetCustomTime>(_onSetCustomTime);
on<ResetEffectivePeriod>(_onResetEffectivePeriod);
on<ResetDays>(_onResetDays);
on<SetDays>(_setAllDays);
}
void _initialEvent(InitialEffectPeriodEvent event, Emitter<EffectPeriodState> emit) {
add(SetCustomTime(event.effectiveTime.start, event.effectiveTime.end));
emit(state.copyWith(
selectedDaysBinary: event.effectiveTime.loops,
customStartTime: event.effectiveTime.start,
customEndTime: event.effectiveTime.end,
));
}
void _onSetPeriod(SetPeriod event, Emitter<EffectPeriodState> emit) {
String startTime = '';
String endTime = '';
switch (event.period) {
case EnumEffectivePeriodOptions.allDay:
startTime = '00:00';
endTime = '23:59';
break;
case EnumEffectivePeriodOptions.daytime:
startTime = '06:00';
endTime = '18:00';
break;
case EnumEffectivePeriodOptions.night:
startTime = '18:00';
endTime = '06:00';
break;
case EnumEffectivePeriodOptions.custom:
startTime = state.customStartTime ?? '00:00';
endTime = state.customEndTime ?? '23:59';
break;
default:
break;
}
emit(state.copyWith(
selectedPeriod: event.period, customStartTime: startTime, customEndTime: endTime));
}
void _onToggleDay(ToggleDay event, Emitter<EffectPeriodState> emit) {
final daysList = state.selectedDaysBinary.split('');
final dayIndex = getDayIndex(event.day);
if (daysList[dayIndex] == '1') {
daysList[dayIndex] = '0';
} else {
daysList[dayIndex] = '1';
}
final newDaysBinary = daysList.join();
emit(state.copyWith(selectedDaysBinary: newDaysBinary));
}
void _onSetCustomTime(SetCustomTime event, Emitter<EffectPeriodState> emit) {
String startTime = event.startTime;
String endTime = event.endTime;
EnumEffectivePeriodOptions period;
// Determine the period based on start and end times
if (startTime == '00:00' && endTime == '23:59') {
period = EnumEffectivePeriodOptions.allDay;
} else if (startTime == '06:00' && endTime == '18:00') {
period = EnumEffectivePeriodOptions.daytime;
} else if (startTime == '18:00' && endTime == '06:00') {
period = EnumEffectivePeriodOptions.night;
} else {
period = EnumEffectivePeriodOptions.custom;
}
emit(
state.copyWith(customStartTime: startTime, customEndTime: endTime, selectedPeriod: period));
}
void _onResetEffectivePeriod(ResetEffectivePeriod event, Emitter<EffectPeriodState> emit) {
emit(state.copyWith(
selectedPeriod: EnumEffectivePeriodOptions.allDay,
customStartTime: '00:00',
customEndTime: '23:59',
selectedDaysBinary: '1111111'));
}
void _onResetDays(ResetDays event, Emitter<EffectPeriodState> emit) {
emit(state.copyWith(selectedDaysBinary: '1111111'));
}
int getDayIndex(String day) {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return days.indexOf(day);
}
FutureOr<void> _setAllDays(SetDays event, Emitter<EffectPeriodState> emit) {
emit(state.copyWith(selectedDaysBinary: event.daysBinary));
}
}

View File

@ -0,0 +1,60 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/create_automation_model.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
abstract class EffectPeriodEvent extends Equatable {
const EffectPeriodEvent();
@override
List<Object> get props => [];
}
class InitialEffectPeriodEvent extends EffectPeriodEvent {
final EffectiveTime effectiveTime;
const InitialEffectPeriodEvent(this.effectiveTime);
@override
List<Object> get props => [effectiveTime];
}
class SetPeriod extends EffectPeriodEvent {
final EnumEffectivePeriodOptions period;
const SetPeriod(this.period);
@override
List<Object> get props => [period];
}
class ToggleDay extends EffectPeriodEvent {
final String day;
const ToggleDay(this.day);
@override
List<Object> get props => [day];
}
class SetCustomTime extends EffectPeriodEvent {
final String startTime;
final String endTime;
const SetCustomTime(this.startTime, this.endTime);
@override
List<Object> get props => [startTime, endTime];
}
class ResetEffectivePeriod extends EffectPeriodEvent {}
class ResetDays extends EffectPeriodEvent {
@override
List<Object> get props => [];
}
class SetDays extends EffectPeriodEvent {
final String daysBinary;
const SetDays(this.daysBinary);
}

View File

@ -0,0 +1,54 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
class EffectPeriodState extends Equatable {
final EnumEffectivePeriodOptions selectedPeriod;
final String selectedDaysBinary;
final String? customStartTime;
final String? customEndTime;
const EffectPeriodState({
required this.selectedPeriod,
required this.selectedDaysBinary,
this.customStartTime,
this.customEndTime,
});
factory EffectPeriodState.initial() {
return const EffectPeriodState(
selectedPeriod: EnumEffectivePeriodOptions.allDay,
selectedDaysBinary: "1111111", // All days selected
customStartTime: "00:00",
customEndTime: "23:59",
);
}
EffectPeriodState copyWith({
EnumEffectivePeriodOptions? selectedPeriod,
String? selectedDaysBinary,
String? customStartTime,
String? customEndTime,
}) {
return EffectPeriodState(
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
selectedDaysBinary: selectedDaysBinary ?? this.selectedDaysBinary,
customStartTime: customStartTime ?? this.customStartTime,
customEndTime: customEndTime ?? this.customEndTime,
);
}
EnumEffectivePeriodOptions getEffectivePeriod() {
if (customStartTime == '00:00' && customEndTime == '23:59') {
return EnumEffectivePeriodOptions.allDay;
} else if (customStartTime == '06:00' && customEndTime == '18:00') {
return EnumEffectivePeriodOptions.daytime;
} else if (customStartTime == '18:00' && customEndTime == '06:00') {
return EnumEffectivePeriodOptions.night;
} else {
return EnumEffectivePeriodOptions.custom;
}
}
@override
List<Object?> get props => [selectedPeriod, selectedDaysBinary, customStartTime, customEndTime];
}

View File

@ -0,0 +1,65 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
part 'functions_bloc_event.dart';
part 'functions_bloc_state.dart';
class FunctionBloc extends Bloc<FunctionBlocEvent, FunctionBlocState> {
FunctionBloc() : super(const FunctionBlocState()) {
on<InitializeFunctions>(_onInitializeFunctions);
on<AddFunction>(_onAddFunction);
on<SelectFunction>(_onSelectFunction);
}
void _onAddFunction(AddFunction event, Emitter<FunctionBlocState> emit) {
final functions = List<DeviceFunctionData>.from(state.addedFunctions);
final existingIndex = functions.indexWhere(
(f) => f.functionCode == event.functionData.functionCode,
);
if (existingIndex != -1) {
final existingData = functions[existingIndex];
functions[existingIndex] = DeviceFunctionData(
entityId: event.functionData.entityId,
functionCode: event.functionData.functionCode,
operationName: event.functionData.operationName,
value: event.functionData.value ?? existingData.value,
valueDescription: event.functionData.valueDescription ?? existingData.valueDescription,
condition: event.functionData.condition ?? existingData.condition,
);
} else {
functions.add(event.functionData);
}
emit(state.copyWith(
addedFunctions: functions,
selectedFunction: event.functionData.functionCode,
));
}
void _onInitializeFunctions(
InitializeFunctions event,
Emitter<FunctionBlocState> emit,
) {
emit(state.copyWith(addedFunctions: event.functions));
}
DeviceFunctionData? getFunction(String functionCode) {
return state.addedFunctions.firstWhere(
(data) => data.functionCode == functionCode,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: functionCode,
operationName: '',
value: null,
),
);
}
FutureOr<void> _onSelectFunction(SelectFunction event, Emitter<FunctionBlocState> emit) {
emit(state.copyWith(
selectedFunction: event.functionCode, selectedOperationName: event.operationName));
}
}

View File

@ -0,0 +1,41 @@
part of 'functions_bloc_bloc.dart';
abstract class FunctionBlocEvent extends Equatable {
const FunctionBlocEvent();
@override
List<Object?> get props => [];
}
class AddFunction extends FunctionBlocEvent {
final DeviceFunctionData functionData;
const AddFunction({
required this.functionData,
});
@override
List<Object?> get props => [functionData];
}
class SelectFunction extends FunctionBlocEvent {
final String functionCode;
final String operationName;
const SelectFunction({
required this.functionCode,
required this.operationName,
});
@override
List<Object?> get props => [functionCode, operationName];
}
class InitializeFunctions extends FunctionBlocEvent {
final List<DeviceFunctionData> functions;
const InitializeFunctions(this.functions);
@override
List<Object?> get props => [functions];
}

View File

@ -0,0 +1,29 @@
part of 'functions_bloc_bloc.dart';
class FunctionBlocState extends Equatable {
final List<DeviceFunctionData> addedFunctions;
final String? selectedFunction;
final String? selectedOperationName;
const FunctionBlocState({
this.addedFunctions = const [],
this.selectedFunction,
this.selectedOperationName,
});
FunctionBlocState copyWith({
List<DeviceFunctionData>? addedFunctions,
String? selectedFunction,
String? selectedOperationName,
}) {
return FunctionBlocState(
addedFunctions: addedFunctions ?? this.addedFunctions,
selectedFunction: selectedFunction ?? this.selectedFunction,
selectedOperationName:
selectedOperationName ?? this.selectedOperationName,
);
}
@override
List<Object?> get props =>
[addedFunctions, selectedFunction, selectedOperationName];
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,210 @@
part of 'routine_bloc.dart';
abstract class RoutineEvent extends Equatable {
const RoutineEvent();
@override
List<Object> get props => [];
}
class AddToIfContainer extends RoutineEvent {
final Map<String, dynamic> item;
final bool isTabToRun;
const AddToIfContainer(this.item, this.isTabToRun);
@override
List<Object> get props => [item, isTabToRun];
}
class AddToThenContainer extends RoutineEvent {
final Map<String, dynamic> item;
const AddToThenContainer(this.item);
@override
List<Object> get props => [item];
}
class LoadScenes extends RoutineEvent {
final String spaceId;
final String communityId;
const LoadScenes(this.spaceId, this.communityId);
@override
List<Object> get props => [spaceId, communityId];
}
class LoadAutomation extends RoutineEvent {
final String spaceId;
const LoadAutomation(this.spaceId);
@override
List<Object> get props => [spaceId];
}
class AddFunctionToRoutine extends RoutineEvent {
final List<DeviceFunctionData> functions;
final String uniqueCustomId;
const AddFunctionToRoutine(this.functions, this.uniqueCustomId);
@override
List<Object> get props => [functions, uniqueCustomId];
}
class RemoveFunction extends RoutineEvent {
final DeviceFunctionData function;
const RemoveFunction(this.function);
@override
List<Object> get props => [function];
}
class SearchRoutines extends RoutineEvent {
final String query;
const SearchRoutines(this.query);
@override
List<Object> get props => [query];
}
class AddSelectedIcon extends RoutineEvent {
final String icon;
const AddSelectedIcon(this.icon);
@override
List<Object> get props => [icon];
}
class CreateSceneEvent extends RoutineEvent {
const CreateSceneEvent();
@override
List<Object> get props => [];
}
class RemoveDragCard extends RoutineEvent {
final int index;
final bool isFromThen;
final String key;
const RemoveDragCard({required this.index, required this.isFromThen, required this.key});
@override
List<Object> get props => [index, isFromThen, key];
}
class ChangeAutomationOperator extends RoutineEvent {
final String operator;
const ChangeAutomationOperator({required this.operator});
@override
List<Object> get props => [operator];
}
class EffectiveTimePeriodEvent extends RoutineEvent {
final EffectiveTime effectiveTime;
const EffectiveTimePeriodEvent(this.effectiveTime);
@override
List<Object> get props => [effectiveTime];
}
class CreateAutomationEvent extends RoutineEvent {
final String? automationId;
final bool updateAutomation;
const CreateAutomationEvent({
this.automationId,
this.updateAutomation = false,
});
@override
List<Object> get props => [];
}
class SetRoutineName extends RoutineEvent {
final String name;
const SetRoutineName(this.name);
@override
List<Object> get props => [name];
}
class GetSceneDetails extends RoutineEvent {
final String sceneId;
final bool isUpdate;
final bool isTabToRun;
const GetSceneDetails({
required this.sceneId,
required this.isUpdate,
required this.isTabToRun,
});
@override
List<Object> get props => [sceneId];
}
class GetAutomationDetails extends RoutineEvent {
final String automationId;
final bool isUpdate;
final bool isAutomation;
const GetAutomationDetails({
required this.automationId,
this.isUpdate = false,
this.isAutomation = false,
});
@override
List<Object> get props => [automationId];
}
class InitializeRoutineState extends RoutineEvent {
final RoutineDetailsModel routineDetails;
const InitializeRoutineState(this.routineDetails);
@override
List<Object> get props => [routineDetails];
}
class DeleteScene extends RoutineEvent {
const DeleteScene();
@override
List<Object> get props => [];
}
// class DeleteAutomation extends RoutineEvent {
// final String automationId;
// const DeleteAutomation({required this.automationId});
// @override
// List<Object> get props => [automationId];
// }
class UpdateScene extends RoutineEvent {
const UpdateScene();
@override
List<Object> get props => [];
}
class UpdateAutomation extends RoutineEvent {
const UpdateAutomation();
@override
List<Object> get props => [];
}
class SetAutomationActionExecutor extends RoutineEvent {
final String automationActionExecutor;
const SetAutomationActionExecutor({required this.automationActionExecutor});
@override
List<Object> get props => [automationActionExecutor];
}
class TriggerSwitchTabsEvent extends RoutineEvent {
final bool isRoutineTab;
const TriggerSwitchTabsEvent({required this.isRoutineTab});
@override
List<Object> get props => [isRoutineTab];
}
class CreateNewRoutineViewEvent extends RoutineEvent {
final bool createRoutineView;
const CreateNewRoutineViewEvent({required this.createRoutineView});
@override
List<Object> get props => [createRoutineView];
}
class FetchDevicesInRoutine extends RoutineEvent {}
class ResetRoutineState extends RoutineEvent {}
class ClearFunctions extends RoutineEvent {}
class ResetErrorMessage extends RoutineEvent {}

View File

@ -0,0 +1,136 @@
part of 'routine_bloc.dart';
class RoutineState extends Equatable {
final List<Map<String, dynamic>> ifItems;
final List<Map<String, dynamic>> thenItems;
final List<Map<String, String>> availableCards;
final List<ScenesModel> scenes;
final List<ScenesModel> automations;
final Map<String, List<DeviceFunctionData>> selectedFunctions;
final bool isLoading;
final String? errorMessage;
final String? loadScenesErrorMessage;
final String? loadAutomationErrorMessage;
final String? routineName;
final String? selectedIcon;
final String? searchText;
final bool isTabToRun;
final bool isAutomation;
final String selectedAutomationOperator;
final EffectiveTime? effectiveTime;
final String? sceneId;
final String? automationId;
final bool? isUpdate;
final List<AllDevicesModel> devices;
// final String? automationActionExecutor;
final bool routineTab;
final bool createRoutineView;
const RoutineState(
{this.ifItems = const [],
this.thenItems = const [],
this.availableCards = const [],
this.scenes = const [],
this.automations = const [],
this.selectedFunctions = const {},
this.isLoading = false,
this.errorMessage,
this.routineName,
this.selectedIcon,
this.loadScenesErrorMessage,
this.loadAutomationErrorMessage,
this.searchText,
this.isTabToRun = false,
this.isAutomation = false,
this.selectedAutomationOperator = 'or',
this.effectiveTime,
this.sceneId,
this.automationId,
this.isUpdate,
this.devices = const [],
// this.automationActionExecutor,
this.routineTab = false,
this.createRoutineView = false});
RoutineState copyWith({
List<Map<String, dynamic>>? ifItems,
List<Map<String, dynamic>>? thenItems,
List<ScenesModel>? scenes,
List<ScenesModel>? automations,
Map<String, List<DeviceFunctionData>>? selectedFunctions,
bool? isLoading,
String? errorMessage,
String? routineName,
String? selectedIcon,
String? loadAutomationErrorMessage,
String? loadScenesErrorMessage,
String? searchText,
bool? isTabToRun,
bool? isAutomation,
String? selectedAutomationOperator,
EffectiveTime? effectiveTime,
String? sceneId,
String? automationId,
bool? isUpdate,
List<AllDevicesModel>? devices,
// String? automationActionExecutor,
TextEditingController? nameController,
bool? routineTab,
bool? createRoutineView,
}) {
return RoutineState(
ifItems: ifItems ?? this.ifItems,
thenItems: thenItems ?? this.thenItems,
scenes: scenes ?? this.scenes,
automations: automations ?? this.automations,
selectedFunctions: selectedFunctions ?? this.selectedFunctions,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
routineName: routineName ?? this.routineName,
selectedIcon: selectedIcon ?? this.selectedIcon,
loadScenesErrorMessage:
loadScenesErrorMessage ?? this.loadScenesErrorMessage,
loadAutomationErrorMessage:
loadAutomationErrorMessage ?? this.loadAutomationErrorMessage,
searchText: searchText ?? this.searchText,
isTabToRun: isTabToRun ?? this.isTabToRun,
isAutomation: isAutomation ?? this.isAutomation,
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,
routineTab: routineTab ?? this.routineTab,
createRoutineView: createRoutineView ?? this.createRoutineView);
}
@override
List<Object?> get props => [
ifItems,
thenItems,
scenes,
automations,
selectedFunctions,
isLoading,
errorMessage,
routineName,
selectedIcon,
loadScenesErrorMessage,
loadAutomationErrorMessage,
searchText,
isTabToRun,
isAutomation,
selectedAutomationOperator,
effectiveTime,
sceneId,
automationId,
isUpdate,
devices,
// automationActionExecutor,
routineTab,
createRoutineView
];
}

View File

@ -0,0 +1,53 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/setting_bloc/setting_event.dart';
import 'package:syncrow_web/pages/routines/bloc/setting_bloc/setting_state.dart';
import 'package:syncrow_web/pages/routines/models/icon_model.dart';
import 'package:syncrow_web/services/routines_api.dart';
class SettingBloc extends Bloc<SettingEvent, SettingState> {
bool isExpanded = false;
String selectedIcon = '';
List<IconModel> iconModelList = [];
SettingBloc() : super(const InitialState()) {
on<InitialEvent>(_initialSetting);
on<FetchIcons>(_fetchIcons);
on<SelectIcon>(_selectIcon);
}
void _initialSetting(InitialEvent event, Emitter<SettingState> emit) async {
try {
emit(const LoadingState());
selectedIcon = event.selectedIcon;
emit(TabToRunSettingLoaded(
showInDevice: true, selectedIcon: event.selectedIcon, iconList: iconModelList));
} catch (e) {
emit(const FailedState(error: 'Something went wrong'));
}
}
void _fetchIcons(FetchIcons event, Emitter<SettingState> emit) async {
try {
isExpanded = event.expanded;
emit(const LoadingState());
if (isExpanded) {
iconModelList = await SceneApi.getIcon();
emit(TabToRunSettingLoaded(
showInDevice: true, selectedIcon: selectedIcon, iconList: iconModelList));
}
} catch (e) {
emit(const FailedState(error: 'Something went wrong'));
}
}
void _selectIcon(SelectIcon event, Emitter<SettingState> emit) async {
try {
emit(const LoadingState());
selectedIcon = event.iconId;
emit(TabToRunSettingLoaded(
showInDevice: true, selectedIcon: event.iconId, iconList: iconModelList));
} catch (e) {
emit(const FailedState(error: 'Something went wrong'));
}
}
}

View File

@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
abstract class SettingEvent extends Equatable {
const SettingEvent();
@override
List<Object> get props => [];
}
class InitialEvent extends SettingEvent {
final String selectedIcon;
const InitialEvent({required this.selectedIcon});
@override
List<Object> get props => [selectedIcon];
}
class FetchIcons extends SettingEvent {
final bool expanded;
const FetchIcons({required this.expanded});
@override
List<Object> get props => [expanded];
}
class SelectIcon extends SettingEvent {
final String iconId;
const SelectIcon({required this.iconId});
@override
List<Object> get props => [iconId];
}

View File

@ -0,0 +1,56 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/routines/models/icon_model.dart';
abstract class SettingState extends Equatable {
const SettingState();
@override
List<Object> get props => [];
}
class LoadingState extends SettingState {
const LoadingState();
@override
List<Object> get props => [];
}
class InitialState extends SettingState {
const InitialState();
@override
List<Object> get props => [];
}
class IconLoadedState extends SettingState {
final List<String> status;
const IconLoadedState(this.status);
@override
List<Object> get props => [status];
}
class TabToRunSettingLoaded extends SettingState {
final String selectedIcon;
final List<IconModel> iconList;
final bool showInDevice;
const TabToRunSettingLoaded({
required this.selectedIcon,
required this.iconList,
required this.showInDevice,
});
@override
List<Object> get props => [selectedIcon, iconList, showInDevice];
}
class FailedState extends SettingState {
final String error;
const FailedState({required this.error});
@override
List<Object> get props => [error];
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ac_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
class DeviceDialogHelper {
static Future<Map<String, dynamic>?> showDeviceDialog(
BuildContext context,
Map<String, dynamic> data, {
required bool removeComparetors,
}) async {
final functions = data['functions'] as List<DeviceFunction>;
try {
final result = await _getDialogForDeviceType(
context,
data['productType'],
data,
functions,
removeComparetors: removeComparetors,
);
if (result != null) {
return result;
}
} catch (e) {
debugPrint('Error: $e');
}
return null;
}
static Future<Map<String, dynamic>?> _getDialogForDeviceType(BuildContext context,
String productType, Map<String, dynamic> data, List<DeviceFunction> functions,
{required bool removeComparetors}) async {
final routineBloc = context.read<RoutineBloc>();
final deviceSelectedFunctions =
routineBloc.state.selectedFunctions[data['uniqueCustomId']] ?? [];
switch (productType) {
case 'AC':
return ACHelper.showACFunctionsDialog(context, functions, data['device'],
deviceSelectedFunctions, data['uniqueCustomId'], removeComparetors);
case '1G':
return OneGangSwitchHelper.showSwitchFunctionsDialog(context, functions, data['device'],
deviceSelectedFunctions, data['uniqueCustomId'], removeComparetors);
case '2G':
return TwoGangSwitchHelper.showSwitchFunctionsDialog(context, functions, data['device'],
deviceSelectedFunctions, data['uniqueCustomId'], removeComparetors);
case '3G':
return ThreeGangSwitchHelper.showSwitchFunctionsDialog(context, functions, data['device'],
deviceSelectedFunctions, data['uniqueCustomId'], removeComparetors);
default:
return null;
}
}
}

View File

@ -0,0 +1,15 @@
class DurationFormatMixin {
static String formatDuration(int seconds) {
if (seconds >= 3600) {
final hours = (seconds / 3600).floor();
final remainingMinutes = ((seconds % 3600) / 60).floor();
final remainingSeconds = seconds % 60;
return '$hours h ${remainingMinutes}m ${remainingSeconds}s';
} else if (seconds >= 60) {
final minutes = (seconds / 60).floor();
final remainingSeconds = seconds % 60;
return '$minutes m ${remainingSeconds}s';
}
return '${seconds}s';
}
}

View File

@ -0,0 +1,180 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/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 {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: Container(
width: 600,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader('Create a scene: ${state.routineName ?? ""}'),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left side - IF
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'IF:',
style: TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 8),
if (state.isTabToRun)
ListTile(
leading: SvgPicture.asset(
Assets.tabToRun,
width: 24,
height: 24,
),
title: const Text('Tab to run'),
),
if (state.isAutomation)
...state.ifItems.map((item) {
final functions =
state.selectedFunctions[item['uniqueCustomId']] ?? [];
return ListTile(
leading: SvgPicture.asset(
item['imagePath'],
width: 22,
height: 22,
),
title:
Text(item['title'], style: const TextStyle(fontSize: 14)),
subtitle: Wrap(
children: functions
.map((f) => Text(
'${f.operationName}: ${f.value}, ',
style: const TextStyle(
color: ColorsManager.grayColor, fontSize: 8),
overflow: TextOverflow.ellipsis,
maxLines: 3,
))
.toList(),
),
);
}),
],
),
),
const SizedBox(width: 16),
// Right side - THEN items
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'THEN:',
style: TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 8),
...state.thenItems.map((item) {
final functions =
state.selectedFunctions[item['uniqueCustomId']] ?? [];
return ListTile(
leading: item['type'] == 'tap_to_run' || item['type'] == 'scene'
? 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,
),
),
subtitle: Wrap(
children: functions
.map((f) => Text(
'${f.operationName}: ${f.value}, ',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.grayColor, fontSize: 8),
overflow: TextOverflow.ellipsis,
maxLines: 3,
))
.toList(),
),
);
}),
],
),
),
],
),
),
// 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: () async {
if (state.isAutomation) {
if (state.isUpdate ?? false) {
context.read<RoutineBloc>().add(const UpdateAutomation());
} else {
context.read<RoutineBloc>().add(const CreateAutomationEvent());
}
} else {
if (state.isUpdate ?? false) {
context.read<RoutineBloc>().add(const UpdateScene());
} else {
context.read<RoutineBloc>().add(const CreateSceneEvent());
}
}
// if (state.errorMessage == null || state.errorMessage!.isEmpty) {
Navigator.pop(context);
// }
},
isConfirmEnabled: true,
),
],
),
),
);
},
);
},
);
}
}

View File

@ -0,0 +1,153 @@
import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart';
import 'package:syncrow_web/pages/routines/models/ac/ac_operational_value.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
abstract class ACFunction extends DeviceFunction<AcStatusModel> {
ACFunction({
required super.deviceId,
required super.deviceName,
required super.code,
required super.operationName,
required super.icon,
});
List<ACOperationalValue> getOperationalValues();
}
class SwitchFunction extends ACFunction {
SwitchFunction({required super.deviceId, required super.deviceName})
: super(
code: 'switch',
operationName: 'Power',
icon: Assets.assetsAcPower,
);
@override
List<ACOperationalValue> getOperationalValues() => [
ACOperationalValue(
icon: Assets.assetsAcPower,
description: "ON",
value: true,
),
ACOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: "OFF",
value: false,
),
];
}
class ModeFunction extends ACFunction {
ModeFunction({required super.deviceId, required super.deviceName})
: super(
code: 'mode',
operationName: 'Mode',
icon: Assets.assetsFreezing,
);
@override
List<ACOperationalValue> getOperationalValues() => [
ACOperationalValue(
icon: Assets.assetsAcCooling,
description: "Cooling",
value: TempModes.cold.name,
),
ACOperationalValue(
icon: Assets.assetsAcHeating,
description: "Heating",
value: TempModes.hot.name,
),
ACOperationalValue(
icon: Assets.assetsFanSpeed,
description: "Ventilation",
value: TempModes.wind.name,
),
];
}
class TempSetFunction extends ACFunction {
final int min;
final int max;
final int step;
TempSetFunction({required super.deviceId, required super.deviceName})
: min = 160,
max = 300,
step = 1,
super(
code: 'temp_set',
operationName: 'Set Temperature',
icon: Assets.assetsTempreture,
);
@override
List<ACOperationalValue> getOperationalValues() {
List<ACOperationalValue> values = [];
for (int temp = min; temp <= max; temp += step) {
values.add(ACOperationalValue(
icon: Assets.assetsTempreture,
description: "${temp / 10}°C",
value: temp,
));
}
return values;
}
}
class LevelFunction extends ACFunction {
LevelFunction({required super.deviceId, required super.deviceName})
: super(
code: 'level',
operationName: 'Fan Speed',
icon: Assets.assetsFanSpeed,
);
@override
List<ACOperationalValue> getOperationalValues() => [
ACOperationalValue(
icon: Assets.assetsAcFanLow,
description: "LOW",
value: FanSpeeds.low.name,
),
ACOperationalValue(
icon: Assets.assetsAcFanMiddle,
description: "MIDDLE",
value: FanSpeeds.middle.name,
),
ACOperationalValue(
icon: Assets.assetsAcFanHigh,
description: "HIGH",
value: FanSpeeds.high.name,
),
ACOperationalValue(
icon: Assets.assetsAcFanAuto,
description: "AUTO",
value: FanSpeeds.auto.name,
),
];
}
class ChildLockFunction extends ACFunction {
ChildLockFunction({required super.deviceId, required super.deviceName})
: super(
code: 'child_lock',
operationName: 'Child Lock',
icon: Assets.assetsChildLock,
);
@override
List<ACOperationalValue> getOperationalValues() => [
ACOperationalValue(
icon: Assets.assetsSceneChildLock,
description: "Lock",
value: true,
),
ACOperationalValue(
icon: Assets.assetsSceneChildUnlock,
description: "Unlock",
value: false,
),
];
}

View File

@ -0,0 +1,11 @@
class ACOperationalValue {
final String icon;
final String description;
final dynamic value;
ACOperationalValue({
required this.icon,
required this.description,
required this.value,
});
}

View File

@ -0,0 +1,199 @@
import 'dart:convert';
class CreateAutomationModel {
String spaceUuid;
String automationName;
String decisionExpr;
EffectiveTime effectiveTime;
List<Condition> conditions;
List<AutomationAction> actions;
CreateAutomationModel({
required this.spaceUuid,
required this.automationName,
required this.decisionExpr,
required this.effectiveTime,
required this.conditions,
required this.actions,
});
Map<String, dynamic> toMap([String? automationId]) {
return {
'spaceUuid': spaceUuid,
'automationName': automationName,
'decisionExpr': decisionExpr,
'effectiveTime': effectiveTime.toMap(),
'conditions': conditions.map((x) => x.toMap()).toList(),
'actions': actions.map((x) => x.toMap()).toList(),
};
}
factory CreateAutomationModel.fromMap(Map<String, dynamic> map) {
return CreateAutomationModel(
spaceUuid: map['spaceUuid'] ?? '',
automationName: map['automationName'] ?? '',
decisionExpr: map['decisionExpr'] ?? '',
effectiveTime: EffectiveTime.fromMap(map['effectiveTime']),
conditions: List<Condition>.from(
map['conditions']?.map((x) => Condition.fromMap(x)) ?? []),
actions: List<AutomationAction>.from(
map['actions']?.map((x) => AutomationAction.fromMap(x)) ?? []),
);
}
String toJson(String? automationId) => json.encode(toMap(automationId));
factory CreateAutomationModel.fromJson(String source) =>
CreateAutomationModel.fromMap(json.decode(source));
}
class EffectiveTime {
String start;
String end;
String loops;
EffectiveTime({
required this.start,
required this.end,
required this.loops,
});
Map<String, dynamic> toMap() {
return {
'start': start,
'end': end,
'loops': loops,
};
}
factory EffectiveTime.fromMap(Map<String, dynamic> map) {
return EffectiveTime(
start: map['start'] ?? '',
end: map['end'] ?? '',
loops: map['loops'] ?? '',
);
}
}
class Condition {
int code;
String entityId;
String entityType;
ConditionExpr expr;
Condition({
required this.code,
required this.entityId,
required this.entityType,
required this.expr,
});
Map<String, dynamic> toMap() {
return {
'code': code,
'entityId': entityId,
'entityType': 'device_report',
'expr': expr.toMap(),
};
}
factory Condition.fromMap(Map<String, dynamic> map) {
return Condition(
code: map['code']?.toInt() ?? 0,
entityId: map['entityId'] ?? '',
entityType: map['entityType'] ?? '',
expr: ConditionExpr.fromMap(map['expr']),
);
}
}
class ConditionExpr {
String statusCode;
String comparator;
dynamic statusValue;
ConditionExpr({
required this.statusCode,
required this.comparator,
required this.statusValue,
});
Map<String, dynamic> toMap() {
return {
'statusCode': statusCode,
'comparator': comparator,
'statusValue': statusValue,
};
}
factory ConditionExpr.fromMap(Map<String, dynamic> map) {
return ConditionExpr(
statusCode: map['statusCode'] ?? '',
comparator: map['comparator'] ?? '',
statusValue: map['statusValue'],
);
}
}
class AutomationAction {
String entityId;
String? actionType;
String actionExecutor;
ExecutorProperty? executorProperty;
AutomationAction({
required this.entityId,
this.actionType,
required this.actionExecutor,
this.executorProperty,
});
Map<String, dynamic> toMap() {
return {
'entityId': entityId,
'actionExecutor': actionExecutor,
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
? ExecutorProperty.fromMap(map['executorProperty'])
: null,
);
}
}
class ExecutorProperty {
String? functionCode;
dynamic functionValue;
int? delaySeconds;
ExecutorProperty({
this.functionCode,
this.functionValue,
this.delaySeconds,
});
Map<String, dynamic> toMap() {
return {
if (functionCode != null) 'functionCode': functionCode,
if (functionValue != null) 'functionValue': functionValue,
if (delaySeconds != null) 'delaySeconds': delaySeconds,
};
}
factory ExecutorProperty.fromMap(Map<String, dynamic> map) {
return ExecutorProperty(
functionCode: map['functionCode'],
functionValue: map['functionValue'],
delaySeconds: map['delaySeconds']?.toInt(),
);
}
}

View File

@ -0,0 +1,235 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
class CreateSceneModel {
String spaceUuid;
String iconId;
bool showInDevice;
String sceneName;
String decisionExpr;
List<CreateSceneAction> actions;
CreateSceneModel({
required this.spaceUuid,
required this.iconId,
required this.showInDevice,
required this.sceneName,
required this.decisionExpr,
required this.actions,
});
CreateSceneModel copyWith({
String? spaceUuid,
String? iconId,
bool? showInDevice,
String? sceneName,
String? decisionExpr,
List<CreateSceneAction>? actions,
bool? showInHomePage,
}) {
return CreateSceneModel(
spaceUuid: spaceUuid ?? this.spaceUuid,
iconId: iconId ?? this.iconId,
showInDevice: showInDevice ?? this.showInDevice,
sceneName: sceneName ?? this.sceneName,
decisionExpr: decisionExpr ?? this.decisionExpr,
actions: actions ?? this.actions,
);
}
Map<String, dynamic> toMap([String? sceneId]) {
return {
if (sceneId == null) 'spaceUuid': spaceUuid,
if (iconId.isNotEmpty) 'iconUuid': iconId,
'showInHomePage': showInDevice,
'sceneName': sceneName,
'decisionExpr': decisionExpr,
'actions': actions.map((x) => x.toMap()).toList(),
};
}
factory CreateSceneModel.fromMap(Map<String, dynamic> map) {
return CreateSceneModel(
spaceUuid: map['spaceUuid'] ?? '',
showInDevice: map['showInHomePage'] ?? false,
iconId: map['iconUuid'] ?? '',
sceneName: map['sceneName'] ?? '',
decisionExpr: map['decisionExpr'] ?? '',
actions: List<CreateSceneAction>.from(
map['actions']?.map((x) => CreateSceneAction.fromMap(x))),
);
}
String toJson([String? sceneId]) => json.encode(toMap(sceneId));
factory CreateSceneModel.fromJson(String source) =>
CreateSceneModel.fromMap(json.decode(source));
@override
String toString() {
return 'CreateSceneModel(unitUuid: $spaceUuid, sceneName: $sceneName, decisionExpr: $decisionExpr, actions: $actions)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CreateSceneModel &&
other.spaceUuid == spaceUuid &&
other.iconId == iconId &&
other.showInDevice == showInDevice &&
other.sceneName == sceneName &&
other.decisionExpr == decisionExpr &&
listEquals(other.actions, actions);
}
@override
int get hashCode {
return spaceUuid.hashCode ^
sceneName.hashCode ^
decisionExpr.hashCode ^
actions.hashCode;
}
}
class CreateSceneAction {
String entityId;
String? actionType;
String actionExecutor;
CreateSceneExecutorProperty? executorProperty;
CreateSceneAction({
this.actionType,
required this.entityId,
required this.actionExecutor,
required this.executorProperty,
});
CreateSceneAction copyWith({
String? entityId,
String? actionExecutor,
CreateSceneExecutorProperty? executorProperty,
}) {
return CreateSceneAction(
actionType: actionType ?? this.actionType,
entityId: entityId ?? this.entityId,
actionExecutor: actionExecutor ?? this.actionExecutor,
executorProperty: executorProperty ?? this.executorProperty,
);
}
Map<String, dynamic> toMap() {
if (executorProperty != null) {
return {
'entityId': entityId,
'actionExecutor': actionExecutor,
'executorProperty': executorProperty?.toMap(actionExecutor),
};
} else {
return {
"actionType": actionType,
'entityId': entityId,
'actionExecutor': actionExecutor,
};
}
}
factory CreateSceneAction.fromMap(Map<String, dynamic> map) {
return CreateSceneAction(
actionType: map['actionType'],
entityId: map['entityId'] ?? '',
actionExecutor: map['actionExecutor'] ?? '',
executorProperty:
CreateSceneExecutorProperty.fromMap(map['executorProperty']),
);
}
String toJson() => json.encode(toMap());
factory CreateSceneAction.fromJson(String source) =>
CreateSceneAction.fromMap(json.decode(source));
@override
String toString() =>
'CreateSceneAction(entityId: $entityId, actionExecutor: $actionExecutor, executorProperty: $executorProperty)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CreateSceneAction &&
other.entityId == entityId &&
other.actionExecutor == actionExecutor &&
other.executorProperty == executorProperty;
}
@override
int get hashCode =>
entityId.hashCode ^ actionExecutor.hashCode ^ executorProperty.hashCode;
}
class CreateSceneExecutorProperty {
String functionCode;
dynamic functionValue;
int delaySeconds;
CreateSceneExecutorProperty({
required this.functionCode,
required this.functionValue,
required this.delaySeconds,
});
CreateSceneExecutorProperty copyWith({
String? functionCode,
dynamic functionValue,
int? delaySeconds,
}) {
return CreateSceneExecutorProperty(
functionCode: functionCode ?? this.functionCode,
functionValue: functionValue ?? this.functionValue,
delaySeconds: delaySeconds ?? this.delaySeconds,
);
}
Map<String, dynamic> toMap(String actionExecutor) {
final map = <String, dynamic>{};
if (functionCode.isNotEmpty) map['functionCode'] = functionCode;
if (functionValue != null) map['functionValue'] = functionValue;
if (actionExecutor == 'delay' && delaySeconds > 0) {
map['delaySeconds'] = delaySeconds;
}
return map;
}
factory CreateSceneExecutorProperty.fromMap(Map<String, dynamic> map) {
return CreateSceneExecutorProperty(
functionCode: map['functionCode'] ?? '',
functionValue: map['functionValue'] ?? '',
delaySeconds: map['delaySeconds']?.toInt() ?? 0,
);
}
String toJson(String actionExecutor) => json.encode(toMap(actionExecutor));
factory CreateSceneExecutorProperty.fromJson(String source) =>
CreateSceneExecutorProperty.fromMap(json.decode(source));
@override
String toString() =>
'CreateSceneExecutorProperty(functionCode: $functionCode, functionValue: $functionValue, delaySeconds: $delaySeconds)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CreateSceneExecutorProperty &&
other.functionCode == functionCode &&
other.functionValue == functionValue &&
other.delaySeconds == delaySeconds;
}
@override
int get hashCode =>
functionCode.hashCode ^ functionValue.hashCode ^ delaySeconds.hashCode;
}

View File

@ -0,0 +1,28 @@
import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class DelayFunction extends BaseSwitchFunction {
DelayFunction({required super.deviceId, required super.deviceName})
: super(
code: 'delay',
operationName: 'Delay',
icon: Assets.delay,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: '',
description: "Duration in seconds",
value: 0.0,
minValue: 0,
maxValue: 43200,
stepValue: 1,
),
];
int convertToSeconds(int hours, int minutes) {
return (hours * 3600) + (minutes * 60);
}
}

View File

@ -0,0 +1,84 @@
abstract class DeviceFunction<T> {
final String deviceId;
final String deviceName;
final String code;
final String operationName;
final String icon;
DeviceFunction({
required this.deviceId,
required this.deviceName,
required this.code,
required this.operationName,
required this.icon,
});
}
class DeviceFunctionData {
final String entityId;
final String actionExecutor;
final String functionCode;
final String operationName;
final dynamic value;
final String? condition;
final String? valueDescription;
DeviceFunctionData({
required this.entityId,
this.actionExecutor = 'device_issue',
required this.functionCode,
required this.operationName,
required this.value,
this.condition,
this.valueDescription,
});
Map<String, dynamic> toJson() {
return {
'entityId': entityId,
'actionExecutor': actionExecutor,
'function': functionCode,
'operationName': operationName,
'value': value,
if (condition != null) 'condition': condition,
if (valueDescription != null) 'valueDescription': valueDescription,
};
}
factory DeviceFunctionData.fromJson(Map<String, dynamic> json) {
return DeviceFunctionData(
entityId: json['entityId'],
actionExecutor: json['actionExecutor'] ?? 'function',
functionCode: json['function'],
operationName: json['operationName'],
value: json['value'],
condition: json['condition'],
valueDescription: json['valueDescription'],
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DeviceFunctionData &&
other.entityId == entityId &&
other.actionExecutor == actionExecutor &&
other.functionCode == functionCode &&
other.operationName == operationName &&
other.value == value &&
other.condition == condition &&
other.valueDescription == valueDescription;
}
@override
int get hashCode {
return entityId.hashCode ^
actionExecutor.hashCode ^
functionCode.hashCode ^
operationName.hashCode ^
value.hashCode ^
condition.hashCode ^
valueDescription.hashCode;
}
}

View File

@ -0,0 +1,14 @@
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
abstract class BaseSwitchFunction extends DeviceFunction<bool> {
BaseSwitchFunction({
required super.deviceId,
required super.deviceName,
required super.code,
required super.operationName,
required super.icon,
});
List<SwitchOperationalValue> getOperationalValues();
}

View File

@ -0,0 +1,47 @@
import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class OneGangSwitchFunction extends BaseSwitchFunction {
OneGangSwitchFunction({required super.deviceId, required super.deviceName})
: super(
code: 'switch_1',
operationName: 'Light Switch',
icon: Assets.assetsAcPower,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: Assets.assetsAcPower,
description: "ON",
value: true,
),
SwitchOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: "OFF",
value: false,
),
];
}
class OneGangCountdownFunction extends BaseSwitchFunction {
OneGangCountdownFunction({required super.deviceId, required super.deviceName})
: super(
code: 'countdown_1',
operationName: 'Light Countdown',
icon: Assets.assetsLightCountdown,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: 43200,
stepValue: 1,
),
];
}

View File

@ -0,0 +1,17 @@
class SwitchOperationalValue {
final String icon;
final String description;
final dynamic value;
final double? minValue;
final double? maxValue;
final double? stepValue;
SwitchOperationalValue({
required this.icon,
required this.value,
this.description = '',
this.minValue,
this.maxValue,
this.stepValue,
});
}

View File

@ -0,0 +1,135 @@
import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class ThreeGangSwitch1Function extends BaseSwitchFunction {
ThreeGangSwitch1Function({required super.deviceId, required super.deviceName})
: super(
code: 'switch_1',
operationName: 'Light 1 Switch',
icon: Assets.assetsAcPower,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: Assets.assetsAcPower,
description: "ON",
value: true,
),
SwitchOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: "OFF",
value: false,
),
];
}
class ThreeGangCountdown1Function extends BaseSwitchFunction {
ThreeGangCountdown1Function({required super.deviceId, required super.deviceName})
: super(
code: 'countdown_1',
operationName: 'Light 1 Countdown',
icon: Assets.assetsLightCountdown,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: 43200,
stepValue: 1,
),
];
}
class ThreeGangSwitch2Function extends BaseSwitchFunction {
ThreeGangSwitch2Function({required super.deviceId, required super.deviceName})
: super(
code: 'switch_2',
operationName: 'Light 2 Switch',
icon: Assets.assetsAcPower,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: Assets.assetsAcPower,
description: "ON",
value: true,
),
SwitchOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: "OFF",
value: false,
),
];
}
class ThreeGangCountdown2Function extends BaseSwitchFunction {
ThreeGangCountdown2Function({required super.deviceId, required super.deviceName})
: super(
code: 'countdown_2',
operationName: 'Light 2 Countdown',
icon: Assets.assetsLightCountdown,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: 43200,
stepValue: 1,
),
];
}
class ThreeGangSwitch3Function extends BaseSwitchFunction {
ThreeGangSwitch3Function({required super.deviceId, required super.deviceName})
: super(
code: 'switch_3',
operationName: 'Light 3 Switch',
icon: Assets.assetsAcPower,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: Assets.assetsAcPower,
description: "ON",
value: true,
),
SwitchOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: "OFF",
value: false,
),
];
}
class ThreeGangCountdown3Function extends BaseSwitchFunction {
ThreeGangCountdown3Function({required super.deviceId, required super.deviceName})
: super(
code: 'countdown_3',
operationName: 'Light 3 Countdown',
icon: Assets.assetsLightCountdown,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: 43200,
stepValue: 1,
),
];
}

View File

@ -0,0 +1,91 @@
import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class TwoGangSwitch1Function extends BaseSwitchFunction {
TwoGangSwitch1Function({required super.deviceId, required super.deviceName})
: super(
code: 'switch_1',
operationName: 'Light 1 Switch',
icon: Assets.assetsAcPower,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: Assets.assetsAcPower,
description: "ON",
value: true,
),
SwitchOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: "OFF",
value: false,
),
];
}
class TwoGangSwitch2Function extends BaseSwitchFunction {
TwoGangSwitch2Function({required super.deviceId, required super.deviceName})
: super(
code: 'switch_2',
operationName: 'Light 2 Switch',
icon: Assets.assetsAcPower,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: Assets.assetsAcPower,
description: "ON",
value: true,
),
SwitchOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: "OFF",
value: false,
),
];
}
class TwoGangCountdown1Function extends BaseSwitchFunction {
TwoGangCountdown1Function({required super.deviceId, required super.deviceName})
: super(
code: 'countdown_1',
operationName: 'Light 1 Countdown',
icon: Assets.assetsLightCountdown,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: 43200,
stepValue: 1,
),
];
}
class TwoGangCountdown2Function extends BaseSwitchFunction {
TwoGangCountdown2Function({required super.deviceId, required super.deviceName})
: super(
code: 'countdown_2',
operationName: 'Light 2 Countdown',
icon: Assets.assetsLightCountdown,
);
@override
List<SwitchOperationalValue> getOperationalValues() => [
SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: 43200,
stepValue: 1,
),
];
}

View File

@ -0,0 +1,39 @@
import 'dart:convert';
import 'dart:typed_data';
class IconModel {
final String uuid;
final DateTime createdAt;
final DateTime updatedAt;
final String iconBase64;
IconModel({
required this.uuid,
required this.createdAt,
required this.updatedAt,
required this.iconBase64,
});
// Method to decode the icon from Base64 and return as Uint8List
Uint8List get iconBytes => base64Decode(iconBase64);
// Factory constructor to create an instance from JSON
factory IconModel.fromJson(Map<String, dynamic> json) {
return IconModel(
uuid: json['uuid'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
iconBase64: json['icon'] as String,
);
}
// Method to convert an instance back to JSON
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'icon': iconBase64,
};
}
}

View File

@ -0,0 +1,280 @@
import 'dart:convert';
import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/create_automation_model.dart';
import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/create_scene_model.dart';
class RoutineDetailsModel {
final String spaceUuid;
final String name;
final String decisionExpr;
final List<RoutineAction> actions;
final String? iconId;
final bool? showInDevice;
final EffectiveTime? effectiveTime;
final List<RoutineCondition>? conditions;
final String? type;
final String? sceneId;
final String? automationId;
RoutineDetailsModel({
required this.spaceUuid,
required this.name,
required this.decisionExpr,
required this.actions,
this.iconId,
this.showInDevice,
this.effectiveTime,
this.conditions,
this.type,
this.sceneId,
this.automationId,
});
// Convert to CreateSceneModel
CreateSceneModel toCreateSceneModel() {
return CreateSceneModel(
spaceUuid: spaceUuid,
iconId: iconId ?? '',
showInDevice: showInDevice ?? false,
sceneName: name,
decisionExpr: decisionExpr,
actions: actions.map((a) => a.toCreateSceneAction()).toList(),
);
}
// Convert to CreateAutomationModel
CreateAutomationModel toCreateAutomationModel() {
return CreateAutomationModel(
spaceUuid: spaceUuid,
automationName: name,
decisionExpr: decisionExpr,
effectiveTime: effectiveTime ?? EffectiveTime(start: '', end: '', loops: ''),
conditions: conditions?.map((c) => c.toCondition()).toList() ?? [],
actions: actions.map((a) => a.toAutomationAction()).toList(),
);
}
Map<String, dynamic> toMap() {
return {
'spaceUuid': spaceUuid,
'name': name,
'decisionExpr': decisionExpr,
'actions': actions.map((x) => x.toMap()).toList(),
if (iconId != null) 'iconUuid': iconId,
if (showInDevice != null) 'showInDevice': showInDevice,
if (effectiveTime != null) 'effectiveTime': effectiveTime!.toMap(),
if (conditions != null) 'conditions': conditions!.map((x) => x.toMap()).toList(),
if (type != null) 'type': type,
if (sceneId != null) 'sceneId': sceneId,
if (automationId != null) 'automationId': automationId,
};
}
factory RoutineDetailsModel.fromMap(Map<String, dynamic> map) {
return RoutineDetailsModel(
spaceUuid: map['spaceUuid'] ?? '',
name: map['name'] ?? '',
decisionExpr: map['decisionExpr'] ?? '',
actions: List<RoutineAction>.from(
map['actions']?.map((x) => RoutineAction.fromMap(x)) ?? [],
),
iconId: map['iconUuid'],
showInDevice: map['showInDevice'],
effectiveTime:
map['effectiveTime'] != null ? EffectiveTime.fromMap(map['effectiveTime']) : null,
conditions: map['conditions'] != null
? List<RoutineCondition>.from(map['conditions'].map((x) => RoutineCondition.fromMap(x)))
: null,
type: map['type'],
sceneId: map['sceneId'],
automationId: map['automationId'],
);
}
String toJson() => json.encode(toMap());
factory RoutineDetailsModel.fromJson(String source) =>
RoutineDetailsModel.fromMap(json.decode(source));
}
class RoutineAction {
final String entityId;
final String actionExecutor;
final String? name;
final RoutineExecutorProperty? executorProperty;
final String productType;
final String? type;
final String? icon;
RoutineAction(
{required this.entityId,
required this.actionExecutor,
required this.productType,
this.executorProperty,
this.name,
this.type,
this.icon});
CreateSceneAction toCreateSceneAction() {
return CreateSceneAction(
entityId: entityId,
actionExecutor: actionExecutor,
executorProperty: executorProperty?.toCreateSceneExecutorProperty(),
);
}
AutomationAction toAutomationAction() {
return AutomationAction(
entityId: entityId,
actionExecutor: actionExecutor,
executorProperty: executorProperty?.toExecutorProperty(),
);
}
Map<String, dynamic> toMap() {
return {
'entityId': entityId,
'actionExecutor': actionExecutor,
if (type != null) 'type': type,
if (name != null) 'name': name,
if (executorProperty != null) 'executorProperty': executorProperty!.toMap(),
};
}
factory RoutineAction.fromMap(Map<String, dynamic> map) {
return RoutineAction(
entityId: map['entityId'] ?? '',
actionExecutor: map['actionExecutor'] ?? '',
productType: map['productType'] ?? '',
name: map['name'] ?? '',
type: map['type'] ?? '',
executorProperty: map['executorProperty'] != null
? RoutineExecutorProperty.fromMap(map['executorProperty'])
: null,
icon: map['icon']);
}
}
class RoutineExecutorProperty {
final String? functionCode;
final dynamic functionValue;
final int? delaySeconds;
RoutineExecutorProperty({
this.functionCode,
this.functionValue,
this.delaySeconds,
});
CreateSceneExecutorProperty toCreateSceneExecutorProperty() {
return CreateSceneExecutorProperty(
functionCode: functionCode ?? '',
functionValue: functionValue,
delaySeconds: delaySeconds ?? 0,
);
}
ExecutorProperty toExecutorProperty() {
return ExecutorProperty(
functionCode: functionCode,
functionValue: functionValue,
delaySeconds: delaySeconds,
);
}
Map<String, dynamic> toMap() {
return {
if (functionCode != null) 'functionCode': functionCode,
if (functionValue != null) 'functionValue': functionValue,
if (delaySeconds != null) 'delaySeconds': delaySeconds,
};
}
factory RoutineExecutorProperty.fromMap(Map<String, dynamic> map) {
return RoutineExecutorProperty(
functionCode: map['functionCode'],
functionValue: map['functionValue'],
delaySeconds: map['delaySeconds']?.toInt(),
);
}
}
class RoutineCondition {
final int code;
final String entityId;
final String entityType;
final RoutineConditionExpr expr;
final String productType;
RoutineCondition({
required this.code,
required this.entityId,
required this.entityType,
required this.expr,
required this.productType,
});
Condition toCondition() {
return Condition(
code: code,
entityId: entityId,
entityType: entityType,
expr: expr.toConditionExpr(),
);
}
Map<String, dynamic> toMap() {
return {
'code': code,
'entityId': entityId,
'entityType': entityType,
'expr': expr.toMap(),
};
}
factory RoutineCondition.fromMap(Map<String, dynamic> map) {
return RoutineCondition(
code: map['code']?.toInt() ?? 0,
entityId: map['entityId'] ?? '',
entityType: map['entityType'] ?? '',
expr: RoutineConditionExpr.fromMap(map['expr']),
productType: map['productType'] ?? '',
);
}
}
class RoutineConditionExpr {
final String statusCode;
final String comparator;
final dynamic statusValue;
RoutineConditionExpr({
required this.statusCode,
required this.comparator,
required this.statusValue,
});
ConditionExpr toConditionExpr() {
return ConditionExpr(
statusCode: statusCode,
comparator: comparator,
statusValue: statusValue,
);
}
Map<String, dynamic> toMap() {
return {
'statusCode': statusCode,
'comparator': comparator,
'statusValue': statusValue,
};
}
factory RoutineConditionExpr.fromMap(Map<String, dynamic> map) {
return RoutineConditionExpr(
statusCode: map['statusCode'] ?? '',
comparator: map['comparator'] ?? '',
statusValue: map['statusValue'],
);
}
}

View File

@ -0,0 +1,30 @@
// import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
// class RoutineItem {
// final AllDevicesModel device;
// final String? function;
// final dynamic value;
// RoutineItem({
// required this.device,
// this.function,
// this.value,
// });
// Map<String, dynamic> toMap() {
// return {
// 'device': device,
// 'function': function,
// 'value': value,
// };
// }
// factory RoutineItem.fromMap(Map<String, dynamic> map) {
// return RoutineItem(
// device: map['device'] as AllDevicesModel,
// function: map['function'],
// value: map['value'],
// );
// }
// }
// : uniqueCustomId = uniqueCustomId ?? const Uuid().v4()

View File

@ -0,0 +1,56 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:syncrow_web/utils/constants/assets.dart';
class ScenesModel {
final String id;
final String? sceneTuyaId;
final String name;
final String status;
final String type;
final String? icon;
ScenesModel({
required this.id,
this.sceneTuyaId,
required this.name,
required this.status,
required this.type,
this.icon,
});
factory ScenesModel.fromRawJson(String str) =>
ScenesModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Uint8List? get iconInBytes {
if (icon == null || icon?.isEmpty == true) return null;
try {
return base64Decode(icon!);
} catch (e) {
return null;
}
}
factory ScenesModel.fromJson(Map<String, dynamic> json,
{bool? isAutomation}) {
return ScenesModel(
id: json["id"] ?? json["uuid"] ?? '',
sceneTuyaId: json["sceneTuyaId"] as String?,
name: json["name"] ?? '',
status: json["status"] ?? '',
type: json["type"] ?? '',
icon:
isAutomation == true ? Assets.automation : (json["icon"] as String?),
);
}
Map<String, dynamic> toJson() => {
"id": id,
"sceneTuyaId": sceneTuyaId ?? '',
"name": name,
"status": status,
"type": type,
};
}

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/routines/widgets/conditions_routines_devices_view.dart';
import 'package:syncrow_web/pages/routines/widgets/if_container.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_search_and_buttons.dart';
import 'package:syncrow_web/pages/routines/widgets/then_container.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateNewRoutineView extends StatelessWidget {
final bool isUpdate;
final String? routineId;
final bool isScene;
const CreateNewRoutineView({
super.key,
this.isUpdate = false,
this.routineId,
this.isScene = true,
});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const RoutineSearchAndButtons(),
const SizedBox(height: 20),
Flexible(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Card(
child: Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(15),
),
child: const ConditionsRoutinesDevicesView()),
),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
children: [
/// IF Container
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: Container(
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
),
),
child: const IfContainer(),
),
),
),
Container(
height: 2,
width: double.infinity,
color: ColorsManager.dialogBlueTitle,
),
/// THEN Container
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: Container(
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15),
bottomRight: Radius.circular(15),
),
),
child: const ThenContainer(),
),
),
),
],
),
),
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/effictive_period_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/period_option.dart';
import 'package:syncrow_web/pages/routines/widgets/repeat_days.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class EffectivePeriodView extends StatelessWidget {
const EffectivePeriodView({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Spacer(),
Expanded(
child: Text(
'Effective Period',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 14),
),
),
const Spacer(),
],
),
const Divider(
color: ColorsManager.backgroundColor,
),
const PeriodOptions(
showCustomTimePicker: EffectPeriodHelper.showCustomTimePicker,
),
const SizedBox(height: 16),
const RepeatDays(),
const SizedBox(height: 24),
],
),
);
}
}

View File

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/view/create_new_routine_view.dart';
import 'package:syncrow_web/pages/routines/widgets/main_routine_view/fetch_routine_scenes_automation.dart';
import 'package:syncrow_web/pages/routines/widgets/main_routine_view/routine_view_card.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/view/side_spaces_view.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/snack_bar.dart';
class RoutinesView extends StatefulWidget {
const RoutinesView({super.key});
@override
State<RoutinesView> createState() => _RoutinesViewState();
}
class _RoutinesViewState extends State<RoutinesView> {
@override
void initState() {
super.initState();
context.read<RoutineBloc>().add(FetchDevicesInRoutine());
}
@override
Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
if (state.createRoutineView) {
return const CreateNewRoutineView();
}
return Row(
children: [
Expanded(child: SideSpacesView(
onSelectAction: (String communityId, String spaceId) {
context.read<RoutineBloc>()
..add(LoadScenes(spaceId, communityId))
..add(LoadAutomation(spaceId));
},
)),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"Create New Routines",
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: ColorsManager.grayColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
height: 10,
),
RoutineViewCard(
onTap: () {
if (context.read<SpaceTreeBloc>().selectedCommunityId.isNotEmpty &&
context.read<SpaceTreeBloc>().selectedSpaceId.isNotEmpty) {
context.read<RoutineBloc>().add(
(ResetRoutineState()),
);
BlocProvider.of<RoutineBloc>(context).add(
const CreateNewRoutineViewEvent(createRoutineView: true),
);
} else {
CustomSnackBar.redSnackBar('Please select a space');
}
},
icon: Icons.add,
textString: '',
),
const SizedBox(
height: 15,
),
const Expanded(child: FetchRoutineScenesAutomation()),
],
),
],
),
),
),
],
);
},
);
}
}

View File

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/widgets/dragable_card.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_devices.dart';
import 'package:syncrow_web/pages/routines/widgets/routines_title_widget.dart';
import 'package:syncrow_web/pages/routines/widgets/scenes_and_automations.dart';
import 'package:syncrow_web/pages/routines/widgets/search_bar_condition_title.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class ConditionsRoutinesDevicesView extends StatelessWidget {
const ConditionsRoutinesDevicesView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConditionTitleAndSearchBar(),
SizedBox(height: 10),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
DraggableCard(
imagePath: Assets.tabToRun,
title: 'Tab to run',
deviceData: {
'deviceId': 'tab_to_run',
'type': 'trigger',
'name': 'Tab to run',
},
),
DraggableCard(
imagePath: Assets.map,
title: 'Location',
deviceData: {
'deviceId': 'location',
'type': 'trigger',
'name': 'Location',
},
),
DraggableCard(
imagePath: Assets.weather,
title: 'Weather',
deviceData: {
'deviceId': 'weather',
'type': 'trigger',
'name': 'Weather',
},
),
DraggableCard(
imagePath: Assets.schedule,
title: 'Schedule',
deviceData: {
'deviceId': 'schedule',
'type': 'trigger',
'name': 'Schedule',
},
),
],
),
SizedBox(height: 10),
TitleRoutine(
title: 'Conditions',
subtitle: '(THEN)',
),
SizedBox(height: 10),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
DraggableCard(
imagePath: Assets.notification,
title: 'Send Notification',
deviceData: {
'deviceId': 'notification',
'type': 'action',
'name': 'Send Notification',
},
),
DraggableCard(
imagePath: Assets.delay,
title: 'Delay the action',
deviceData: {
'deviceId': 'delay',
'type': 'action',
'name': 'Delay the action',
'uniqueCustomId': '',
},
),
],
),
SizedBox(height: 10),
TitleRoutine(
title: 'Routines',
subtitle: '(THEN)',
),
SizedBox(height: 10),
ScenesAndAutomations(),
SizedBox(height: 10),
TitleRoutine(
title: 'Devices',
subtitle: '',
),
SizedBox(height: 10),
RoutineDevices(),
],
),
),
);
},
);
}
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/custom_dialog.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DeleteSceneWidget extends StatelessWidget {
const DeleteSceneWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(
height: 10,
),
GestureDetector(
onTap: () async {
await showCustomDialog(
context: context,
message: 'Are you sure you want to delete this scene?',
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
alignment: AlignmentDirectional.center,
child: Text(
'Cancel',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textGray,
),
),
),
),
Container(width: 1, height: 50, color: ColorsManager.greyColor),
InkWell(
onTap: () {
context.read<RoutineBloc>().add(const DeleteScene());
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: Container(
alignment: AlignmentDirectional.center,
child: Text(
'Confirm',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
),
),
],
),
]);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.delete,
color: ColorsManager.red,
),
const SizedBox(
width: 2,
),
Text(
'Delete',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.red,
),
),
],
),
),
const SizedBox(
height: 10,
),
],
);
}
}

View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DialogFooter extends StatelessWidget {
final VoidCallback onCancel;
final VoidCallback? onConfirm;
final bool isConfirmEnabled;
final int? dialogWidth;
const DialogFooter({
Key? key,
required this.onCancel,
required this.onConfirm,
required this.isConfirmEnabled,
this.dialogWidth,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
color: ColorsManager.greyColor,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: _buildFooterButton(
context,
'Cancel',
onCancel,
),
),
if (isConfirmEnabled) ...[
Container(width: 1, height: 50, color: ColorsManager.greyColor),
Expanded(
child: _buildFooterButton(
context,
'Confirm',
onConfirm,
),
),
],
],
),
);
}
Widget _buildFooterButton(
BuildContext context,
String text,
VoidCallback? onTap,
) {
return GestureDetector(
onTap: onTap,
child: SizedBox(
height: 50,
child: Center(
child: Text(
text,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: text == 'Confirm'
? ColorsManager.primaryColorWithOpacity
: ColorsManager.textGray,
),
),
),
),
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DialogHeader extends StatelessWidget {
final String title;
const DialogHeader(this.title, {super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 10,
),
Text(
title,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold,
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 50),
child: Container(
height: 1,
width: double.infinity,
color: ColorsManager.greyColor,
),
),
],
);
}
}

View File

@ -0,0 +1,191 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DraggableCard extends StatelessWidget {
final String imagePath;
final String title;
final Map<String, dynamic> deviceData;
final EdgeInsetsGeometry? padding;
final void Function()? onRemove;
final bool? isFromThen;
final bool? isFromIf;
const DraggableCard({
super.key,
required this.imagePath,
required this.title,
required this.deviceData,
this.padding,
this.onRemove,
this.isFromThen,
this.isFromIf,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
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(
angle: -0.1,
child: _buildCardContent(context, deviceFunctions, padding: padding),
),
childWhenDragging: _buildGreyContainer(),
child: _buildCardContent(context, deviceFunctions, padding: padding),
);
},
);
}
Widget _buildCardContent(BuildContext context, List<DeviceFunctionData> deviceFunctions,
{EdgeInsetsGeometry? padding}) {
return Stack(
children: [
Card(
color: ColorsManager.whiteColors,
child: Container(
padding: padding ?? const EdgeInsets.all(16),
width: 110,
height: deviceFunctions.isEmpty ? 123 : null,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 50,
width: 50,
decoration: BoxDecoration(
color: ColorsManager.CircleImageBackground,
borderRadius: BorderRadius.circular(90),
border: Border.all(
color: ColorsManager.graysColor,
),
),
padding: const EdgeInsets.all(8),
child: deviceData['type'] == 'tap_to_run' || deviceData['type'] == 'scene'
? Image.memory(
base64Decode(deviceData['icon']),
)
: SvgPicture.asset(
imagePath,
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: Text(
deviceData['title'] ?? deviceData['name'] ?? title,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
),
),
),
],
),
if (deviceFunctions.isNotEmpty)
...deviceFunctions.map((function) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Text(
'${function.operationName}: ${_formatFunctionValue(function)}',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 9,
color: ColorsManager.textGray,
height: 1.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
)),
],
),
),
),
Positioned(
top: -4,
right: -6,
child: Visibility(
visible: (isFromIf ?? false) || (isFromThen ?? false),
child: IconButton(
onPressed: onRemove == null
? null
: () {
onRemove!();
},
icon: const Icon(
Icons.close,
color: ColorsManager.boxColor,
),
),
),
),
],
);
}
String _formatFunctionValue(DeviceFunctionData function) {
if (function.functionCode == 'temp_set' || function.functionCode == 'temp_current') {
return '${(function.value / 10).toStringAsFixed(0)}°C';
} else if (function.functionCode.contains('countdown')) {
final seconds = function.value.toInt();
if (seconds >= 3600) {
final hours = (seconds / 3600).floor();
final remainingMinutes = ((seconds % 3600) / 60).floor();
final remainingSeconds = seconds % 60;
return '$hours h ${remainingMinutes}m ${remainingSeconds}s';
} else if (seconds >= 60) {
final minutes = (seconds / 60).floor();
final remainingSeconds = seconds % 60;
return '$minutes m ${remainingSeconds}s';
}
return '${seconds}s';
}
return function.value.toString();
}
Widget _buildGreyContainer() {
return Container(
height: 123,
width: 90,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
),
);
}
}

View File

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/helper/dialog_helper/device_dialog_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/dragable_card.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';
import 'package:uuid/uuid.dart';
class IfContainer extends StatelessWidget {
const IfContainer({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
return DragTarget<Map<String, dynamic>>(
builder: (context, candidateData, rejectedData) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('IF', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
if (state.isAutomation && state.ifItems.isNotEmpty)
AutomationOperatorSelector(
selectedOperator: state.selectedAutomationOperator),
],
),
const SizedBox(height: 16),
if (state.isTabToRun)
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DraggableCard(
imagePath: Assets.tabToRun,
title: 'Tab to run',
deviceData: {},
),
],
),
if (!state.isTabToRun)
Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(
state.ifItems.length,
(index) => GestureDetector(
onTap: () async {
if (!state.isTabToRun) {
final result = await DeviceDialogHelper.showDeviceDialog(
context, state.ifItems[index],
removeComparetors: false);
if (result != null) {
context
.read<RoutineBloc>()
.add(AddToIfContainer(state.ifItems[index], false));
} else if (!['AC', '1G', '2G', '3G']
.contains(state.ifItems[index]['productType'])) {
context
.read<RoutineBloc>()
.add(AddToIfContainer(state.ifItems[index], false));
}
}
},
child: DraggableCard(
imagePath: state.ifItems[index]['imagePath'] ?? '',
title: state.ifItems[index]['title'] ?? '',
deviceData: state.ifItems[index],
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
isFromThen: false,
isFromIf: true,
onRemove: () {
context.read<RoutineBloc>().add(RemoveDragCard(
index: index,
isFromThen: false,
key: state.ifItems[index]['uniqueCustomId']));
},
),
)),
),
],
),
);
},
onAcceptWithDetails: (data) async {
final uniqueCustomId = const Uuid().v4();
final mutableData = Map<String, dynamic>.from(data.data);
mutableData['uniqueCustomId'] = uniqueCustomId;
if (state.isAutomation && mutableData['deviceId'] == 'tab_to_run') {
return;
}
if (!state.isTabToRun) {
if (mutableData['deviceId'] == 'tab_to_run') {
context.read<RoutineBloc>().add(AddToIfContainer(mutableData, true));
} else {
final result = await DeviceDialogHelper.showDeviceDialog(context, mutableData,
removeComparetors: false);
if (result != null) {
context.read<RoutineBloc>().add(AddToIfContainer(mutableData, false));
} else if (!['AC', '1G', '2G', '3G'].contains(mutableData['productType'])) {
context.read<RoutineBloc>().add(AddToIfContainer(mutableData, false));
}
}
}
},
);
},
);
}
}
class AutomationOperatorSelector extends StatelessWidget {
const AutomationOperatorSelector({
super.key,
required this.selectedOperator,
});
final String selectedOperator;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.dividerColor),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
TextButton(
style: TextButton.styleFrom(
backgroundColor: selectedOperator.toLowerCase() == 'or'
? ColorsManager.dialogBlueTitle
: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: Text(
'Any condition is met',
style: context.textTheme.bodyMedium?.copyWith(
color: selectedOperator.toLowerCase() == 'or'
? ColorsManager.whiteColors
: ColorsManager.blackColor,
),
),
onPressed: () {
context.read<RoutineBloc>().add(const ChangeAutomationOperator(operator: 'or'));
},
),
Container(
width: 3,
height: 24,
color: ColorsManager.dividerColor,
),
TextButton(
style: TextButton.styleFrom(
backgroundColor: selectedOperator.toLowerCase() == 'and'
? ColorsManager.dialogBlueTitle
: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: Text(
'All condition is met',
style: context.textTheme.bodyMedium?.copyWith(
color: selectedOperator.toLowerCase() == 'and'
? ColorsManager.whiteColors
: ColorsManager.blackColor,
),
),
onPressed: () {
context.read<RoutineBloc>().add(const ChangeAutomationOperator(operator: 'and'));
},
),
],
),
);
}
}

View File

@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/widgets/main_routine_view/routine_view_card.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.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';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class FetchRoutineScenesAutomation extends StatefulWidget {
const FetchRoutineScenesAutomation({super.key});
@override
State<FetchRoutineScenesAutomation> createState() => _FetchRoutineScenesState();
}
class _FetchRoutineScenesState extends State<FetchRoutineScenesAutomation>
with HelperResponsiveLayout {
@override
void initState() {
super.initState();
context.read<RoutineBloc>()
..add(LoadScenes(context.read<SpaceTreeBloc>().selectedSpaceId,
context.read<SpaceTreeBloc>().selectedCommunityId))
..add(LoadAutomation(context.read<SpaceTreeBloc>().selectedSpaceId));
}
@override
Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
return state.isLoading
? const Center(
child: CircularProgressIndicator(),
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Scenes (Tab to Run)",
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: ColorsManager.grayColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
if (state.scenes.isEmpty)
Text(
"No scenes found",
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.grayColor,
),
),
if (state.scenes.isNotEmpty)
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: isSmallScreenSize(context) ? 160 : 170,
maxWidth: MediaQuery.sizeOf(context).width * 0.7),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: state.scenes.length,
itemBuilder: (context, index) => Padding(
padding: EdgeInsets.only(
right: isSmallScreenSize(context) ? 4.0 : 8.0,
),
child: RoutineViewCard(
onTap: () {
BlocProvider.of<RoutineBloc>(context).add(
const CreateNewRoutineViewEvent(createRoutineView: true),
);
context.read<RoutineBloc>().add(
GetSceneDetails(
sceneId: state.scenes[index].id,
isTabToRun: true,
isUpdate: true,
),
);
},
textString: state.scenes[index].name,
icon: state.scenes[index].icon ?? Assets.logoHorizontal,
isFromScenes: true,
iconInBytes: state.scenes[index].iconInBytes,
),
),
),
),
const SizedBox(height: 15),
Text(
"Automations",
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: ColorsManager.grayColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
if (state.automations.isEmpty)
Text(
"No automations found",
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.grayColor,
),
),
if (state.automations.isNotEmpty)
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: isSmallScreenSize(context) ? 160 : 170,
maxWidth: MediaQuery.sizeOf(context).width * 0.7),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: state.automations.length,
itemBuilder: (context, index) => Padding(
padding: EdgeInsets.only(
right: isSmallScreenSize(context) ? 4.0 : 8.0,
),
child: RoutineViewCard(
onTap: () {
BlocProvider.of<RoutineBloc>(context).add(
const CreateNewRoutineViewEvent(createRoutineView: true),
);
context.read<RoutineBloc>().add(
GetAutomationDetails(
automationId: state.automations[index].id,
isAutomation: true,
isUpdate: true),
);
},
textString: state.automations[index].name,
icon: state.automations[index].icon ?? Assets.automation,
),
),
),
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.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';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class RoutineViewCard extends StatelessWidget with HelperResponsiveLayout {
const RoutineViewCard({
super.key,
required this.onTap,
required this.icon,
required this.textString,
this.isFromScenes,
this.iconInBytes,
});
final Function() onTap;
final dynamic icon;
final String textString;
final bool? isFromScenes;
final Uint8List? iconInBytes;
@override
Widget build(BuildContext context) {
final double cardWidth = isSmallScreenSize(context)
? 120
: isMediumScreenSize(context)
? 135
: 150;
final double cardHeight = isSmallScreenSize(context) ? 160 : 170;
final double iconSize = isSmallScreenSize(context)
? 50
: isMediumScreenSize(context)
? 60
: 70;
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: cardWidth,
maxHeight: cardHeight,
),
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
color: ColorsManager.whiteColors,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Center(
child: Container(
decoration: BoxDecoration(
color: ColorsManager.graysColor,
borderRadius: BorderRadius.circular(120),
border: Border.all(
color: ColorsManager.greyColor,
width: 2.0,
),
),
height: iconSize,
width: iconSize,
child: (isFromScenes ?? false)
? (iconInBytes != null && iconInBytes?.isNotEmpty == true)
? Image.memory(
iconInBytes!,
height: iconSize,
width: iconSize,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) => Image.asset(
Assets.logo,
height: iconSize,
width: iconSize,
fit: BoxFit.contain,
),
)
: Image.asset(
Assets.logo,
height: iconSize,
width: iconSize,
fit: BoxFit.contain,
)
: (icon is String && icon.endsWith('.svg'))
? SvgPicture.asset(
icon,
fit: BoxFit.contain,
)
: Icon(
icon,
color: ColorsManager.dialogBlueTitle,
size: isSmallScreenSize(context) ? 30 : 40,
),
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: Text(
textString,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: isSmallScreenSize(context) ? 10 : 12,
),
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_event.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_state.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/effictive_period_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
class PeriodOptions extends StatelessWidget {
final Future<List<String>?> Function(BuildContext) showCustomTimePicker;
const PeriodOptions({
required this.showCustomTimePicker,
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<EffectPeriodBloc, EffectPeriodState>(
builder: (context, state) {
return Column(
children: [
_buildRadioOption(context, EnumEffectivePeriodOptions.allDay, '24 Hours'),
_buildRadioOption(context, EnumEffectivePeriodOptions.daytime, 'Sunrise to Sunset'),
_buildRadioOption(context, EnumEffectivePeriodOptions.night, 'Sunset to Sunrise'),
ListTile(
contentPadding: EdgeInsets.zero,
onTap: () => showCustomTimePicker(context),
title: Text(
'Custom',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 14),
),
subtitle: state.customStartTime != null && state.customEndTime != null
? Text(
'${"${state.customStartTime}"} - ${"${state.customEndTime}"}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 10),
)
: Text(
'00:00 - 23:59',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 10),
),
trailing: Radio<EnumEffectivePeriodOptions>(
value: EnumEffectivePeriodOptions.custom,
groupValue: state.selectedPeriod,
onChanged: (value) async {
if (value != null) {
context.read<EffectPeriodBloc>().add(SetPeriod(value));
}
showCustomTimePicker(context);
},
),
),
],
);
},
);
}
Widget _buildRadioOption(
BuildContext context, EnumEffectivePeriodOptions value, String subtitle) {
return BlocBuilder<EffectPeriodBloc, EffectPeriodState>(
builder: (context, state) {
return ListTile(
contentPadding: EdgeInsets.zero,
onTap: () {
context.read<EffectPeriodBloc>().add(SetPeriod(value));
},
title: Text(
EffectPeriodHelper.formatEnumValue(value),
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor, fontWeight: FontWeight.w400, fontSize: 12),
),
subtitle: Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w400, fontSize: 10),
),
trailing: Radio<EnumEffectivePeriodOptions>(
value: value,
groupValue: state.selectedPeriod,
onChanged: (value) {
if (value != null) {
context.read<EffectPeriodBloc>().add(SetPeriod(value));
}
},
),
);
},
);
}
}

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_event.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_state.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class RepeatDays extends StatelessWidget {
const RepeatDays({super.key});
@override
Widget build(BuildContext context) {
final effectiveBloc = context.read<EffectPeriodBloc>();
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Repeat',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w400, fontSize: 14)),
const SizedBox(width: 8),
BlocBuilder<EffectPeriodBloc, EffectPeriodState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: effectiveBloc.daysMap.entries.map((entry) {
final day = entry.key;
final abbreviation = entry.value;
final dayIndex = effectiveBloc.getDayIndex(day);
final isSelected = state.selectedDaysBinary[dayIndex] == '1';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 3.0),
child: GestureDetector(
onTap: () {
effectiveBloc.add(ToggleDay(day));
},
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? Colors.grey : Colors.grey.shade300,
width: 1,
),
),
child: CircleAvatar(
radius: 15,
backgroundColor: Colors.white,
child: Text(
abbreviation,
style: TextStyle(
fontSize: 16,
color: isSelected ? Colors.grey : Colors.grey.shade300,
),
),
),
),
),
);
}).toList(),
),
const SizedBox(
height: 8,
),
if (state.selectedDaysBinary == '0000000')
Text(
'At least one day must be selected',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 14),
),
],
);
},
),
],
);
}
}

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/widgets/dragable_card.dart';
class RoutineDevices extends StatelessWidget {
const RoutineDevices({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
Future.delayed(const Duration(seconds: 1), () {
if (state.devices.isEmpty) {
return const Center(child: Text('No devices found'));
}
});
List<AllDevicesModel> deviceList = state.devices
.where((device) =>
device.productType == 'AC' ||
device.productType == '1G' ||
device.productType == '2G' ||
device.productType == '3G')
.toList();
return Wrap(
spacing: 10,
runSpacing: 10,
children: deviceList.asMap().entries.map((entry) {
final device = entry.value;
if (state.searchText != null && state.searchText!.isNotEmpty) {
return device.name!.toLowerCase().contains(state.searchText!.toLowerCase())
? DraggableCard(
imagePath: device.getDefaultIcon(device.productType),
title: device.name ?? '',
deviceData: {
'device': device,
'imagePath': device.getDefaultIcon(device.productType),
'title': device.name ?? '',
'deviceId': device.uuid,
'productType': device.productType,
'functions': device.functions,
'uniqueCustomId': '',
},
)
: Container();
} else {
return DraggableCard(
imagePath: device.getDefaultIcon(device.productType),
title: device.name ?? '',
deviceData: {
'device': device,
'imagePath': device.getDefaultIcon(device.productType),
'title': device.name ?? '',
'deviceId': device.uuid,
'productType': device.productType,
'functions': device.functions,
'uniqueCustomId': '',
},
);
}
}).toList(),
);
},
);
}
}

View File

@ -0,0 +1,411 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/ac/ac_function.dart';
import 'package:syncrow_web/pages/routines/models/ac/ac_operational_value.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
class ACHelper {
static Future<Map<String, dynamic>?> showACFunctionsDialog(
BuildContext context,
List<DeviceFunction> functions,
AllDevicesModel? device,
List<DeviceFunctionData>? deviceSelectedFunctions,
String uniqueCustomId,
bool? removeComparetors,
) async {
List<ACFunction> acFunctions = functions.whereType<ACFunction>().toList();
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext context) {
return BlocProvider(
create: (_) => FunctionBloc()..add(InitializeFunctions(deviceSelectedFunctions ?? [])),
child: AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) {
final selectedFunction = state.selectedFunction;
final selectedOperationName = state.selectedOperationName;
final selectedFunctionData =
state.addedFunctions.firstWhere((f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction ?? '',
operationName: '',
value: null,
));
return Container(
width: selectedFunction != null ? 600 : 360,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('AC Functions'),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Function list
SizedBox(
width: selectedFunction != null ? 320 : 360,
child: _buildFunctionsList(
context: context,
acFunctions: acFunctions,
onFunctionSelected: (functionCode, operationName) =>
context.read<FunctionBloc>().add(SelectFunction(
functionCode: functionCode,
operationName: operationName,
)),
),
),
// Value selector
if (selectedFunction != null)
Expanded(
child: _buildValueSelector(
context: context,
selectedFunction: selectedFunction,
selectedFunctionData: selectedFunctionData,
acFunctions: acFunctions,
device: device,
operationName: selectedOperationName ?? '',
removeComparators: removeComparetors,
),
),
],
),
),
DialogFooter(
onCancel: () {
Navigator.pop(context);
},
onConfirm: state.addedFunctions.isNotEmpty
? () {
/// add the functions to the routine bloc
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
uniqueCustomId,
),
);
// Return the device data to be added to the container
Navigator.pop(context, {
'deviceId': functions.first.deviceId,
});
}
: null,
isConfirmEnabled: selectedFunction != null,
),
],
),
);
},
),
),
);
},
).then((value) {
return value;
});
}
/// Build functions list for AC functions dialog
static Widget _buildFunctionsList({
required BuildContext context,
required List<ACFunction> acFunctions,
required Function(String, String) onFunctionSelected,
}) {
return ListView.separated(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: acFunctions.length,
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Divider(
color: ColorsManager.dividerColor,
),
),
itemBuilder: (context, index) {
final function = acFunctions[index];
return ListTile(
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
placeholderBuilder: (BuildContext context) => Container(
width: 24,
height: 24,
color: Colors.transparent,
),
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () => onFunctionSelected(
function.code,
function.operationName,
),
);
},
);
}
/// Build value selector for AC functions dialog
static Widget _buildValueSelector({
required BuildContext context,
required String selectedFunction,
required DeviceFunctionData? selectedFunctionData,
required List<ACFunction> acFunctions,
AllDevicesModel? device,
required String operationName,
bool? removeComparators,
}) {
if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') {
final initialValue = selectedFunctionData?.value ?? 250;
return _buildTemperatureSelector(
context: context,
initialValue: initialValue,
selectCode: selectedFunction,
currentCondition: selectedFunctionData?.condition,
device: device,
operationName: operationName,
selectedFunctionData: selectedFunctionData,
removeComparators: removeComparators,
);
}
final selectedFn = acFunctions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList(
context: context,
values: values,
selectedValue: selectedFunctionData?.value,
device: device,
operationName: operationName,
selectCode: selectedFunction,
selectedFunctionData: selectedFunctionData,
);
}
/// Build temperature selector for AC functions dialog
static Widget _buildTemperatureSelector({
required BuildContext context,
required dynamic initialValue,
required String? currentCondition,
required String selectCode,
AllDevicesModel? device,
required String operationName,
DeviceFunctionData? selectedFunctionData,
bool? removeComparators,
}) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (removeComparators != true)
_buildConditionToggle(
context,
currentCondition,
selectCode,
device,
operationName,
selectedFunctionData,
),
const SizedBox(height: 20),
_buildTemperatureDisplay(
context,
initialValue,
device,
operationName,
selectedFunctionData,
selectCode,
),
const SizedBox(height: 20),
_buildTemperatureSlider(
context,
initialValue,
device,
operationName,
selectedFunctionData,
selectCode,
),
],
);
}
/// Build condition toggle for AC functions dialog
static Widget _buildConditionToggle(
BuildContext context,
String? currentCondition,
String selectCode,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
// Function(String) onConditionChanged,
) {
final conditions = ["<", "==", ">"];
return ToggleButtons(
onPressed: (int index) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
condition: conditions[index],
value: selectedFunctionData?.value,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
selectedColor: Colors.white,
fillColor: ColorsManager.primaryColorWithOpacity,
color: ColorsManager.primaryColorWithOpacity,
constraints: const BoxConstraints(
minHeight: 40.0,
minWidth: 40.0,
),
isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: conditions.map((c) => Text(c)).toList(),
);
}
/// Build temperature display for AC functions dialog
static Widget _buildTemperatureDisplay(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode,
) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${(initialValue ?? 200) / 10}°C',
style: context.textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
);
}
static Widget _buildTemperatureSlider(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode,
) {
return Slider(
value: initialValue is int ? initialValue.toDouble() : 200.0,
min: 200,
max: 300,
divisions: 10,
label: '${((initialValue ?? 160) / 10).toInt()}°C',
onChanged: (value) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
);
}
static Widget _buildOperationalValuesList({
required BuildContext context,
required List<ACOperationalValue> values,
required dynamic selectedValue,
AllDevicesModel? device,
required String operationName,
required String selectCode,
DeviceFunctionData? selectedFunctionData,
// required Function(dynamic) onValueChanged,
}) {
return ListView.builder(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
final isSelected = selectedValue == value.value;
return ListTile(
leading: SvgPicture.asset(
value.icon,
width: 24,
height: 24,
placeholderBuilder: (BuildContext context) => Container(
width: 24,
height: 24,
color: Colors.transparent,
),
),
title: Text(
value.description,
style: context.textTheme.bodyMedium,
),
trailing: Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
size: 24,
color: isSelected ? ColorsManager.primaryColorWithOpacity : ColorsManager.textGray,
),
onTap: () {
if (!isSelected) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value.value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
}
},
);
},
);
}
}

View File

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
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
State<AutomationDialog> createState() => _AutomationDialogState();
}
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

@ -0,0 +1,85 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
class DelayHelper {
static Future<Map<String, dynamic>?> showDelayPickerDialog(
BuildContext context, Map<String, dynamic> data) async {
int hours = 0;
int minutes = 0;
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext context) {
final routineBloc = context.read<RoutineBloc>();
int totalSec = 0;
final selectedFunctionData =
routineBloc.state.selectedFunctions[data['uniqueCustomId']] ?? [];
if (selectedFunctionData.isNotEmpty) {
totalSec = selectedFunctionData[0].value;
// Convert seconds to hours and minutes
hours = totalSec ~/ 3600;
minutes = (totalSec % 3600) ~/ 60;
}
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: Container(
width: 600,
height: 300,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('Select Delay Duration'),
Expanded(
child: CupertinoTimerPicker(
mode: CupertinoTimerPickerMode.hm,
initialTimerDuration: Duration(hours: hours, minutes: minutes),
onTimerDurationChanged: (Duration newDuration) {
hours = newDuration.inHours;
minutes = newDuration.inMinutes % 60;
},
),
),
DialogFooter(
onCancel: () {
Navigator.of(context).pop();
},
onConfirm: () {
int totalSeconds = (hours * 3600) + (minutes * 60);
context.read<RoutineBloc>().add(AddFunctionToRoutine(
[
DeviceFunctionData(
entityId: 'delay',
functionCode: 'delay',
operationName: 'Delay',
value: totalSeconds,
)
],
data['uniqueCustomId'],
));
Navigator.pop(context, {
'deviceId': 'delay',
'value': totalSeconds,
});
},
isConfirmEnabled: true,
),
],
),
),
);
},
);
}
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DiscardDialog {
static void show(BuildContext context) {
context.customAlertDialog(
alertBody: Container(
height: 150,
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(
'If you close, you will lose all the changes you have made.',
textAlign: TextAlign.center,
style: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.red,
fontWeight: FontWeight.w400,
fontSize: 14,
),
),
const SizedBox(
height: 20,
),
Text(
'Are you sure you wish to close?',
style: context.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w400,
color: ColorsManager.grayColor,
),
)
],
)),
title: 'Discard',
titleStyle: context.textTheme.titleLarge!.copyWith(
color: ColorsManager.red,
fontWeight: FontWeight.bold,
),
onDismissText: "Dont Close",
onConfirmText: "Close",
onDismissColor: ColorsManager.grayColor,
onConfirmColor: ColorsManager.red.withOpacity(0.8),
onDismiss: () {
Navigator.pop(context);
},
onConfirm: () {
context.read<RoutineBloc>().add(ResetRoutineState());
Navigator.pop(context);
BlocProvider.of<RoutineBloc>(context).add(
const CreateNewRoutineViewEvent(createRoutineView: false),
);
BlocProvider.of<RoutineBloc>(context).add(
const TriggerSwitchTabsEvent(isRoutineTab: true),
);
});
}
}

View File

@ -0,0 +1,151 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_event.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:time_picker_spinner/time_picker_spinner.dart';
class EffectPeriodHelper {
static Future<List<String>?> showCustomTimePicker(BuildContext context) async {
String selectedStartTime = "00:00";
String selectedEndTime = "23:59";
PageController pageController = PageController(initialPage: 0);
DateTime startDateTime = DateTime(2022, 1, 1, 0, 0);
DateTime endDateTime = DateTime(2022, 1, 1, 23, 59);
context.customAlertDialog(
alertBody: SizedBox(
height: 250,
child: PageView(
controller: pageController,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildTimePickerPage(
context: context,
pageController: pageController,
isStartTime: true,
time: startDateTime,
onTimeChange: (time) {
selectedStartTime =
"${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}";
},
),
_buildTimePickerPage(
context: context,
pageController: pageController,
isStartTime: false,
time: endDateTime,
onTimeChange: (time) {
selectedEndTime =
"${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}";
},
),
],
),
),
title: "Custom",
onConfirm: () {
context.read<EffectPeriodBloc>().add(
SetCustomTime(selectedStartTime, selectedEndTime),
);
context.read<EffectPeriodBloc>().add(
const SetPeriod(EnumEffectivePeriodOptions.custom),
);
Navigator.of(context).pop();
},
);
return null;
}
static Widget _buildTimePickerPage({
required BuildContext context,
required PageController pageController,
required bool isStartTime,
required DateTime time,
required Function(DateTime) onTimeChange,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (!isStartTime)
TextButton(
onPressed: () {
pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
);
},
child: const Text('Start'),
),
TextButton(
onPressed: () {},
child: Text(isStartTime ? "Start" : "End",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 10)),
),
if (isStartTime)
TextButton(
onPressed: () {
pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
);
},
child: Text('End',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 10)),
),
],
),
),
TimePickerSpinner(
is24HourMode: false,
normalTextStyle: const TextStyle(
fontSize: 24,
color: Colors.grey,
),
highlightedTextStyle: const TextStyle(
fontSize: 24,
color: ColorsManager.primaryColor,
),
spacing: 20,
itemHeight: 50,
isForce2Digits: true,
time: time,
onTimeChange: onTimeChange,
),
const SizedBox(height: 16),
],
);
}
static String formatEnumValue(EnumEffectivePeriodOptions value) {
switch (value) {
case EnumEffectivePeriodOptions.allDay:
return "All Day";
case EnumEffectivePeriodOptions.daytime:
return "Daytime";
case EnumEffectivePeriodOptions.night:
return "Night";
case EnumEffectivePeriodOptions.custom:
return "Custom";
case EnumEffectivePeriodOptions.none:
return "None";
default:
return "";
}
}
}

View File

@ -0,0 +1,384 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/helper/duration_format_helper.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class OneGangSwitchHelper {
static Future<Map<String, dynamic>?> showSwitchFunctionsDialog(
BuildContext context,
List<DeviceFunction> functions,
AllDevicesModel? device,
List<DeviceFunctionData>? deviceSelectedFunctions,
String uniqueCustomId,
bool removeComparetors,
) async {
List<BaseSwitchFunction> acFunctions = functions.whereType<BaseSwitchFunction>().toList();
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext context) {
return BlocProvider(
create: (_) => FunctionBloc()..add(InitializeFunctions(deviceSelectedFunctions ?? [])),
child: AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) {
final selectedFunction = state.selectedFunction;
final selectedOperationName = state.selectedOperationName;
final selectedFunctionData =
state.addedFunctions.firstWhere((f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction ?? '',
operationName: '',
value: null,
));
return Container(
width: selectedFunction != null ? 600 : 360,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('1 Gang Light Switch Condition'),
Expanded(
child: Row(
children: [
// Left side: Function list
Expanded(
child: ListView.separated(
itemCount: acFunctions.length,
separatorBuilder: (_, __) => const Divider(
color: ColorsManager.dividerColor,
),
itemBuilder: (context, index) {
final function = acFunctions[index];
return ListTile(
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () {
context.read<FunctionBloc>().add(SelectFunction(
functionCode: function.code,
operationName: function.operationName,
));
},
);
},
),
),
// Right side: Value selector
if (selectedFunction != null)
Expanded(
child: _buildValueSelector(
context: context,
selectedFunction: selectedFunction,
selectedFunctionData: selectedFunctionData,
acFunctions: acFunctions,
device: device,
operationName: selectedOperationName ?? '',
removeComparetors: removeComparetors,
),
),
],
),
),
Container(
height: 1,
width: double.infinity,
color: ColorsManager.greyColor,
),
DialogFooter(
onCancel: () {
Navigator.pop(context);
},
onConfirm: state.addedFunctions.isNotEmpty
? () {
/// add the functions to the routine bloc
// for (var function in state.addedFunctions) {
// context.read<RoutineBloc>().add(
// AddFunctionToRoutine(
// function,
// uniqueCustomId,
// ),
// );
// }
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
uniqueCustomId,
),
);
// Return the device data to be added to the container
Navigator.pop(context, {
'deviceId': functions.first.deviceId,
});
}
: null,
isConfirmEnabled: selectedFunction != null,
),
],
),
);
},
),
));
},
);
}
static Widget _buildValueSelector({
required BuildContext context,
required String selectedFunction,
required DeviceFunctionData? selectedFunctionData,
required List<BaseSwitchFunction> acFunctions,
AllDevicesModel? device,
required String operationName,
required bool removeComparetors,
}) {
if (selectedFunction == 'countdown_1') {
final initialValue = selectedFunctionData?.value ?? 200;
return _buildCountDownSelector(
context: context,
initialValue: initialValue,
selectCode: selectedFunction,
currentCondition: selectedFunctionData?.condition,
device: device,
operationName: operationName,
selectedFunctionData: selectedFunctionData,
removeComparetors: removeComparetors,
);
}
final selectedFn = acFunctions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList(
context: context,
values: values,
selectedValue: selectedFunctionData?.value,
device: device,
operationName: operationName,
selectCode: selectedFunction,
selectedFunctionData: selectedFunctionData,
);
}
static Widget _buildCountDownSelector({
required BuildContext context,
required dynamic initialValue,
required String? currentCondition,
required String selectCode,
AllDevicesModel? device,
required String operationName,
DeviceFunctionData? selectedFunctionData,
required bool removeComparetors,
}) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (removeComparetors != true)
_buildConditionToggle(
context,
currentCondition,
selectCode,
device,
operationName,
selectedFunctionData,
),
const SizedBox(height: 20),
_buildCountDownDisplay(
context, initialValue, device, operationName, selectedFunctionData, selectCode),
const SizedBox(height: 20),
_buildCountDownSlider(
context, initialValue, device, operationName, selectedFunctionData, selectCode),
],
);
}
/// Build condition toggle for AC functions dialog
static Widget _buildConditionToggle(
BuildContext context,
String? currentCondition,
String selectCode,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
// Function(String) onConditionChanged,
) {
final conditions = ["<", "==", ">"];
return ToggleButtons(
onPressed: (int index) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
condition: conditions[index],
value: selectedFunctionData?.value,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
selectedColor: Colors.white,
fillColor: ColorsManager.primaryColorWithOpacity,
color: ColorsManager.primaryColorWithOpacity,
constraints: const BoxConstraints(
minHeight: 40.0,
minWidth: 40.0,
),
isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: conditions.map((c) => Text(c)).toList(),
);
}
/// Build temperature display for AC functions dialog
static Widget _buildCountDownDisplay(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0),
style: context.textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
);
}
static Widget _buildCountDownSlider(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode,
) {
final operationalValues = SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: 86400,
stepValue: 1,
);
return Slider(
value: (initialValue ?? 0).toDouble(),
min: operationalValues.minValue?.toDouble() ?? 0.0,
max: operationalValues.maxValue?.toDouble() ?? 0.0,
divisions: (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) /
(operationalValues.stepValue ?? 1))
.round(),
onChanged: (value) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
);
}
static Widget _buildOperationalValuesList({
required BuildContext context,
required List<SwitchOperationalValue> values,
required dynamic selectedValue,
AllDevicesModel? device,
required String operationName,
required String selectCode,
DeviceFunctionData? selectedFunctionData,
}) {
return ListView.builder(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
final isSelected = selectedValue == value.value;
return ListTile(
leading: SvgPicture.asset(
value.icon,
width: 24,
height: 24,
placeholderBuilder: (BuildContext context) => Container(
width: 24,
height: 24,
color: Colors.transparent,
),
),
title: Text(
value.description,
style: context.textTheme.bodyMedium,
),
trailing: Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
size: 24,
color: isSelected ? ColorsManager.primaryColorWithOpacity : ColorsManager.textGray,
),
onTap: () {
if (!isSelected) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value.value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
}
},
);
},
);
}
}

View File

@ -0,0 +1,431 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_event.dart';
import 'package:syncrow_web/pages/routines/bloc/effective_period/effect_period_state.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/setting_bloc/setting_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/setting_bloc/setting_event.dart';
import 'package:syncrow_web/pages/routines/bloc/setting_bloc/setting_state.dart';
import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/create_automation_model.dart';
import 'package:syncrow_web/pages/routines/models/icon_model.dart';
import 'package:syncrow_web/pages/routines/view/effective_period_view.dart';
import 'package:syncrow_web/pages/routines/widgets/delete_scene.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:flutter/cupertino.dart';
class SettingHelper {
static Future<String?> showSettingDialog({
required BuildContext context,
String? iconId,
}) async {
return showDialog<String>(
context: context,
builder: (BuildContext context) {
final isAutomation = context.read<RoutineBloc>().state.isAutomation;
final effectiveTime = context.read<RoutineBloc>().state.effectiveTime;
return MultiBlocProvider(
providers: [
if (effectiveTime != null)
BlocProvider(
create: (_) => EffectPeriodBloc()..add(InitialEffectPeriodEvent(effectiveTime)),
),
if (effectiveTime == null)
BlocProvider(
create: (_) => EffectPeriodBloc(),
),
BlocProvider(
create: (_) => SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? ''))),
],
child: AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<EffectPeriodBloc, EffectPeriodState>(
builder: (context, effectPeriodState) {
return BlocBuilder<SettingBloc, SettingState>(
builder: (context, settingState) {
String selectedIcon = '';
List<IconModel> list = [];
if (settingState is TabToRunSettingLoaded) {
selectedIcon = settingState.selectedIcon;
list = settingState.iconList;
}
return Container(
width: context.read<SettingBloc>().isExpanded ? 800 : 400,
height: context.read<SettingBloc>().isExpanded && isAutomation ? 500 : 350,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const DialogHeader('Settings'),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 400,
child: isAutomation
? Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.only(
top: 10, left: 10, right: 10, bottom: 10),
child: Column(
children: [
InkWell(
onTap: () {},
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'Validity',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color:
ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 14),
),
const Icon(
Icons.arrow_forward_ios_outlined,
color: ColorsManager.textGray,
size: 15,
)
],
),
),
const SizedBox(
height: 5,
),
const Divider(
color: ColorsManager.graysColor,
),
const SizedBox(
height: 5,
),
InkWell(
onTap: () {
BlocProvider.of<SettingBloc>(context).add(
FetchIcons(
expanded: !context
.read<SettingBloc>()
.isExpanded));
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'Effective Period',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color:
ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 14),
),
const Icon(
Icons.arrow_forward_ios_outlined,
color: ColorsManager.textGray,
size: 15,
)
],
),
),
const SizedBox(
height: 5,
),
const Divider(
color: ColorsManager.graysColor,
),
const SizedBox(
height: 5,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Executed by',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 14),
),
Text('Cloud',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ColorsManager.textGray,
fontWeight: FontWeight.w400,
fontSize: 14)),
],
),
if (context.read<RoutineBloc>().state.isUpdate ??
false)
const DeleteSceneWidget()
],
)),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.only(
top: 10, left: 10, right: 10, bottom: 10),
child: Column(
children: [
InkWell(
onTap: () {
BlocProvider.of<SettingBloc>(context).add(
FetchIcons(
expanded: !context
.read<SettingBloc>()
.isExpanded));
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'Icons',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color:
ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 14),
),
const Icon(
Icons.arrow_forward_ios_outlined,
color: ColorsManager.textGray,
size: 15,
)
],
),
),
const SizedBox(
height: 5,
),
const Divider(
color: ColorsManager.graysColor,
),
const SizedBox(
height: 5,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Show on devices page',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 14),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
height: 30,
width: 1,
color: ColorsManager.graysColor,
),
Transform.scale(
scale: .8,
child: CupertinoSwitch(
value: true,
onChanged: (value) {},
applyTheme: true,
),
),
],
)
],
),
const SizedBox(
height: 5,
),
const Divider(
color: ColorsManager.graysColor,
),
const SizedBox(
height: 5,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Executed by',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 14),
),
Text('Cloud',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ColorsManager.textGray,
fontWeight: FontWeight.w400,
fontSize: 14)),
],
),
if (context.read<RoutineBloc>().state.isUpdate ??
false)
const DeleteSceneWidget()
],
)),
],
),
),
if (context.read<SettingBloc>().isExpanded && !isAutomation)
SizedBox(
width: 400,
height: 150,
child: settingState is LoadingState
? const Center(child: CircularProgressIndicator())
: GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 6,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
shrinkWrap: true,
itemCount: list.length,
itemBuilder: (context, index) {
final iconModel = list[index];
return SizedBox(
width: 35,
height: 35,
child: InkWell(
onTap: () {
BlocProvider.of<SettingBloc>(context)
.add(SelectIcon(
iconId: iconModel.uuid,
));
selectedIcon = iconModel.uuid;
},
child: SizedBox(
child: ClipOval(
child: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
border: Border.all(
color: selectedIcon == iconModel.uuid
? ColorsManager
.primaryColorWithOpacity
: Colors.transparent,
width: 2,
),
shape: BoxShape.circle,
),
child: Image.memory(
iconModel.iconBytes,
),
),
),
),
),
);
},
)),
if (context.read<SettingBloc>().isExpanded && isAutomation)
const SizedBox(height: 350, width: 400, child: EffectivePeriodView())
],
),
Container(
width: MediaQuery.sizeOf(context).width,
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
color: ColorsManager.greyColor,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
alignment: AlignmentDirectional.center,
child: Text(
'Cancel',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textGray,
),
),
),
),
),
Container(width: 1, height: 50, color: ColorsManager.greyColor),
Expanded(
child: InkWell(
onTap: () {
if (isAutomation) {
BlocProvider.of<RoutineBloc>(context).add(
EffectiveTimePeriodEvent(EffectiveTime(
start: effectPeriodState.customStartTime!,
end: effectPeriodState.customEndTime!,
loops: effectPeriodState.selectedDaysBinary)));
Navigator.of(context).pop();
} else {
Navigator.of(context).pop(selectedIcon);
}
},
child: Container(
alignment: AlignmentDirectional.center,
child: Text(
'Confirm',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
),
),
),
],
),
)
],
),
);
},
);
}),
),
);
},
);
}
}

View File

@ -0,0 +1,386 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/helper/duration_format_helper.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ThreeGangSwitchHelper {
static Future<Map<String, dynamic>?> showSwitchFunctionsDialog(
BuildContext context,
List<DeviceFunction> functions,
AllDevicesModel? device,
List<DeviceFunctionData>? deviceSelectedFunctions,
String uniqueCustomId,
bool removeComparetors,
) async {
List<BaseSwitchFunction> switchFunctions = functions.whereType<BaseSwitchFunction>().toList();
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext context) {
return BlocProvider(
create: (_) => FunctionBloc()..add(InitializeFunctions(deviceSelectedFunctions ?? [])),
child: AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) {
final selectedFunction = state.selectedFunction;
final selectedOperationName = state.selectedOperationName;
final selectedFunctionData =
state.addedFunctions.firstWhere((f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction ?? '',
operationName: '',
value: null,
));
return Container(
width: selectedFunction != null ? 600 : 360,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('3 Gangs Light Switch Condition'),
Expanded(
child: Row(
children: [
// Left side: Function list
Expanded(
child: ListView.separated(
itemCount: switchFunctions.length,
separatorBuilder: (_, __) => const Divider(
color: ColorsManager.dividerColor,
),
itemBuilder: (context, index) {
final function = switchFunctions[index];
return ListTile(
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () {
context.read<FunctionBloc>().add(SelectFunction(
functionCode: function.code,
operationName: function.operationName,
));
},
);
},
),
),
// Right side: Value selector
if (selectedFunction != null)
Expanded(
child: _buildValueSelector(
context: context,
selectedFunction: selectedFunction,
selectedFunctionData: selectedFunctionData,
switchFunctions: switchFunctions,
device: device,
operationName: selectedOperationName ?? '',
removeComparetors: removeComparetors,
),
),
],
),
),
Container(
height: 1,
width: double.infinity,
color: ColorsManager.greyColor,
),
DialogFooter(
onCancel: () {
Navigator.pop(context);
},
onConfirm: state.addedFunctions.isNotEmpty
? () {
/// add the functions to the routine bloc
// for (var function in state.addedFunctions) {
// context.read<RoutineBloc>().add(
// AddFunctionToRoutine(
// function,
// uniqueCustomId,
// ),
// );
// }
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
uniqueCustomId,
),
);
// Return the device data to be added to the container
Navigator.pop(context, {
'deviceId': functions.first.deviceId,
});
}
: null,
isConfirmEnabled: selectedFunction != null,
),
],
),
);
},
),
));
},
);
}
static Widget _buildValueSelector({
required BuildContext context,
required String selectedFunction,
required DeviceFunctionData? selectedFunctionData,
required List<BaseSwitchFunction> switchFunctions,
AllDevicesModel? device,
required String operationName,
required bool removeComparetors,
}) {
if (selectedFunction == 'countdown_1' ||
selectedFunction == 'countdown_2' ||
selectedFunction == 'countdown_3') {
final initialValue = selectedFunctionData?.value ?? 200;
return _buildTemperatureSelector(
context: context,
initialValue: initialValue,
selectCode: selectedFunction,
currentCondition: selectedFunctionData?.condition,
device: device,
operationName: operationName,
selectedFunctionData: selectedFunctionData,
removeComparetors: removeComparetors,
);
}
final selectedFn = switchFunctions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList(
context: context,
values: values,
selectedValue: selectedFunctionData?.value,
device: device,
operationName: operationName,
selectCode: selectedFunction,
selectedFunctionData: selectedFunctionData,
);
}
static Widget _buildTemperatureSelector({
required BuildContext context,
required dynamic initialValue,
required String? currentCondition,
required String selectCode,
AllDevicesModel? device,
required String operationName,
DeviceFunctionData? selectedFunctionData,
bool? removeComparetors,
}) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (removeComparetors != true)
_buildConditionToggle(
context,
currentCondition,
selectCode,
device,
operationName,
selectedFunctionData,
),
const SizedBox(height: 20),
_buildCountDownDisplay(
context, initialValue, device, operationName, selectedFunctionData, selectCode),
const SizedBox(height: 20),
_buildCountDownSlider(
context, initialValue, device, operationName, selectedFunctionData, selectCode),
],
);
}
/// Build condition toggle for AC functions dialog
static Widget _buildConditionToggle(
BuildContext context,
String? currentCondition,
String selectCode,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
// Function(String) onConditionChanged,
) {
final conditions = ["<", "==", ">"];
return ToggleButtons(
onPressed: (int index) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
condition: conditions[index],
value: selectedFunctionData?.value,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
selectedColor: Colors.white,
fillColor: ColorsManager.primaryColorWithOpacity,
color: ColorsManager.primaryColorWithOpacity,
constraints: const BoxConstraints(
minHeight: 40.0,
minWidth: 40.0,
),
isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: conditions.map((c) => Text(c)).toList(),
);
}
/// Build temperature display for AC functions dialog
static Widget _buildCountDownDisplay(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0),
style: context.textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
);
}
static Widget _buildCountDownSlider(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode,
) {
final operationalValues = SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: 86400,
stepValue: 1,
);
return Slider(
value: (initialValue ?? 0).toDouble(),
min: operationalValues.minValue?.toDouble() ?? 0.0,
max: operationalValues.maxValue?.toDouble() ?? 0.0,
divisions: (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) /
(operationalValues.stepValue ?? 1))
.round(),
onChanged: (value) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
);
}
static Widget _buildOperationalValuesList({
required BuildContext context,
required List<SwitchOperationalValue> values,
required dynamic selectedValue,
AllDevicesModel? device,
required String operationName,
required String selectCode,
DeviceFunctionData? selectedFunctionData,
}) {
return ListView.builder(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
final isSelected = selectedValue == value.value;
return ListTile(
leading: SvgPicture.asset(
value.icon,
width: 24,
height: 24,
placeholderBuilder: (BuildContext context) => Container(
width: 24,
height: 24,
color: Colors.transparent,
),
),
title: Text(
value.description,
style: context.textTheme.bodyMedium,
),
trailing: Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
size: 24,
color: isSelected ? ColorsManager.primaryColorWithOpacity : ColorsManager.textGray,
),
onTap: () {
if (!isSelected) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value.value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
}
},
);
},
);
}
}

View File

@ -0,0 +1,384 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/helper/duration_format_helper.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class TwoGangSwitchHelper {
static Future<Map<String, dynamic>?> showSwitchFunctionsDialog(
BuildContext context,
List<DeviceFunction> functions,
AllDevicesModel? device,
List<DeviceFunctionData>? deviceSelectedFunctions,
String uniqueCustomId,
bool removeComparetors,
) async {
List<BaseSwitchFunction> switchFunctions = functions.whereType<BaseSwitchFunction>().toList();
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext context) {
return BlocProvider(
create: (_) => FunctionBloc()..add(InitializeFunctions(deviceSelectedFunctions ?? [])),
child: AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) {
final selectedFunction = state.selectedFunction;
final selectedOperationName = state.selectedOperationName;
final selectedFunctionData =
state.addedFunctions.firstWhere((f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction ?? '',
operationName: '',
value: null,
));
return Container(
width: selectedFunction != null ? 600 : 360,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('2 Gangs Light Switch Condition'),
Expanded(
child: Row(
children: [
// Left side: Function list
Expanded(
child: ListView.separated(
itemCount: switchFunctions.length,
separatorBuilder: (_, __) => const Divider(
color: ColorsManager.dividerColor,
),
itemBuilder: (context, index) {
final function = switchFunctions[index];
return ListTile(
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () {
context.read<FunctionBloc>().add(SelectFunction(
functionCode: function.code,
operationName: function.operationName,
));
},
);
},
),
),
// Right side: Value selector
if (selectedFunction != null)
Expanded(
child: _buildValueSelector(
context: context,
selectedFunction: selectedFunction,
selectedFunctionData: selectedFunctionData,
switchFunctions: switchFunctions,
device: device,
operationName: selectedOperationName ?? '',
removeComparetors: removeComparetors,
),
),
],
),
),
Container(
height: 1,
width: double.infinity,
color: ColorsManager.greyColor,
),
DialogFooter(
onCancel: () {
Navigator.pop(context);
},
onConfirm: state.addedFunctions.isNotEmpty
? () {
/// add the functions to the routine bloc
// for (var function in state.addedFunctions) {
// context.read<RoutineBloc>().add(
// AddFunctionToRoutine(
// function,
// uniqueCustomId
// ),
// );
// }
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
uniqueCustomId,
),
);
// Return the device data to be added to the container
Navigator.pop(context, {
'deviceId': functions.first.deviceId,
});
}
: null,
isConfirmEnabled: selectedFunction != null,
),
],
),
);
},
),
));
},
);
}
static Widget _buildValueSelector({
required BuildContext context,
required String selectedFunction,
required DeviceFunctionData? selectedFunctionData,
required List<BaseSwitchFunction> switchFunctions,
AllDevicesModel? device,
required String operationName,
required bool removeComparetors,
}) {
if (selectedFunction == 'countdown_1' || selectedFunction == 'countdown_2') {
final initialValue = selectedFunctionData?.value ?? 200;
return _buildTemperatureSelector(
context: context,
initialValue: initialValue,
selectCode: selectedFunction,
currentCondition: selectedFunctionData?.condition,
device: device,
operationName: operationName,
selectedFunctionData: selectedFunctionData,
removeComparetors: removeComparetors,
);
}
final selectedFn = switchFunctions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList(
context: context,
values: values,
selectedValue: selectedFunctionData?.value,
device: device,
operationName: operationName,
selectCode: selectedFunction,
selectedFunctionData: selectedFunctionData,
);
}
static Widget _buildTemperatureSelector({
required BuildContext context,
required dynamic initialValue,
required String? currentCondition,
required String selectCode,
AllDevicesModel? device,
required String operationName,
DeviceFunctionData? selectedFunctionData,
bool? removeComparetors,
}) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (removeComparetors != true)
_buildConditionToggle(
context,
currentCondition,
selectCode,
device,
operationName,
selectedFunctionData,
),
const SizedBox(height: 20),
_buildCountDownDisplay(
context, initialValue, device, operationName, selectedFunctionData, selectCode),
const SizedBox(height: 20),
_buildCountDownSlider(
context, initialValue, device, operationName, selectedFunctionData, selectCode),
],
);
}
/// Build condition toggle for AC functions dialog
static Widget _buildConditionToggle(
BuildContext context,
String? currentCondition,
String selectCode,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
// Function(String) onConditionChanged,
) {
final conditions = ["<", "==", ">"];
return ToggleButtons(
onPressed: (int index) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
condition: conditions[index],
value: selectedFunctionData?.value,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
selectedColor: Colors.white,
fillColor: ColorsManager.primaryColorWithOpacity,
color: ColorsManager.primaryColorWithOpacity,
constraints: const BoxConstraints(
minHeight: 40.0,
minWidth: 40.0,
),
isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: conditions.map((c) => Text(c)).toList(),
);
}
/// Build temperature display for AC functions dialog
static Widget _buildCountDownDisplay(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0),
style: context.textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
);
}
static Widget _buildCountDownSlider(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode,
) {
final operationalValues = SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: 86400,
stepValue: 1,
);
return Slider(
value: (initialValue ?? 0).toDouble(),
min: operationalValues.minValue?.toDouble() ?? 0.0,
max: operationalValues.maxValue?.toDouble() ?? 0.0,
divisions: (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) /
(operationalValues.stepValue ?? 1))
.round(),
onChanged: (value) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
);
}
static Widget _buildOperationalValuesList({
required BuildContext context,
required List<SwitchOperationalValue> values,
required dynamic selectedValue,
AllDevicesModel? device,
required String operationName,
required String selectCode,
DeviceFunctionData? selectedFunctionData,
}) {
return ListView.builder(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
final isSelected = selectedValue == value.value;
return ListTile(
leading: SvgPicture.asset(
value.icon,
width: 24,
height: 24,
placeholderBuilder: (BuildContext context) => Container(
width: 24,
height: 24,
color: Colors.transparent,
),
),
title: Text(
value.description,
style: context.textTheme.bodyMedium,
),
trailing: Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
size: 24,
color: isSelected ? ColorsManager.primaryColorWithOpacity : ColorsManager.textGray,
),
onTap: () {
if (!isSelected) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value.value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
}
},
);
},
);
}
}

View File

@ -0,0 +1,386 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/helper/save_routine_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/discard_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/setting_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
class RoutineSearchAndButtons extends StatefulWidget {
const RoutineSearchAndButtons({
super.key,
});
@override
State<RoutineSearchAndButtons> createState() => _RoutineSearchAndButtonsState();
}
class _RoutineSearchAndButtonsState extends State<RoutineSearchAndButtons> {
late TextEditingController _nameController;
@override
void initState() {
super.initState();
_nameController = TextEditingController();
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
_nameController.text = state.routineName ?? '';
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
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: [
Expanded(
child: Wrap(
spacing: 12,
runSpacing: 12,
crossAxisAlignment: WrapCrossAlignment.end,
children: [
ConstrainedBox(
constraints: BoxConstraints(
maxWidth:
constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('* ',
style: context.textTheme.bodyMedium!
.copyWith(color: ColorsManager.red, fontSize: 13)),
Text(
'Routine Name',
style: context.textTheme.bodyMedium!.copyWith(
fontSize: 13,
fontWeight: FontWeight.w600,
color: ColorsManager.blackColor,
),
),
],
),
Container(
width: 450,
height: 40,
decoration: containerWhiteDecoration,
child: TextFormField(
style: context.textTheme.bodyMedium!
.copyWith(color: ColorsManager.blackColor),
controller: _nameController,
decoration: InputDecoration(
hintText: 'Please enter the name',
hintStyle: context.textTheme.bodyMedium!
.copyWith(fontSize: 12, color: ColorsManager.grayColor),
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: InputBorder.none,
),
onTapOutside: (_) {
context
.read<RoutineBloc>()
.add(SetRoutineName(_nameController.text));
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'This field is required';
}
return null;
},
),
),
],
),
),
(constraints.maxWidth <= 1000)
? const SizedBox()
: SizedBox(
height: 40,
width: 200,
child: Center(
child: DefaultButton(
onPressed: state.isAutomation || state.isTabToRun
? () async {
final result = await SettingHelper.showSettingDialog(
context: context,
iconId: state.selectedIcon ?? '',
);
if (result != null) {
context
.read<RoutineBloc>()
.add(AddSelectedIcon(result));
}
}
: null,
borderRadius: 15,
elevation: 0,
borderColor: ColorsManager.greyColor,
backgroundColor: ColorsManager.boxColor,
child: const Text(
'Settings',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: ColorsManager.primaryColor,
),
),
),
),
),
],
),
),
if (constraints.maxWidth > 1000)
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 40,
width: 200,
child: Center(
child: DefaultButton(
onPressed: () {
DiscardDialog.show(context);
},
borderRadius: 15,
elevation: 0,
borderColor: ColorsManager.greyColor,
backgroundColor: ColorsManager.boxColor,
child: const Text(
'Cancel',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
),
),
),
const SizedBox(width: 12),
SizedBox(
height: 40,
width: 200,
child: Center(
child: DefaultButton(
onPressed: () async {
if (state.routineName == null || state.routineName!.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Please enter the routine name'),
duration: const Duration(seconds: 2),
backgroundColor: ColorsManager.red,
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {
// Optional action on Snackbar
},
),
),
);
return;
}
if (state.ifItems.isEmpty || state.thenItems.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Please add if and then condition'),
duration: const Duration(seconds: 2),
backgroundColor: ColorsManager.red,
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {
// Optional action on Snackbar
},
),
),
);
return;
}
// final result =
// await
BlocProvider.of<RoutineBloc>(context).add(ResetErrorMessage());
SaveRoutineHelper.showSaveRoutineDialog(context);
// if (result != null && result) {
// BlocProvider.of<RoutineBloc>(context).add(
// const CreateNewRoutineViewEvent(createRoutineView: false),
// );
// BlocProvider.of<RoutineBloc>(context).add(
// const TriggerSwitchTabsEvent(isRoutineTab: true),
// );
// }
},
borderRadius: 15,
elevation: 0,
backgroundColor: ColorsManager.primaryColor,
child: const Text(
'Save',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: ColorsManager.whiteColors,
),
),
),
),
),
],
),
],
),
if (constraints.maxWidth <= 1000)
Wrap(
runSpacing: 12,
children: [
SizedBox(
height: 40,
width: 200,
child: Center(
child: DefaultButton(
onPressed: state.isAutomation || state.isTabToRun
? () async {
final result = await SettingHelper.showSettingDialog(
context: context, iconId: state.selectedIcon ?? '');
if (result != null) {
context.read<RoutineBloc>().add(AddSelectedIcon(result));
}
}
: null,
borderRadius: 15,
elevation: 0,
borderColor: ColorsManager.greyColor,
backgroundColor: ColorsManager.boxColor,
child: const Text(
'Settings',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: ColorsManager.primaryColor,
),
),
),
),
),
const SizedBox(width: 12),
SizedBox(
height: 40,
width: 200,
child: Center(
child: DefaultButton(
onPressed: () {
DiscardDialog.show(context);
},
borderRadius: 15,
elevation: 0,
borderColor: ColorsManager.greyColor,
backgroundColor: ColorsManager.boxColor,
child: const Text(
'Cancel',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
),
),
),
const SizedBox(width: 12),
SizedBox(
height: 40,
width: 200,
child: Center(
child: DefaultButton(
onPressed: () async {
if (state.routineName == null || state.routineName!.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Please enter the routine name'),
duration: const Duration(seconds: 2),
backgroundColor: ColorsManager.red,
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {
// Optional action on Snackbar
},
),
),
);
return;
}
if (state.ifItems.isEmpty || state.thenItems.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Please add if and then condition'),
duration: const Duration(seconds: 2),
backgroundColor: ColorsManager.red,
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {
// Optional action on Snackbar
},
),
),
);
return;
}
// final result =
// await
BlocProvider.of<RoutineBloc>(context).add(ResetErrorMessage());
SaveRoutineHelper.showSaveRoutineDialog(context);
// if (result != null && result) {
// BlocProvider.of<RoutineBloc>(context).add(
// const CreateNewRoutineViewEvent(createRoutineView: false),
// );
// BlocProvider.of<RoutineBloc>(context).add(
// const TriggerSwitchTabsEvent(isRoutineTab: true),
// );
// }
},
borderRadius: 15,
elevation: 0,
backgroundColor: ColorsManager.primaryColor,
child: const Text(
'Save',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: ColorsManager.whiteColors,
),
),
),
),
),
],
),
],
);
},
);
},
);
}
}

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class TitleRoutine extends StatelessWidget {
const TitleRoutine({
super.key,
required this.title,
required this.subtitle,
});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: context.textTheme.titleLarge?.copyWith(
color: ColorsManager.greyColor,
),
),
const SizedBox(
width: 4,
),
Text(
subtitle,
style: context.textTheme.titleLarge?.copyWith(
color: ColorsManager.greyColor,
),
),
],
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/widgets/dragable_card.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class ScenesAndAutomations extends StatefulWidget {
const ScenesAndAutomations({
super.key,
});
@override
State<ScenesAndAutomations> createState() => _ScenesAndAutomationsState();
}
class _ScenesAndAutomationsState extends State<ScenesAndAutomations> {
@override
void initState() {
super.initState();
context.read<RoutineBloc>()
..add(LoadScenes(context.read<SpaceTreeBloc>().selectedSpaceId,
context.read<SpaceTreeBloc>().selectedCommunityId))
..add(LoadAutomation(context.read<SpaceTreeBloc>().selectedSpaceId));
}
@override
Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
if (!state.isLoading) {
var scenes = [...state.scenes, ...state.automations];
return Wrap(
spacing: 10,
runSpacing: 10,
children: scenes.asMap().entries.map((entry) {
final scene = entry.value;
if (state.searchText != null && state.searchText!.isNotEmpty) {
return scene.name.toLowerCase().contains(state.searchText!.toLowerCase())
? DraggableCard(
imagePath: scene.icon ?? Assets.loginLogo,
title: scene.name,
deviceData: {
'deviceId': scene.id,
'name': scene.name,
'status': scene.status,
'type': scene.type,
'icon': scene.icon,
},
)
: Container();
} else {
return DraggableCard(
imagePath: scene.icon ?? Assets.loginLogo,
title: scene.name,
deviceData: {
'deviceId': scene.id,
'name': scene.name,
'status': scene.status,
'type': scene.type,
'icon': scene.icon,
},
);
}
}).toList(),
);
}
return const Center(child: CircularProgressIndicator());
},
);
}
}

View File

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/widgets/routines_title_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class ConditionTitleAndSearchBar extends StatelessWidget with HelperResponsiveLayout {
const ConditionTitleAndSearchBar({
super.key,
});
@override
Widget build(BuildContext context) {
final isMedium = isMediumScreenSize(context);
final isSmall = isSmallScreenSize(context);
return isMedium || isSmall
? Wrap(
spacing: 10,
runSpacing: 10,
children: [
const TitleRoutine(title: 'Conditions', subtitle: '(IF)'),
StatefulTextField(
title: '',
width: 250,
height: 40,
hintText: 'Search',
elevation: 0,
borderRadius: 15,
icon: Icons.search,
hintColor: ColorsManager.grayColor,
boxDecoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(15),
),
controller: TextEditingController(),
// onSubmitted: (value) {
// context.read<RoutineBloc>().add(SearchRoutines(value));
// },
onChanged: (value) {
context.read<RoutineBloc>().add(SearchRoutines(value));
},
),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const TitleRoutine(title: 'Conditions', subtitle: '(IF)'),
StatefulTextField(
title: '',
width: 250,
height: 40,
hintText: 'Search',
elevation: 0,
borderRadius: 15,
icon: Icons.search,
hintColor: ColorsManager.grayColor,
boxDecoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(15),
),
controller: TextEditingController(),
// onSubmitted: (value) {
// context.read<RoutineBloc>().add(SearchRoutines(value));
// },
onChanged: (value) {
context.read<RoutineBloc>().add(SearchRoutines(value));
},
),
],
);
}
}

View File

@ -0,0 +1,201 @@
// lib/pages/routiens/widgets/then_container.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/automation_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/delay_dialog.dart';
import 'package:syncrow_web/pages/routines/helper/dialog_helper/device_dialog_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/dragable_card.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:uuid/uuid.dart';
class ThenContainer extends StatelessWidget {
const ThenContainer({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
return DragTarget<Map<String, dynamic>>(
builder: (context, candidateData, rejectedData) {
return SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(16),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('THEN', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
state.isLoading && state.isUpdate == true
? const Center(
child: CircularProgressIndicator(),
)
: Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(
state.thenItems.length,
(index) => GestureDetector(
onTap: () async {
if (state.thenItems[index]['deviceId'] == 'delay') {
final result = await DelayHelper.showDelayPickerDialog(
context, state.thenItems[index]);
if (result != null) {
context.read<RoutineBloc>().add(AddToThenContainer({
...state.thenItems[index],
'imagePath': Assets.delay,
'title': 'Delay',
}));
}
return;
}
if (state.thenItems[index]['type'] == 'automation') {
final result = await showDialog<bool>(
context: context,
builder: (BuildContext context) => AutomationDialog(
automationName:
state.thenItems[index]['name'] ?? 'Automation',
automationId:
state.thenItems[index]['deviceId'] ?? '',
uniqueCustomId: state.thenItems[index]
['uniqueCustomId'],
),
);
if (result != null) {
context.read<RoutineBloc>().add(AddToThenContainer({
...state.thenItems[index],
'imagePath': Assets.automation,
'title': state.thenItems[index]['name'] ??
state.thenItems[index]['title'],
}));
}
return;
}
final result = await DeviceDialogHelper.showDeviceDialog(
context, state.thenItems[index],
removeComparetors: true);
if (result != null) {
context
.read<RoutineBloc>()
.add(AddToThenContainer(state.thenItems[index]));
} else if (!['AC', '1G', '2G', '3G']
.contains(state.thenItems[index]['productType'])) {
context
.read<RoutineBloc>()
.add(AddToThenContainer(state.thenItems[index]));
}
},
child: DraggableCard(
imagePath: state.thenItems[index]['imagePath'] ?? '',
title: state.thenItems[index]['title'] ?? '',
deviceData: state.thenItems[index],
padding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
isFromThen: true,
isFromIf: false,
onRemove: () {
context.read<RoutineBloc>().add(RemoveDragCard(
index: index,
isFromThen: true,
key: state.thenItems[index]['uniqueCustomId']));
},
),
))),
],
),
),
);
},
onAcceptWithDetails: (data) async {
final uniqueCustomId = const Uuid().v4();
final mutableData = Map<String, dynamic>.from(data.data);
mutableData['uniqueCustomId'] = uniqueCustomId;
if (mutableData['type'] == 'scene') {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
return;
}
if (state.automationId == mutableData['deviceId'] ||
state.sceneId == mutableData['deviceId']) {
return;
}
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(
automationName: mutableData['name'] ?? 'Automation',
automationId: mutableData['deviceId'] ?? '',
uniqueCustomId: uniqueCustomId,
),
);
if (result != null) {
context.read<RoutineBloc>().add(AddToThenContainer({
...mutableData,
'imagePath': Assets.automation,
'title': mutableData['name'],
}));
}
return;
}
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': mutableData['imagePath'] ?? Assets.logo,
'title': mutableData['name'],
}));
return;
}
if (mutableData['type'] == 'tap_to_run' && !state.isAutomation) {
return;
}
if (mutableData['deviceId'] == 'delay') {
final result = await DelayHelper.showDelayPickerDialog(context, mutableData);
if (result != null) {
context.read<RoutineBloc>().add(AddToThenContainer({
...mutableData,
'imagePath': Assets.delay,
'title': 'Delay',
}));
}
return;
}
final result = await DeviceDialogHelper.showDeviceDialog(context, mutableData,
removeComparetors: true);
if (result != null) {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
} else if (!['AC', '1G', '2G', '3G'].contains(mutableData['productType'])) {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
}
},
);
},
);
}
}