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,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));
}
},
);
},
);
}
}