push value notifers

This commit is contained in:
ashrafzarkanisala
2024-11-22 19:55:16 +03:00
parent 1d6673b5b0
commit fb4a4d4d6c
14 changed files with 1173 additions and 1046 deletions

View File

@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart';
@ -14,7 +15,8 @@ import 'package:syncrow_web/utils/theme/theme.dart';
Future<void> main() async {
try {
const environment = String.fromEnvironment('FLAVOR', defaultValue: 'development');
const environment =
String.fromEnvironment('FLAVOR', defaultValue: 'development');
await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized();
initialSetup();
@ -46,10 +48,14 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider(
create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
)
),
BlocProvider<RoutineBloc>(
create: (context) => RoutineBloc(),
),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,

View File

@ -1,219 +1,330 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart';
import 'package:syncrow_web/pages/routiens/models/device_functions.dart';
import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ACHelper {
static Future<void> showACFunctionsDialog(
static Future<Map<String, dynamic>?> showACFunctionsDialog(
BuildContext context,
List<DeviceFunction<dynamic>> functions,
) async {
List<ACFunction> acFunctions = functions.whereType<ACFunction>().toList();
// Track multiple selections using a map
Map<String, dynamic> selectedValues = {};
List<DeviceFunctionData> selectedFunctions = [];
String? selectedFunction;
dynamic selectedValue = 20;
String? selectedCondition = "==";
List<bool> _selectedConditions = [false, true, false];
await showDialog(
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext context) {
return Dialog(
child: Container(
width: 600,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'AC Functions',
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,
),
),
Expanded(
child: Row(
children: [
Expanded(
child: ListView.separated(
itemCount: acFunctions.length,
separatorBuilder: (_, __) => const Divider(
color: ColorsManager.dividerColor,
),
itemBuilder: (context, index) {
final function = acFunctions[index];
final isSelected =
selectedValues.containsKey(function.code);
return ListTile(
tileColor:
isSelected ? Colors.grey.shade100 : null,
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: isSelected
? Icon(
Icons.check_circle,
color:
ColorsManager.primaryColorWithOpacity,
size: 20,
)
: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () {
if (isSelected) {
selectedValues.remove(function.code);
selectedFunctions.removeWhere(
(f) => f.function == function.code);
}
(context as Element).markNeedsBuild();
},
);
},
),
),
Expanded(
child: Builder(
builder: (context) {
final selectedFunction = acFunctions.firstWhere(
(f) => selectedValues.containsKey(f.code),
orElse: () => acFunctions.first,
);
return _buildValueSelector(
context,
selectedFunction,
selectedValues[selectedFunction.code],
(value) {
selectedValues[selectedFunction.code] = value;
// Update or add the function data
final functionData = DeviceFunctionData(
entityId: selectedFunction.deviceId,
function: selectedFunction.code,
operationName: selectedFunction.operationName,
value: value,
valueDescription: _getValueDescription(
selectedFunction, value),
);
final existingIndex =
selectedFunctions.indexWhere((f) =>
f.function == selectedFunction.code);
if (existingIndex != -1) {
selectedFunctions[existingIndex] =
functionData;
} else {
selectedFunctions.add(functionData);
}
(context as Element).markNeedsBuild();
},
);
},
),
),
],
),
),
Container(
height: 1,
width: double.infinity,
color: ColorsManager.greyColor,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancel',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.greyColor),
),
),
TextButton(
onPressed: selectedFunctions.isNotEmpty
? () {
// Add all selected functions to the bloc
for (final function in selectedFunctions) {
context
.read<RoutineBloc>()
.add(AddFunction(function));
}
Navigator.pop(context, true);
}
: null,
child: Text(
'Confirm',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
),
],
),
],
),
),
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: _buildDialogContent(
context,
setState,
acFunctions,
selectedFunction,
selectedValue,
selectedCondition,
_selectedConditions,
(fn) => selectedFunction = fn,
(val) => selectedValue = val,
(cond) => selectedCondition = cond,
),
);
},
);
},
);
}
static Widget _buildValueSelector(
/// Build dialog content for AC functions dialog
static Widget _buildDialogContent(
BuildContext context,
ACFunction function,
StateSetter setState,
List<ACFunction> acFunctions,
String? selectedFunction,
dynamic selectedValue,
String? selectedCondition,
List<bool> selectedConditions,
Function(String?) onFunctionSelected,
Function(dynamic) onValueSelected,
Function(String?) onConditionSelected,
) {
final values = function.getOperationalValues();
return Container(
height: 200,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: ListView.builder(
itemCount: values.length,
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'),
Flexible(
child: Row(
children: [
_buildFunctionsList(
context,
setState,
acFunctions,
selectedFunction,
onFunctionSelected,
),
if (selectedFunction != null)
_buildValueSelector(
context,
setState,
selectedFunction,
selectedValue,
selectedCondition,
selectedConditions,
onValueSelected,
onConditionSelected,
acFunctions,
),
],
),
),
DialogFooter(
onCancel: () => Navigator.pop(context),
onConfirm: selectedFunction != null && selectedValue != null
? () => Navigator.pop(context, {
'function': selectedFunction,
'value': selectedValue,
'condition': selectedCondition ?? "==",
})
: null,
isConfirmEnabled: selectedFunction != null && selectedValue != null,
),
],
),
);
}
/// Build functions list for AC functions dialog
static Widget _buildFunctionsList(
BuildContext context,
StateSetter setState,
List<ACFunction> acFunctions,
String? selectedFunction,
Function(String?) onFunctionSelected,
) {
return Expanded(
child: ListView.separated(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: acFunctions.length,
separatorBuilder: (context, index) => const Divider(
color: ColorsManager.dividerColor,
),
itemBuilder: (context, index) {
final value = values[index];
return RadioListTile<dynamic>(
value: value.value,
groupValue: selectedValue,
onChanged: onValueSelected,
title: Text(value.description),
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: () => setState(() => onFunctionSelected(function.code)),
);
},
),
);
}
static String _getValueDescription(ACFunction function, dynamic value) {
final values = function.getOperationalValues();
final selectedValue = values.firstWhere((v) => v.value == value);
return selectedValue.description;
/// Build value selector for AC functions dialog
static Widget _buildValueSelector(
BuildContext context,
StateSetter setState,
String selectedFunction,
dynamic selectedValue,
String? selectedCondition,
List<bool> selectedConditions,
Function(dynamic) onValueSelected,
Function(String?) onConditionSelected,
List<ACFunction> acFunctions,
) {
if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') {
return Expanded(
child: _buildTemperatureSelector(
context,
setState,
selectedValue,
selectedCondition,
selectedConditions,
onValueSelected,
onConditionSelected,
),
);
}
final selectedFn =
acFunctions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
return Expanded(
child: _buildOperationalValuesList(
context,
setState,
values,
selectedValue,
onValueSelected,
),
);
}
/// Build temperature selector for AC functions dialog
static Widget _buildTemperatureSelector(
BuildContext context,
StateSetter setState,
dynamic selectedValue,
String? selectedCondition,
List<bool> selectedConditions,
Function(dynamic) onValueSelected,
Function(String?) onConditionSelected,
) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildConditionToggle(
context,
setState,
selectedConditions,
onConditionSelected,
),
const SizedBox(height: 20),
_buildTemperatureDisplay(context, selectedValue),
const SizedBox(height: 20),
_buildTemperatureSlider(
context,
setState,
selectedValue,
onValueSelected,
),
],
);
}
/// Build condition toggle for AC functions dialog
static Widget _buildConditionToggle(
BuildContext context,
StateSetter setState,
List<bool> selectedConditions,
Function(String?) onConditionSelected,
) {
return ToggleButtons(
onPressed: (int index) {
setState(() {
for (int i = 0; i < selectedConditions.length; i++) {
selectedConditions[i] = i == index;
}
onConditionSelected(index == 0
? "<"
: index == 1
? "=="
: ">");
});
},
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: selectedConditions,
children: const [Text("<"), Text("="), Text(">")],
);
}
/// Build temperature display for AC functions dialog
static Widget _buildTemperatureDisplay(
BuildContext context, dynamic selectedValue) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${selectedValue ?? 20}°C',
style: context.textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
);
}
static Widget _buildTemperatureSlider(
BuildContext context,
StateSetter setState,
dynamic selectedValue,
Function(dynamic) onValueSelected,
) {
final currentValue = selectedValue is int ? selectedValue.toDouble() : 20.0;
return Slider(
value: currentValue,
min: 16,
max: 30,
divisions: 14,
label: '${currentValue.toInt()}°C',
onChanged: (value) {
setState(() => onValueSelected(value.toInt()));
},
);
}
static Widget _buildOperationalValuesList(
BuildContext context,
StateSetter setState,
List<dynamic> values,
dynamic selectedValue,
Function(dynamic) onValueSelected,
) {
return ListView.builder(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
return ListTile(
leading: SvgPicture.asset(
value.icon,
width: 24,
height: 24,
),
title: Text(
value.description,
style: context.textTheme.bodyMedium,
),
trailing: Radio<dynamic>(
value: value.value,
groupValue: selectedValue,
onChanged: (newValue) {
setState(() => onValueSelected(newValue));
},
),
);
},
);
}
}

