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

View File

@ -1,219 +1,330 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.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/ac/ac_function.dart';
import 'package:syncrow_web/pages/routiens/models/device_functions.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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ACHelper { class ACHelper {
static Future<void> showACFunctionsDialog( static Future<Map<String, dynamic>?> showACFunctionsDialog(
BuildContext context, BuildContext context,
List<DeviceFunction<dynamic>> functions, List<DeviceFunction<dynamic>> functions,
) async { ) async {
List<ACFunction> acFunctions = functions.whereType<ACFunction>().toList(); List<ACFunction> acFunctions = functions.whereType<ACFunction>().toList();
// Track multiple selections using a map String? selectedFunction;
Map<String, dynamic> selectedValues = {}; dynamic selectedValue = 20;
List<DeviceFunctionData> selectedFunctions = []; String? selectedCondition = "==";
List<bool> _selectedConditions = [false, true, false];
await showDialog( return showDialog<Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return Dialog( return StatefulBuilder(
child: Container( builder: (context, setState) {
width: 600, return AlertDialog(
height: 450, contentPadding: EdgeInsets.zero,
decoration: BoxDecoration( content: _buildDialogContent(
color: Colors.white, context,
borderRadius: BorderRadius.circular(20), setState,
), acFunctions,
padding: const EdgeInsets.only(top: 20), selectedFunction,
child: Column( selectedValue,
mainAxisSize: MainAxisSize.min, selectedCondition,
children: [ _selectedConditions,
Text( (fn) => selectedFunction = fn,
'AC Functions', (val) => selectedValue = val,
style: Theme.of(context).textTheme.bodyMedium!.copyWith( (cond) => selectedCondition = cond,
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,
),
),
),
],
),
],
),
),
); );
}, },
); );
} }
static Widget _buildValueSelector( /// Build dialog content for AC functions dialog
static Widget _buildDialogContent(
BuildContext context, BuildContext context,
ACFunction function, StateSetter setState,
List<ACFunction> acFunctions,
String? selectedFunction,
dynamic selectedValue, dynamic selectedValue,
String? selectedCondition,
List<bool> selectedConditions,
Function(String?) onFunctionSelected,
Function(dynamic) onValueSelected, Function(dynamic) onValueSelected,
Function(String?) onConditionSelected,
) { ) {
final values = function.getOperationalValues();
return Container( return Container(
height: 200, width: selectedFunction != null ? 600 : 360,
padding: const EdgeInsets.symmetric(horizontal: 20), height: 450,
child: ListView.builder( decoration: BoxDecoration(
itemCount: values.length, 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) { itemBuilder: (context, index) {
final value = values[index]; final function = acFunctions[index];
return RadioListTile<dynamic>( return ListTile(
value: value.value, leading: SvgPicture.asset(
groupValue: selectedValue, function.icon,
onChanged: onValueSelected, width: 24,
title: Text(value.description), 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) { /// Build value selector for AC functions dialog
final values = function.getOperationalValues(); static Widget _buildValueSelector(
final selectedValue = values.firstWhere((v) => v.value == value); BuildContext context,
return selectedValue.description; 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.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/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/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/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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
class OneGangSwitchHelper { class OneGangSwitchHelper {
static Future<void> showSwitchFunctionsDialog( static Future<Map<String, dynamic>?> showSwitchFunctionsDialog(
BuildContext context, List<DeviceFunction<dynamic>> functions) async { BuildContext context,
List<DeviceFunction<dynamic>> functions,
) async {
List<DeviceFunction<dynamic>> switchFunctions = functions List<DeviceFunction<dynamic>> switchFunctions = functions
.where( .where(
(f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction) (f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction)
.toList(); .toList();
Map<String, dynamic> selectedValues = {}; final selectedFunctionNotifier = ValueNotifier<String?>(null);
List<DeviceFunctionData> selectedFunctions = []; final selectedValueNotifier = ValueNotifier<dynamic>(null);
String? selectedCondition = "<"; final selectedConditionNotifier = ValueNotifier<String?>("<");
List<bool> selectedConditions = [true, false, false]; final selectedConditionsNotifier =
ValueNotifier<List<bool>>([true, false, false]);
await showDialog( return showDialog<Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return StatefulBuilder( return ValueListenableBuilder<String?>(
builder: (context, setState) { valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
return AlertDialog( return AlertDialog(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
content: Container( content: Container(
width: 600, width: selectedFunction != null ? 600 : 360,
height: 450, height: 450,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@ -38,22 +42,7 @@ class OneGangSwitchHelper {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( const DialogHeader('1 Gang Light Switch Condition'),
'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,
),
),
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
@ -66,143 +55,67 @@ class OneGangSwitchHelper {
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final function = switchFunctions[index]; final function = switchFunctions[index];
final isSelected = return ValueListenableBuilder<String?>(
selectedValues.containsKey(function.code); valueListenable: selectedFunctionNotifier,
return ListTile( builder: (context, selectedFunction, _) {
tileColor: final isSelected =
isSelected ? Colors.grey.shade100 : null, selectedFunction == function.code;
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 ListTile( return ListTile(
tileColor: isSelected
? Colors.grey.shade100
: null,
leading: SvgPicture.asset( leading: SvgPicture.asset(
value.icon, function.icon,
width: 24, width: 24,
height: 24, height: 24,
), ),
title: Text( title: Text(
value.description, function.operationName,
style: context.textTheme.bodyMedium, style: context.textTheme.bodyMedium,
), ),
trailing: Radio<dynamic>( trailing: const Icon(
value: value.value, Icons.arrow_forward_ios,
groupValue: size: 16,
selectedValues[selectedFn.code], color: ColorsManager.textGray,
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();
},
), ),
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, width: double.infinity,
color: ColorsManager.greyColor, color: ColorsManager.greyColor,
), ),
Row( DialogFooter(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, onCancel: () => Navigator.pop(context),
children: [ onConfirm: selectedFunctionNotifier.value != null &&
TextButton( selectedValueNotifier.value != null
onPressed: () => Navigator.pop(context), ? () {
child: Text( final selectedFn = switchFunctions.firstWhere(
'Cancel', (f) => f.code == selectedFunctionNotifier.value,
style: Theme.of(context) );
.textTheme final value = selectedValueNotifier.value;
.bodyMedium! final functionData = DeviceFunctionData(
.copyWith(color: ColorsManager.greyColor), entityId: selectedFn.deviceId,
), function: selectedFn.code,
), operationName: selectedFn.operationName,
TextButton( value: value,
onPressed: selectedFunctions.isNotEmpty condition: selectedConditionNotifier.value,
? () { valueDescription:
for (final function in selectedFunctions) { selectedFn is OneGangCountdownFunction
context ? '${value} sec'
.read<RoutineBloc>() : ((selectedFn as BaseSwitchFunction)
.add(AddFunction(function)); .getOperationalValues()
} .firstWhere((v) => v.value == value)
Navigator.pop(context, true); .description),
} );
: null, Navigator.pop(
child: Text( context, {selectedFn.code: functionData});
'Confirm', }
style: Theme.of(context) : null,
.textTheme isConfirmEnabled:
.bodyMedium! selectedFunctionNotifier.value != null &&
.copyWith( selectedValueNotifier.value != null,
color: ColorsManager.primaryColorWithOpacity,
),
),
),
],
), ),
], ],
), ),
@ -257,77 +165,101 @@ class OneGangSwitchHelper {
); );
} }
/// Build countdown selector for switch functions dialog
static Widget _buildCountDownSelector( static Widget _buildCountDownSelector(
BuildContext context, BuildContext context,
StateSetter setState, ValueNotifier<dynamic> valueNotifier,
dynamic selectedValue, ValueNotifier<String?> conditionNotifier,
String? selectedCondition, ValueNotifier<List<bool>> conditionsNotifier,
List<bool> selectedConditions,
Function(dynamic) onValueSelected,
Function(String?) onConditionSelected,
) { ) {
return Column( return ValueListenableBuilder<dynamic>(
mainAxisAlignment: MainAxisAlignment.center, valueListenable: valueNotifier,
children: [ builder: (context, value, _) {
_buildConditionToggle( return Column(
context, mainAxisAlignment: MainAxisAlignment.center,
setState, children: [
selectedConditions, ValueListenableBuilder<List<bool>>(
onConditionSelected, valueListenable: conditionsNotifier,
), builder: (context, selectedConditions, _) {
const SizedBox(height: 20), return ToggleButtons(
Text( onPressed: (int index) {
'${selectedValue.toString()} sec', final newConditions = List<bool>.filled(3, false);
style: Theme.of(context).textTheme.headlineMedium, newConditions[index] = true;
), conditionsNotifier.value = newConditions;
const SizedBox(height: 20), conditionNotifier.value = index == 0
Slider( ? "<"
value: selectedValue.toDouble(), : index == 1
min: 0, ? "=="
max: 300, // 5 minutes in seconds : ">";
divisions: 300, },
onChanged: (value) { borderRadius: const BorderRadius.all(Radius.circular(8)),
setState(() { selectedBorderColor: ColorsManager.primaryColorWithOpacity,
onValueSelected(value.toInt()); 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 _buildOperationalValuesList(
static Widget _buildConditionToggle(
BuildContext context, BuildContext context,
StateSetter setState, BaseSwitchFunction function,
List<bool> selectedConditions, ValueNotifier<dynamic> valueNotifier,
Function(String?) onConditionSelected,
) { ) {
return ToggleButtons( final values = function.getOperationalValues();
onPressed: (int index) { return ValueListenableBuilder<dynamic>(
setState(() { valueListenable: valueNotifier,
for (int i = 0; i < selectedConditions.length; i++) { builder: (context, selectedValue, _) {
selectedConditions[i] = i == index; return ListView.builder(
} itemCount: values.length,
onConditionSelected(index == 0 itemBuilder: (context, index) {
? "<" final value = values[index];
: index == 1 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/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/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/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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -18,20 +20,23 @@ class TwoGangSwitchHelper {
f is TwoGangCountdown1Function || f is TwoGangCountdown1Function ||
f is TwoGangCountdown2Function) f is TwoGangCountdown2Function)
.toList(); .toList();
Map<String, dynamic> selectedValues = {};
List<DeviceFunctionData> selectedFunctions = []; final selectedFunctionNotifier = ValueNotifier<String?>(null);
String? selectedCondition = "<"; final selectedValueNotifier = ValueNotifier<dynamic>(null);
List<bool> selectedConditions = [true, false, false]; final selectedConditionNotifier = ValueNotifier<String?>('<');
final selectedConditionsNotifier =
ValueNotifier<List<bool>>([true, false, false]);
await showDialog( await showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return StatefulBuilder( return ValueListenableBuilder<String?>(
builder: (context, setState) { valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
return AlertDialog( return AlertDialog(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
content: Container( content: Container(
width: 600, width: selectedFunction != null ? 600 : 300,
height: 450, height: 450,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@ -41,22 +46,7 @@ class TwoGangSwitchHelper {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( const DialogHeader('2 Gangs Light Switch Condition'),
'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,
),
),
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
@ -69,187 +59,105 @@ class TwoGangSwitchHelper {
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final function = switchFunctions[index]; final function = switchFunctions[index];
final isSelected = return ValueListenableBuilder<String?>(
selectedValues.containsKey(function.code); valueListenable: selectedFunctionNotifier,
return ListTile( builder: (context, selectedFunction, _) {
tileColor: final isSelected =
isSelected ? Colors.grey.shade100 : null, selectedFunction == function.code;
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 ListTile( return ListTile(
tileColor: isSelected
? Colors.grey.shade100
: null,
leading: SvgPicture.asset( leading: SvgPicture.asset(
value.icon, function.icon,
width: 24, width: 24,
height: 24, height: 24,
), ),
title: Text( title: Text(
value.description, function.operationName,
style: context.textTheme.bodyMedium, style: context.textTheme.bodyMedium,
), ),
trailing: Radio<dynamic>( trailing: const Icon(
value: value.value, Icons.arrow_forward_ios,
groupValue: size: 16,
selectedValues[selectedFn.code], color: ColorsManager.textGray,
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();
},
), ),
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( DialogFooter(
height: 1, onCancel: () => Navigator.pop(context),
width: double.infinity, onConfirm: selectedFunction != null &&
color: ColorsManager.greyColor, selectedValueNotifier.value != null
), ? () {
Row( final selectedFn = switchFunctions.firstWhere(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, (f) => f.code == selectedFunction,
children: [ );
TextButton( final value = selectedValueNotifier.value;
onPressed: () => Navigator.pop(context), final functionData = DeviceFunctionData(
child: Text( entityId: selectedFn.deviceId,
'Cancel', function: selectedFn.code,
style: Theme.of(context) operationName: selectedFn.operationName,
.textTheme value: value,
.bodyMedium! condition: selectedConditionNotifier.value,
.copyWith(color: ColorsManager.greyColor), valueDescription: selectedFn
), is TwoGangCountdown1Function ||
), selectedFn is TwoGangCountdown2Function
TextButton( ? '${value} sec'
onPressed: selectedFunctions.isNotEmpty : ((selectedFn as BaseSwitchFunction)
? () { .getOperationalValues()
for (final function in selectedFunctions) { .firstWhere((v) => v.value == value)
context .description),
.read<RoutineBloc>() );
.add(AddFunction(function)); Navigator.pop(
} context, {selectedFn.code: functionData});
Navigator.pop(context, true); }
} : null,
: null, isConfirmEnabled: selectedFunction != null,
child: Text(
'Confirm',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
),
],
), ),
], ],
), ),
@ -261,77 +169,101 @@ class TwoGangSwitchHelper {
); );
} }
/// Build countdown selector for switch functions dialog static Widget _buildOperationalValuesList(
static Widget _buildCountDownSelector(
BuildContext context, BuildContext context,
StateSetter setState, BaseSwitchFunction function,
dynamic selectedValue, ValueNotifier<dynamic> valueNotifier,
String? selectedCondition,
List<bool> selectedConditions,
Function(dynamic) onValueSelected,
Function(String?) onConditionSelected,
) { ) {
return Column( final values = function.getOperationalValues();
mainAxisAlignment: MainAxisAlignment.center, return ValueListenableBuilder<dynamic>(
children: [ valueListenable: valueNotifier,
_buildConditionToggle( builder: (context, selectedValue, _) {
context, return ListView.builder(
setState, itemCount: values.length,
selectedConditions, itemBuilder: (context, index) {
onConditionSelected, final value = values[index];
), return ListTile(
const SizedBox(height: 20), leading: SvgPicture.asset(
Text( value.icon,
'${selectedValue.toString()} sec', width: 24,
style: Theme.of(context).textTheme.headlineMedium, height: 24,
), ),
const SizedBox(height: 20), title: Text(
Slider( value.description,
value: selectedValue.toDouble(), style: context.textTheme.bodyMedium,
min: 0, ),
max: 300, // 5 minutes in seconds trailing: Radio<dynamic>(
divisions: 300, value: value.value,
onChanged: (value) { groupValue: selectedValue,
setState(() { onChanged: (newValue) {
onValueSelected(value.toInt()); valueNotifier.value = newValue;
}); },
),
);
}, },
), );
], },
); );
} }
/// Build condition toggle for AC functions dialog static Widget _buildCountDownSelector(
static Widget _buildConditionToggle(
BuildContext context, BuildContext context,
StateSetter setState, ValueNotifier<dynamic> valueNotifier,
List<bool> selectedConditions, ValueNotifier<String?> conditionNotifier,
Function(String?) onConditionSelected, ValueNotifier<List<bool>> conditionsNotifier,
) { ) {
return ToggleButtons( return ValueListenableBuilder<dynamic>(
onPressed: (int index) { valueListenable: valueNotifier,
setState(() { builder: (context, value, _) {
for (int i = 0; i < selectedConditions.length; i++) { return Column(
selectedConditions[i] = i == index; mainAxisAlignment: MainAxisAlignment.center,
} children: [
onConditionSelected(index == 0 ValueListenableBuilder<List<bool>>(
? "<" valueListenable: conditionsNotifier,
: index == 1 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return Container(
create: (context) => RoutineBloc(), alignment: Alignment.topLeft,
child: Container( padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft, child: Column(
padding: const EdgeInsets.all(16), mainAxisSize: MainAxisSize.min,
child: Column( children: [
mainAxisSize: MainAxisSize.min, const RoutineSearchAndButtons(),
children: [ const SizedBox(height: 20),
const RoutineSearchAndButtons(), Flexible(
const SizedBox(height: 20), child: Row(
Flexible( crossAxisAlignment: CrossAxisAlignment.stretch,
child: Row( children: [
crossAxisAlignment: CrossAxisAlignment.stretch, Expanded(
children: [ child: Card(
Expanded( child: Container(
child: Card( decoration: BoxDecoration(
child: Container( color: ColorsManager.whiteColors,
decoration: BoxDecoration( borderRadius: BorderRadius.circular(15),
color: ColorsManager.whiteColors, ),
borderRadius: BorderRadius.circular(15), child: const ConditionsRoutinesDevicesView()),
),
child: const ConditionsRoutinesDevicesView()),
),
), ),
const SizedBox( ),
width: 10, const SizedBox(
), width: 10,
Expanded( ),
child: Column( Expanded(
children: [ child: Column(
/// IF Container children: [
Expanded( /// IF Container
child: Card( Expanded(
margin: EdgeInsets.zero, child: Card(
child: Container( margin: EdgeInsets.zero,
decoration: const BoxDecoration( child: Container(
color: ColorsManager.whiteColors, decoration: const BoxDecoration(
borderRadius: BorderRadius.only( color: ColorsManager.whiteColors,
topLeft: Radius.circular(15), borderRadius: BorderRadius.only(
topRight: Radius.circular(15), topLeft: Radius.circular(15),
), topRight: Radius.circular(15),
), ),
child: const IfContainer(),
), ),
child: const IfContainer(),
), ),
), ),
Container( ),
height: 2, Container(
width: double.infinity, height: 2,
color: ColorsManager.dialogBlueTitle, width: double.infinity,
), color: ColorsManager.dialogBlueTitle,
),
/// THEN Container /// THEN Container
Expanded( Expanded(
child: Card( child: Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Container( child: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: ColorsManager.boxColor, color: ColorsManager.boxColor,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15), bottomLeft: Radius.circular(15),
bottomRight: 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/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'; import 'package:syncrow_web/utils/color_manager.dart';
class RoutinesView extends StatelessWidget { class RoutinesView extends StatelessWidget {
@ -8,54 +10,67 @@ class RoutinesView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return BlocBuilder<SwitchTabsBloc, SwitchTabsState>(
padding: const EdgeInsets.all(16), builder: (context, state) {
child: Column( if (state is ShowCreateRoutineState && state.showCreateRoutine) {
mainAxisSize: MainAxisSize.min, return const CreateNewRoutineView();
crossAxisAlignment: CrossAxisAlignment.start, }
mainAxisAlignment: MainAxisAlignment.start, return Padding(
children: [ padding: const EdgeInsets.all(16),
Text("Create New Routines", child: Column(
style: Theme.of(context).textTheme.titleMedium?.copyWith( mainAxisSize: MainAxisSize.min,
fontWeight: FontWeight.bold, crossAxisAlignment: CrossAxisAlignment.start,
color: ColorsManager.grayColor, mainAxisAlignment: MainAxisAlignment.start,
)), children: [
SizedBox( Text(
height: 200, "Create New Routines",
width: 150, style: Theme.of(context).textTheme.titleMedium?.copyWith(
child: GestureDetector( fontWeight: FontWeight.bold,
onTap: () { color: ColorsManager.grayColor,
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,
),
)),
), ),
), 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/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/dragable_card.dart';
import 'package:syncrow_web/pages/routiens/widgets/routine_devices.dart'; import 'package:syncrow_web/pages/routiens/widgets/routine_devices.dart';
import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart';
@ -11,87 +13,107 @@ class ConditionsRoutinesDevicesView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return BlocBuilder<RoutineBloc, RoutineState>(
padding: EdgeInsets.symmetric(horizontal: 8.0), builder: (context, state) {
child: SingleChildScrollView( return const Padding(
child: Column( padding: const EdgeInsets.symmetric(horizontal: 8.0),
crossAxisAlignment: CrossAxisAlignment.start, child: SingleChildScrollView(
children: [ child: Column(
const ConditionTitleAndSearchBar(), crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(
height: 10,
),
const Wrap(
spacing: 10,
runSpacing: 10,
children: [ children: [
DraggableCard( ConditionTitleAndSearchBar(),
imagePath: Assets.tabToRun, SizedBox(height: 10),
title: 'Tab to run', 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( const SizedBox(height: 10),
imagePath: Assets.map, const TitleRoutine(
title: 'Location', title: 'Conditions',
subtitle: '(THEN)',
), ),
DraggableCard( const SizedBox(height: 10),
imagePath: Assets.weather, const Wrap(
title: 'Weather', 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( const SizedBox(height: 10),
imagePath: Assets.schedule, const TitleRoutine(
title: 'Schedule', 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_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DraggableCard extends StatelessWidget { class DraggableCard extends StatelessWidget {
final String imagePath;
final String title;
final Map<String, dynamic> deviceData;
const DraggableCard({ const DraggableCard({
super.key, super.key,
required this.imagePath, required this.imagePath,
required this.title, required this.title,
this.titleColor, required this.deviceData,
this.isDragged = false,
this.isDisabled = false,
this.deviceData,
}); });
final String imagePath;
final String title;
final Color? titleColor;
final bool isDragged;
final bool isDisabled;
final Map<String, dynamic>? deviceData;
@override @override
Widget build(BuildContext context) { 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>( return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) { builder: (context, state) {
// Filter functions for this device
final deviceFunctions = state.selectedFunctions final deviceFunctions = state.selectedFunctions
.where((f) => f.entityId == deviceData?['deviceId']) .where((f) => f.entityId == deviceData['deviceId'])
.toList(); .toList();
return Card( return Draggable<Map<String, dynamic>>(
color: ColorsManager.whiteColors, data: deviceData,
child: SizedBox( feedback: Transform.rotate(
width: 90, angle: -0.1,
child: Column( child: _buildCardContent(context, deviceFunctions),
mainAxisSize: MainAxisSize.min, ),
mainAxisAlignment: MainAxisAlignment.center, childWhenDragging: _buildGreyContainer(),
crossAxisAlignment: CrossAxisAlignment.center, child: _buildCardContent(context, deviceFunctions),
children: [ );
SizedBox( },
height: 123, );
child: Column( }
mainAxisAlignment: MainAxisAlignment.center,
children: [ Widget _buildCardContent(
Container( BuildContext context, List<DeviceFunctionData> deviceFunctions) {
height: 50, return Card(
width: 50, color: ColorsManager.whiteColors,
decoration: BoxDecoration( child: SizedBox(
color: ColorsManager.CircleImageBackground, width: 90,
borderRadius: BorderRadius.circular(90), child: Column(
border: Border.all( mainAxisSize: MainAxisSize.min,
color: ColorsManager.graysColor, mainAxisAlignment: MainAxisAlignment.center,
), crossAxisAlignment: CrossAxisAlignment.center,
), children: [
padding: const EdgeInsets.all(8), SizedBox(
child: imagePath.contains('.svg') height: 123,
? SvgPicture.asset( child: Column(
imagePath, mainAxisAlignment: MainAxisAlignment.center,
) children: [
: Image.network(imagePath), 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.all(8),
padding: const EdgeInsets.symmetric(horizontal: 3), 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( child: Text(
title, '${function.operationName}: ${function.valueDescription}',
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: titleColor ?? ColorsManager.blackColor, fontSize: 9,
fontSize: 12, 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']!), key: Key(item['key']!),
imagePath: item['imagePath']!, imagePath: item['imagePath']!,
title: item['title']!, title: item['title']!,
deviceData: item,
)) ))
.toList(), .toList(),
), ),

View File

@ -1,20 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_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'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart';
class RoutineDevices extends StatelessWidget { class RoutineDevices extends StatelessWidget {
const RoutineDevices({ const RoutineDevices({super.key});
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Get the RoutineBloc instance from the parent
final routineBloc = context.read<RoutineBloc>();
return BlocProvider( return BlocProvider(
create: (context) => DeviceManagementBloc() create: (context) => DeviceManagementBloc()..add(FetchDevices()),
..add(
FetchDevices(),
),
child: BlocBuilder<DeviceManagementBloc, DeviceManagementState>( child: BlocBuilder<DeviceManagementBloc, DeviceManagementState>(
builder: (context, state) { builder: (context, state) {
if (state is DeviceManagementLoaded) { if (state is DeviceManagementLoaded) {
@ -25,24 +24,34 @@ class RoutineDevices extends StatelessWidget {
device.productType == '2G' || device.productType == '2G' ||
device.productType == '3G') device.productType == '3G')
.toList(); .toList();
return Wrap(
spacing: 10, // Provide the RoutineBloc to the child widgets
runSpacing: 10, return BlocProvider.value(
children: deviceList.asMap().entries.map((entry) { value: routineBloc,
final device = entry.value; child: BlocBuilder<RoutineBloc, RoutineState>(
return DraggableCard( builder: (context, routineState) {
imagePath: device.getDefaultIcon(device.productType), return Wrap(
title: device.name ?? '', spacing: 10,
deviceData: { runSpacing: 10,
'key': UniqueKey().toString(), children: deviceList.asMap().entries.map((entry) {
'imagePath': device.getDefaultIcon(device.productType), final device = entry.value;
'title': device.name ?? '', return DraggableCard(
'deviceId': device.uuid, imagePath: device.getDefaultIcon(device.productType),
'productType': device.productType, title: device.name ?? '',
'functions': device.functions, deviceData: {
}, 'key': UniqueKey().toString(),
); 'imagePath':
}).toList(), device.getDefaultIcon(device.productType),
'title': device.name ?? '',
'deviceId': device.uuid,
'productType': device.productType,
'functions': device.functions,
},
);
}).toList(),
);
},
),
); );
} }
return const Center(child: CircularProgressIndicator()); 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/pages/routiens/widgets/dragable_card.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
class ScenesAndAutomations extends StatelessWidget { class ScenesAndAutomations extends StatefulWidget {
const ScenesAndAutomations({ const ScenesAndAutomations({
super.key, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocBuilder<RoutineBloc, RoutineState>(
create: (context) => RoutineBloc() builder: (context, state) {
..add( if (state.scenes.isNotEmpty || state.automations.isNotEmpty) {
const LoadScenes(spaceId), var scenes = [...state.scenes, ...state.automations];
) return Wrap(
..add( spacing: 10,
const LoadAutomation(spaceId), runSpacing: 10,
), children: scenes.asMap().entries.map((entry) {
child: BlocBuilder<RoutineBloc, RoutineState>( final scene = entry.value;
builder: (context, state) { return DraggableCard(
if (state.scenes.isNotEmpty || state.automations.isNotEmpty) { imagePath: Assets.logo,
var scenes = [...state.scenes, ...state.automations]; title: scene.name,
return Wrap( deviceData: {
spacing: 10, 'deviceId': scene.id,
runSpacing: 10, 'name': scene.name,
children: scenes.asMap().entries.map((entry) { 'status': scene.status,
final scene = entry.value; 'type': scene.type,
return DraggableCard( 'icon': scene.icon,
imagePath: Assets.logo, },
title: scene.name, );
); }).toList(),
}).toList(), );
); }
} return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator()); },
},
),
); );
} }
} }

View File

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