View File

@ -1,34 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/models/device_functions.dart';
import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart';
import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart';
import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routiens/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<void> showSwitchFunctionsDialog(
BuildContext context, List<DeviceFunction<dynamic>> functions) async {
static Future<Map<String, dynamic>?> showSwitchFunctionsDialog(
BuildContext context,
List<DeviceFunction<dynamic>> functions,
) async {
List<DeviceFunction<dynamic>> switchFunctions = functions
.where(
(f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction)
.toList();
Map<String, dynamic> selectedValues = {};
List<DeviceFunctionData> selectedFunctions = [];
String? selectedCondition = "<";
List<bool> selectedConditions = [true, false, false];
final selectedFunctionNotifier = ValueNotifier<String?>(null);
final selectedValueNotifier = ValueNotifier<dynamic>(null);
final selectedConditionNotifier = ValueNotifier<String?>("<");
final selectedConditionsNotifier =
ValueNotifier<List<bool>>([true, false, false]);
await showDialog(
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
return ValueListenableBuilder<String?>(
valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: Container(
width: 600,
width: selectedFunction != null ? 600 : 360,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
@ -38,22 +42,7 @@ class OneGangSwitchHelper {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'1 Gang Light Switch Condition',
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,
),
),
const DialogHeader('1 Gang Light Switch Condition'),
Expanded(
child: Row(
children: [
@ -66,143 +55,67 @@ class OneGangSwitchHelper {
),
itemBuilder: (context, index) {
final function = switchFunctions[index];
final isSelected =
selectedValues.containsKey(function.code);
return ListTile(
tileColor:
isSelected ? Colors.grey.shade100 : null,
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: isSelected
? Icon(
Icons.check_circle,
color: ColorsManager
.primaryColorWithOpacity,
size: 20,
)
: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () {
if (isSelected) {
selectedValues.remove(function.code);
selectedFunctions.removeWhere(
(f) => f.function == function.code);
}
(context as Element).markNeedsBuild();
},
);
},
),
),
// Right side: Value selector
Expanded(
child: Builder(
builder: (context) {
final selectedFn = switchFunctions.firstWhere(
(f) => selectedValues.containsKey(f.code),
orElse: () => switchFunctions.first,
) as BaseSwitchFunction;
if (selectedFn is OneGangCountdownFunction) {
return _buildCountDownSelector(
context,
setState,
selectedValues[selectedFn.code] ?? 0,
selectedCondition,
selectedConditions,
(value) {
selectedValues[selectedFn.code] = value;
final functionData = DeviceFunctionData(
entityId: selectedFn.deviceId,
function: selectedFn.code,
operationName: selectedFn.operationName,
value: value,
condition: selectedCondition,
valueDescription: '${value} sec',
);
final existingIndex =
selectedFunctions.indexWhere((f) =>
f.function == selectedFn.code);
if (existingIndex != -1) {
selectedFunctions[existingIndex] =
functionData;
} else {
selectedFunctions.add(functionData);
}
(context as Element).markNeedsBuild();
},
(condition) {
setState(() {
selectedCondition = condition;
});
},
);
}
final values =
selectedFn.getOperationalValues();
return ListView.builder(
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
return ValueListenableBuilder<String?>(
valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
final isSelected =
selectedFunction == function.code;
return ListTile(
tileColor: isSelected
? Colors.grey.shade100
: null,
leading: SvgPicture.asset(
value.icon,
function.icon,
width: 24,
height: 24,
),
title: Text(
value.description,
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: Radio<dynamic>(
value: value.value,
groupValue:
selectedValues[selectedFn.code],
onChanged: (newValue) {
selectedValues[selectedFn.code] =
newValue;
final functionData =
DeviceFunctionData(
entityId: selectedFn.deviceId,
function: selectedFn.code,
operationName:
selectedFn.operationName,
value: newValue,
valueDescription: value.description,
);
final existingIndex =
selectedFunctions.indexWhere(
(f) =>
f.function ==
selectedFn.code);
if (existingIndex != -1) {
selectedFunctions[existingIndex] =
functionData;
} else {
selectedFunctions.add(functionData);
}
(context as Element).markNeedsBuild();
},
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () {
selectedFunctionNotifier.value =
function.code;
selectedValueNotifier.value =
function is OneGangCountdownFunction
? 0
: null;
},
);
},
);
},
),
),
// Right side: Value selector
if (selectedFunction != null)
ValueListenableBuilder<String?>(
valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
final selectedFn = switchFunctions.firstWhere(
(f) => f.code == selectedFunction,
);
return Expanded(
child: selectedFn is OneGangCountdownFunction
? _buildCountDownSelector(
context,
selectedValueNotifier,
selectedConditionNotifier,
selectedConditionsNotifier,
)
: _buildOperationalValuesList(
context,
selectedFn as BaseSwitchFunction,
selectedValueNotifier,
),
);
},
),
],
),
),
@ -211,41 +124,36 @@ class OneGangSwitchHelper {
width: double.infinity,
color: ColorsManager.greyColor,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancel',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.greyColor),
),
),
TextButton(
onPressed: selectedFunctions.isNotEmpty
? () {
for (final function in selectedFunctions) {
context
.read<RoutineBloc>()
.add(AddFunction(function));
}
Navigator.pop(context, true);
}
: null,
child: Text(
'Confirm',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
),
],
DialogFooter(
onCancel: () => Navigator.pop(context),
onConfirm: selectedFunctionNotifier.value != null &&
selectedValueNotifier.value != null
? () {
final selectedFn = switchFunctions.firstWhere(
(f) => f.code == selectedFunctionNotifier.value,
);
final value = selectedValueNotifier.value;
final functionData = DeviceFunctionData(
entityId: selectedFn.deviceId,
function: selectedFn.code,
operationName: selectedFn.operationName,
value: value,
condition: selectedConditionNotifier.value,
valueDescription:
selectedFn is OneGangCountdownFunction
? '${value} sec'
: ((selectedFn as BaseSwitchFunction)
.getOperationalValues()
.firstWhere((v) => v.value == value)
.description),
);
Navigator.pop(
context, {selectedFn.code: functionData});
}
: null,
isConfirmEnabled:
selectedFunctionNotifier.value != null &&
selectedValueNotifier.value != null,
),
],
),
@ -257,77 +165,101 @@ class OneGangSwitchHelper {
);
}
/// Build countdown selector for switch functions dialog
static Widget _buildCountDownSelector(
BuildContext context,
StateSetter setState,
dynamic selectedValue,
String? selectedCondition,
List<bool> selectedConditions,
Function(dynamic) onValueSelected,
Function(String?) onConditionSelected,
ValueNotifier<dynamic> valueNotifier,
ValueNotifier<String?> conditionNotifier,
ValueNotifier<List<bool>> conditionsNotifier,
) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildConditionToggle(
context,
setState,
selectedConditions,
onConditionSelected,
),
const SizedBox(height: 20),
Text(
'${selectedValue.toString()} sec',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 20),
Slider(
value: selectedValue.toDouble(),
min: 0,
max: 300, // 5 minutes in seconds
divisions: 300,
onChanged: (value) {
setState(() {
onValueSelected(value.toInt());
});
},
),
],
return ValueListenableBuilder<dynamic>(
valueListenable: valueNotifier,
builder: (context, value, _) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ValueListenableBuilder<List<bool>>(
valueListenable: conditionsNotifier,
builder: (context, selectedConditions, _) {
return ToggleButtons(
onPressed: (int index) {
final newConditions = List<bool>.filled(3, false);
newConditions[index] = true;
conditionsNotifier.value = newConditions;
conditionNotifier.value = index == 0
? "<"
: index == 1
? "=="
: ">";
},
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: selectedConditions,
children: const [Text("<"), Text("="), Text(">")],
);
},
),
const SizedBox(height: 20),
Text(
'${value ?? 0} sec',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 20),
Slider(
value: (value ?? 0).toDouble(),
min: 0,
max: 300,
divisions: 300,
onChanged: (newValue) {
valueNotifier.value = newValue.toInt();
},
),
],
);
},
);
}
/// Build condition toggle for AC functions dialog
static Widget _buildConditionToggle(
static Widget _buildOperationalValuesList(
BuildContext context,
StateSetter setState,
List<bool> selectedConditions,
Function(String?) onConditionSelected,
BaseSwitchFunction function,
ValueNotifier<dynamic> valueNotifier,
) {
return ToggleButtons(
onPressed: (int index) {
setState(() {
for (int i = 0; i < selectedConditions.length; i++) {
selectedConditions[i] = i == index;
}
onConditionSelected(index == 0
? "<"
: index == 1
? "=="
: ">");
});
final values = function.getOperationalValues();
return ValueListenableBuilder<dynamic>(
valueListenable: valueNotifier,
builder: (context, selectedValue, _) {
return ListView.builder(
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
return ListTile(
leading: SvgPicture.asset(
value.icon,
width: 24,
height: 24,
),
title: Text(
value.description,
style: context.textTheme.bodyMedium,
),
trailing: Radio<dynamic>(
value: value.value,
groupValue: selectedValue,
onChanged: (newValue) {
valueNotifier.value = newValue;
},
),
);
},
);
},
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: selectedConditions,
children: const [Text("<"), Text("="), Text(">")],
);
}
}

View File

@ -5,6 +5,8 @@ import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/models/device_functions.dart';
import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart';
import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart';
import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -18,20 +20,23 @@ class TwoGangSwitchHelper {
f is TwoGangCountdown1Function ||
f is TwoGangCountdown2Function)
.toList();
Map<String, dynamic> selectedValues = {};
List<DeviceFunctionData> selectedFunctions = [];
String? selectedCondition = "<";
List<bool> selectedConditions = [true, false, false];
final selectedFunctionNotifier = ValueNotifier<String?>(null);
final selectedValueNotifier = ValueNotifier<dynamic>(null);
final selectedConditionNotifier = ValueNotifier<String?>('<');
final selectedConditionsNotifier =
ValueNotifier<List<bool>>([true, false, false]);
await showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
return ValueListenableBuilder<String?>(
valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: Container(
width: 600,
width: selectedFunction != null ? 600 : 300,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
@ -41,22 +46,7 @@ class TwoGangSwitchHelper {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'2 Gangs Light Switch Condition',
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,
),
),
const DialogHeader('2 Gangs Light Switch Condition'),
Expanded(
child: Row(
children: [
@ -69,187 +59,105 @@ class TwoGangSwitchHelper {
),
itemBuilder: (context, index) {
final function = switchFunctions[index];
final isSelected =
selectedValues.containsKey(function.code);
return ListTile(
tileColor:
isSelected ? Colors.grey.shade100 : null,
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: isSelected
? Icon(
Icons.check_circle,
color: ColorsManager
.primaryColorWithOpacity,
size: 20,
)
: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () {
if (isSelected) {
selectedValues.remove(function.code);
selectedFunctions.removeWhere(
(f) => f.function == function.code);
}
(context as Element).markNeedsBuild();
},
);
},
),
),
// Right side: Value selector
Expanded(
child: Builder(
builder: (context) {
final selectedFn = switchFunctions.firstWhere(
(f) => selectedValues.containsKey(f.code),
orElse: () => switchFunctions.first,
) as BaseSwitchFunction;
if (selectedFn is TwoGangCountdown1Function ||
selectedFn is TwoGangCountdown2Function) {
return _buildCountDownSelector(
context,
setState,
selectedValues[selectedFn.code] ?? 0,
selectedCondition,
selectedConditions,
(value) {
selectedValues[selectedFn.code] = value;
final functionData = DeviceFunctionData(
entityId: selectedFn.deviceId,
function: selectedFn.code,
operationName: selectedFn.operationName,
value: value,
condition: selectedCondition,
valueDescription: '${value} sec',
);
final existingIndex =
selectedFunctions.indexWhere((f) =>
f.function == selectedFn.code);
if (existingIndex != -1) {
selectedFunctions[existingIndex] =
functionData;
} else {
selectedFunctions.add(functionData);
}
(context as Element).markNeedsBuild();
},
(condition) {
setState(() {
selectedCondition = condition;
});
},
);
}
final values =
selectedFn.getOperationalValues();
return ListView.builder(
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
return ValueListenableBuilder<String?>(
valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
final isSelected =
selectedFunction == function.code;
return ListTile(
tileColor: isSelected
? Colors.grey.shade100
: null,
leading: SvgPicture.asset(
value.icon,
function.icon,
width: 24,
height: 24,
),
title: Text(
value.description,
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: Radio<dynamic>(
value: value.value,
groupValue:
selectedValues[selectedFn.code],
onChanged: (newValue) {
selectedValues[selectedFn.code] =
newValue;
final functionData =
DeviceFunctionData(
entityId: selectedFn.deviceId,
function: selectedFn.code,
operationName:
selectedFn.operationName,
value: newValue,
valueDescription: value.description,
);
final existingIndex =
selectedFunctions.indexWhere(
(f) =>
f.function ==
selectedFn.code);
if (existingIndex != -1) {
selectedFunctions[existingIndex] =
functionData;
} else {
selectedFunctions.add(functionData);
}
(context as Element).markNeedsBuild();
},
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () {
selectedFunctionNotifier.value =
function.code;
selectedValueNotifier.value = function
is TwoGangCountdown1Function ||
function
is TwoGangCountdown2Function
? 0
: null;
},
);
},
);
},
),
),
// Right side: Value selector
if (selectedFunction != null)
Expanded(
child: ValueListenableBuilder<dynamic>(
valueListenable: selectedValueNotifier,
builder: (context, selectedValue, _) {
final selectedFn = switchFunctions.firstWhere(
(f) => f.code == selectedFunction,
);
if (selectedFn is TwoGangCountdown1Function ||
selectedFn is TwoGangCountdown2Function) {
return _buildCountDownSelector(
context,
selectedValueNotifier,
selectedConditionNotifier,
selectedConditionsNotifier,
);
}
return _buildOperationalValuesList(
context,
selectedFn as BaseSwitchFunction,
selectedValueNotifier,
);
},
),
),
],
),
),
Container(
height: 1,
width: double.infinity,
color: ColorsManager.greyColor,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancel',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.greyColor),
),
),
TextButton(
onPressed: selectedFunctions.isNotEmpty
? () {
for (final function in selectedFunctions) {
context
.read<RoutineBloc>()
.add(AddFunction(function));
}
Navigator.pop(context, true);
}
: null,
child: Text(
'Confirm',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
),
],
DialogFooter(
onCancel: () => Navigator.pop(context),
onConfirm: selectedFunction != null &&
selectedValueNotifier.value != null
? () {
final selectedFn = switchFunctions.firstWhere(
(f) => f.code == selectedFunction,
);
final value = selectedValueNotifier.value;
final functionData = DeviceFunctionData(
entityId: selectedFn.deviceId,
function: selectedFn.code,
operationName: selectedFn.operationName,
value: value,
condition: selectedConditionNotifier.value,
valueDescription: selectedFn
is TwoGangCountdown1Function ||
selectedFn is TwoGangCountdown2Function
? '${value} sec'
: ((selectedFn as BaseSwitchFunction)
.getOperationalValues()
.firstWhere((v) => v.value == value)
.description),
);
Navigator.pop(
context, {selectedFn.code: functionData});
}
: null,
isConfirmEnabled: selectedFunction != null,
),
],
),
@ -261,77 +169,101 @@ class TwoGangSwitchHelper {
);
}
/// Build countdown selector for switch functions dialog
static Widget _buildCountDownSelector(
static Widget _buildOperationalValuesList(
BuildContext context,
StateSetter setState,
dynamic selectedValue,
String? selectedCondition,
List<bool> selectedConditions,
Function(dynamic) onValueSelected,
Function(String?) onConditionSelected,
BaseSwitchFunction function,
ValueNotifier<dynamic> valueNotifier,
) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildConditionToggle(
context,
setState,
selectedConditions,
onConditionSelected,
),
const SizedBox(height: 20),
Text(
'${selectedValue.toString()} sec',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 20),
Slider(
value: selectedValue.toDouble(),
min: 0,
max: 300, // 5 minutes in seconds
divisions: 300,
onChanged: (value) {
setState(() {
onValueSelected(value.toInt());
});
final values = function.getOperationalValues();
return ValueListenableBuilder<dynamic>(
valueListenable: valueNotifier,
builder: (context, selectedValue, _) {
return ListView.builder(
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
return ListTile(
leading: SvgPicture.asset(
value.icon,
width: 24,
height: 24,
),
title: Text(
value.description,
style: context.textTheme.bodyMedium,
),
trailing: Radio<dynamic>(
value: value.value,
groupValue: selectedValue,
onChanged: (newValue) {
valueNotifier.value = newValue;
},
),
);
},
),
],
);
},
);
}
/// Build condition toggle for AC functions dialog
static Widget _buildConditionToggle(
static Widget _buildCountDownSelector(
BuildContext context,
StateSetter setState,
List<bool> selectedConditions,
Function(String?) onConditionSelected,
ValueNotifier<dynamic> valueNotifier,
ValueNotifier<String?> conditionNotifier,
ValueNotifier<List<bool>> conditionsNotifier,
) {
return ToggleButtons(
onPressed: (int index) {
setState(() {
for (int i = 0; i < selectedConditions.length; i++) {
selectedConditions[i] = i == index;
}
onConditionSelected(index == 0
? "<"
: index == 1
? "=="
: ">");
});
return ValueListenableBuilder<dynamic>(
valueListenable: valueNotifier,
builder: (context, value, _) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ValueListenableBuilder<List<bool>>(
valueListenable: conditionsNotifier,
builder: (context, selectedConditions, _) {
return ToggleButtons(
onPressed: (int index) {
final newConditions = List<bool>.filled(3, false);
newConditions[index] = true;
conditionsNotifier.value = newConditions;
conditionNotifier.value = index == 0
? "<"
: index == 1
? "=="
: ">";
},
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: selectedConditions,
children: const [Text("<"), Text("="), Text(">")],
);
},
),
const SizedBox(height: 20),
Text(
'${value ?? 0} sec',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 20),
Slider(
value: (value ?? 0).toDouble(),
min: 0,
max: 300,
divisions: 300,
onChanged: (newValue) {
valueNotifier.value = newValue.toInt();
},
),
],
);
},
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: selectedConditions,
children: const [Text("<"), Text("="), Text(">")],
);
}
}

View File

@ -12,82 +12,79 @@ class CreateNewRoutineView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => RoutineBloc(),
child: Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const RoutineSearchAndButtons(),
const SizedBox(height: 20),
Flexible(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Card(
child: Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(15),
),
child: const ConditionsRoutinesDevicesView()),
),
return Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const RoutineSearchAndButtons(),
const SizedBox(height: 20),
Flexible(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Card(
child: Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(15),
),
child: const ConditionsRoutinesDevicesView()),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
children: [
/// IF Container
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: Container(
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
children: [
/// IF Container
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: Container(
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
),
child: const IfContainer(),
),
child: const IfContainer(),
),
),
Container(
height: 2,
width: double.infinity,
color: ColorsManager.dialogBlueTitle,
),
),
Container(
height: 2,
width: double.infinity,
color: ColorsManager.dialogBlueTitle,
),
/// THEN Container
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: Container(
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15),
bottomRight: Radius.circular(15),
),
/// THEN Container
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: Container(
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15),
bottomRight: Radius.circular(15),
),
child: const ThenContainer(),
),
child: const ThenContainer(),
),
),
],
),
),
],
),
],
),
),
],
),
],
),
),
],
),
);
}

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class RoutinesView extends StatelessWidget {
@ -8,54 +10,67 @@ class RoutinesView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text("Create New Routines",
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.grayColor,
)),
SizedBox(
height: 200,
width: 150,
child: GestureDetector(
onTap: () {
BlocProvider.of<SwitchTabsBloc>(context).add(
const CreateNewRoutineViewEvent(true),
);
},
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
color: ColorsManager.whiteColors,
child: Center(
child: Container(
decoration: BoxDecoration(
color: ColorsManager.graysColor,
borderRadius: BorderRadius.circular(120),
border: Border.all(color: ColorsManager.greyColor, width: 2.0),
),
height: 70,
width: 70,
child: Icon(
Icons.add,
color: ColorsManager.dialogBlueTitle,
size: 40,
),
)),
return BlocBuilder<SwitchTabsBloc, SwitchTabsState>(
builder: (context, state) {
if (state is ShowCreateRoutineState && state.showCreateRoutine) {
return const CreateNewRoutineView();
}
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"Create New Routines",
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.grayColor,
),
),
),
SizedBox(
height: 200,
width: 150,
child: GestureDetector(
onTap: () {
BlocProvider.of<SwitchTabsBloc>(context).add(
const CreateNewRoutineViewEvent(true),
);
},
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
color: ColorsManager.whiteColors,
child: Center(
child: Container(
decoration: BoxDecoration(
color: ColorsManager.graysColor,
borderRadius: BorderRadius.circular(120),
border: Border.all(
color: ColorsManager.greyColor,
width: 2.0,
),
),
height: 70,
width: 70,
child: Icon(
Icons.add,
color: ColorsManager.dialogBlueTitle,
size: 40,
),
),
),
),
),
),
const Spacer(),
],
),
const Spacer(),
],
),
);
},
);
}
}

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart';
import 'package:syncrow_web/pages/routiens/widgets/routine_devices.dart';
import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart';
@ -11,87 +13,107 @@ class ConditionsRoutinesDevicesView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ConditionTitleAndSearchBar(),
const SizedBox(
height: 10,
),
const Wrap(
spacing: 10,
runSpacing: 10,
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
return const Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DraggableCard(
imagePath: Assets.tabToRun,
title: 'Tab to run',
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',
},
),
],
),
DraggableCard(
imagePath: Assets.map,
title: 'Location',
const SizedBox(height: 10),
const TitleRoutine(
title: 'Conditions',
subtitle: '(THEN)',
),
DraggableCard(
imagePath: Assets.weather,
title: 'Weather',
const SizedBox(height: 10),
const 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',
},
),
],
),
DraggableCard(
imagePath: Assets.schedule,
title: 'Schedule',
const SizedBox(height: 10),
const TitleRoutine(
title: 'Routines',
subtitle: '(THEN)',
),
const SizedBox(height: 10),
const ScenesAndAutomations(),
const SizedBox(height: 10),
const TitleRoutine(
title: 'Devices',
subtitle: '',
),
const SizedBox(height: 10),
const RoutineDevices(),
],
),
const SizedBox(
height: 10,
),
const TitleRoutine(
title: 'Conditions',
subtitle: '(THEN)',
),
const SizedBox(
height: 10,
),
const Wrap(
spacing: 10,
runSpacing: 10,
children: [
DraggableCard(
imagePath: Assets.notification,
title: 'Send Notification',
),
DraggableCard(
imagePath: Assets.delay,
title: 'Delay the action',
),
],
),
const SizedBox(
height: 10,
),
const TitleRoutine(
title: 'Routines',
subtitle: '(THEN)',
),
const SizedBox(
height: 10,
),
const ScenesAndAutomations(),
const SizedBox(
height: 10,
),
const TitleRoutine(
title: 'Devices',
subtitle: '',
),
const SizedBox(
height: 10,
),
const RoutineDevices(),
],
),
),
),
);
},
);
}
}

View File

@ -0,0 +1,76 @@
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;
const DialogFooter({
Key? key,
required this.onCancel,
required this.onConfirm,
required this.isConfirmEnabled,
}) : 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: [
_buildFooterButton(
context,
'Cancel',
onCancel,
width: isConfirmEnabled ? 299 : 179,
),
if (isConfirmEnabled)
Row(
children: [
Container(width: 1, height: 50, color: ColorsManager.greyColor),
_buildFooterButton(
context,
'Confirm',
onConfirm,
width: 299,
),
],
),
],
),
);
}
Widget _buildFooterButton(
BuildContext context,
String text,
VoidCallback? onTap, {
required double width,
}) {
return GestureDetector(
onTap: onTap,
child: SizedBox(
height: 50,
width: width,
child: Center(
child: Text(
text,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: text == 'Confirm'
? ColorsManager.primaryColorWithOpacity
: ColorsManager.textGray,
),
),
),
),
);
}
}

View File

@ -0,0 +1,31 @@
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(
children: [
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

@ -2,156 +2,139 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/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;
const DraggableCard({
super.key,
required this.imagePath,
required this.title,
this.titleColor,
this.isDragged = false,
this.isDisabled = false,
this.deviceData,
required this.deviceData,
});
final String imagePath;
final String title;
final Color? titleColor;
final bool isDragged;
final bool isDisabled;
final Map<String, dynamic>? deviceData;
@override
Widget build(BuildContext context) {
Widget card = Draggable<Map<String, dynamic>>(
data: deviceData ??
{
'key': UniqueKey().toString(),
'imagePath': imagePath,
'title': title,
},
feedback: Transform.rotate(
angle: -0.1,
child: _buildCardContent(context),
),
childWhenDragging: _buildGreyContainer(),
child: _buildCardContent(context),
);
if (isDisabled) {
card = AbsorbPointer(child: card);
}
return card;
}
Widget _buildCardContent(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
// Filter functions for this device
final deviceFunctions = state.selectedFunctions
.where((f) => f.entityId == deviceData?['deviceId'])
.where((f) => f.entityId == deviceData['deviceId'])
.toList();
return Card(
color: ColorsManager.whiteColors,
child: SizedBox(
width: 90,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 123,
child: 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: imagePath.contains('.svg')
? SvgPicture.asset(
imagePath,
)
: Image.network(imagePath),
return Draggable<Map<String, dynamic>>(
data: deviceData,
feedback: Transform.rotate(
angle: -0.1,
child: _buildCardContent(context, deviceFunctions),
),
childWhenDragging: _buildGreyContainer(),
child: _buildCardContent(context, deviceFunctions),
);
},
);
}
Widget _buildCardContent(
BuildContext context, List<DeviceFunctionData> deviceFunctions) {
return Card(
color: ColorsManager.whiteColors,
child: SizedBox(
width: 90,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 123,
child: 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,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
),
padding: const EdgeInsets.all(8),
child: imagePath.contains('.svg')
? SvgPicture.asset(
imagePath,
)
: Image.network(imagePath),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: Text(
title,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
),
),
),
],
),
),
if (deviceFunctions.isNotEmpty) ...[
const Divider(height: 1),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
itemCount: deviceFunctions.length,
itemBuilder: (context, index) {
final function = deviceFunctions[index];
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Text(
title,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
'${function.operationName}: ${function.valueDescription}',
style: context.textTheme.bodySmall?.copyWith(
color: titleColor ?? ColorsManager.blackColor,
fontSize: 12,
fontSize: 9,
color: ColorsManager.textGray,
height: 1.2,
),
overflow: TextOverflow.ellipsis,
),
),
InkWell(
onTap: () {
context.read<RoutineBloc>().add(
RemoveFunction(function),
);
},
child: const Padding(
padding: EdgeInsets.all(2),
child: Icon(
Icons.close,
size: 12,
color: ColorsManager.textGray,
),
),
),
],
),
),
if (deviceFunctions.isNotEmpty) ...[
const Divider(height: 1),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding:
const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
itemCount: deviceFunctions.length,
itemBuilder: (context, index) {
final function = deviceFunctions[index];
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Text(
'${function.operationName}: ${function.valueDescription}',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 9,
color: ColorsManager.textGray,
height: 1.2,
),
overflow: TextOverflow.ellipsis,
),
),
InkWell(
onTap: () {
context.read<RoutineBloc>().add(
RemoveFunction(function),
);
},
child: const Padding(
padding: EdgeInsets.all(2),
child: Icon(
Icons.close,
size: 12,
color: ColorsManager.textGray,
),
),
),
],
);
},
),
],
],
),
),
);
},
);
},
),
],
],
),
),
);
}

View File

@ -31,6 +31,7 @@ class IfContainer extends StatelessWidget {
key: Key(item['key']!),
imagePath: item['imagePath']!,
title: item['title']!,
deviceData: item,
))
.toList(),
),

View File

@ -1,20 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart';
class RoutineDevices extends StatelessWidget {
const RoutineDevices({
super.key,
});
const RoutineDevices({super.key});
@override
Widget build(BuildContext context) {
// Get the RoutineBloc instance from the parent
final routineBloc = context.read<RoutineBloc>();
return BlocProvider(
create: (context) => DeviceManagementBloc()
..add(
FetchDevices(),
),
create: (context) => DeviceManagementBloc()..add(FetchDevices()),
child: BlocBuilder<DeviceManagementBloc, DeviceManagementState>(
builder: (context, state) {
if (state is DeviceManagementLoaded) {
@ -25,24 +24,34 @@ class RoutineDevices extends StatelessWidget {
device.productType == '2G' ||
device.productType == '3G')
.toList();
return Wrap(
spacing: 10,
runSpacing: 10,
children: deviceList.asMap().entries.map((entry) {
final device = entry.value;
return DraggableCard(
imagePath: device.getDefaultIcon(device.productType),
title: device.name ?? '',
deviceData: {
'key': UniqueKey().toString(),
'imagePath': device.getDefaultIcon(device.productType),
'title': device.name ?? '',
'deviceId': device.uuid,
'productType': device.productType,
'functions': device.functions,
},
);
}).toList(),
// Provide the RoutineBloc to the child widgets
return BlocProvider.value(
value: routineBloc,
child: BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, routineState) {
return Wrap(
spacing: 10,
runSpacing: 10,
children: deviceList.asMap().entries.map((entry) {
final device = entry.value;
return DraggableCard(
imagePath: device.getDefaultIcon(device.productType),
title: device.name ?? '',
deviceData: {
'key': UniqueKey().toString(),
'imagePath':
device.getDefaultIcon(device.productType),
'title': device.name ?? '',
'deviceId': device.uuid,
'productType': device.productType,
'functions': device.functions,
},
);
}).toList(),
);
},
),
);
}
return const Center(child: CircularProgressIndicator());

View File

@ -4,40 +4,51 @@ import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class ScenesAndAutomations extends StatelessWidget {
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(const LoadScenes(spaceId))
..add(const LoadAutomation(spaceId));
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => RoutineBloc()
..add(
const LoadScenes(spaceId),
)
..add(
const LoadAutomation(spaceId),
),
child: BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
if (state.scenes.isNotEmpty || state.automations.isNotEmpty) {
var scenes = [...state.scenes, ...state.automations];
return Wrap(
spacing: 10,
runSpacing: 10,
children: scenes.asMap().entries.map((entry) {
final scene = entry.value;
return DraggableCard(
imagePath: Assets.logo,
title: scene.name,
);
}).toList(),
);
}
return const Center(child: CircularProgressIndicator());
},
),
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
if (state.scenes.isNotEmpty || state.automations.isNotEmpty) {
var scenes = [...state.scenes, ...state.automations];
return Wrap(
spacing: 10,
runSpacing: 10,
children: scenes.asMap().entries.map((entry) {
final scene = entry.value;
return DraggableCard(
imagePath: Assets.logo,
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

@ -33,6 +33,7 @@ class ThenContainer extends StatelessWidget {
key: Key(item['key']!),
imagePath: item['imagePath']!,
title: item['title']!,
deviceData: item,
))
.toList(),
),