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,28 +1,64 @@
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(
contentPadding: EdgeInsets.zero,
content: _buildDialogContent(
context,
setState,
acFunctions,
selectedFunction,
selectedValue,
selectedCondition,
_selectedConditions,
(fn) => selectedFunction = fn,
(val) => selectedValue = val,
(cond) => selectedCondition = cond,
),
);
},
);
},
);
}
/// Build dialog content for AC functions dialog
static Widget _buildDialogContent(
BuildContext context,
StateSetter setState,
List<ACFunction> acFunctions,
String? selectedFunction,
dynamic selectedValue,
String? selectedCondition,
List<bool> selectedConditions,
Function(String?) onFunctionSelected,
Function(dynamic) onValueSelected,
Function(String?) onConditionSelected,
) {
return Container(
width: selectedFunction != null ? 600 : 360,
height: 450, height: 450,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@ -32,38 +68,67 @@ class ACHelper {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( const DialogHeader('AC Functions'),
'AC Functions', Flexible(
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( child: Row(
children: [ children: [
Expanded( _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( child: ListView.separated(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: acFunctions.length, itemCount: acFunctions.length,
separatorBuilder: (_, __) => const Divider( separatorBuilder: (context, index) => const Divider(
color: ColorsManager.dividerColor, color: ColorsManager.dividerColor,
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final function = acFunctions[index]; final function = acFunctions[index];
final isSelected =
selectedValues.containsKey(function.code);
return ListTile( return ListTile(
tileColor:
isSelected ? Colors.grey.shade100 : null,
leading: SvgPicture.asset( leading: SvgPicture.asset(
function.icon, function.icon,
width: 24, width: 24,
@ -73,147 +138,193 @@ class ACHelper {
function.operationName, function.operationName,
style: context.textTheme.bodyMedium, style: context.textTheme.bodyMedium,
), ),
trailing: isSelected trailing: const Icon(
? Icon(
Icons.check_circle,
color:
ColorsManager.primaryColorWithOpacity,
size: 20,
)
: const Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 16, size: 16,
color: ColorsManager.textGray, color: ColorsManager.textGray,
), ),
onTap: () { onTap: () => setState(() => onFunctionSelected(function.code)),
if (isSelected) { );
selectedValues.remove(function.code); },
selectedFunctions.removeWhere( ),
(f) => f.function == function.code); );
} }
(context as Element).markNeedsBuild();
}, /// Build value selector for AC functions dialog
); static Widget _buildValueSelector(
}, BuildContext context,
), StateSetter setState,
), String selectedFunction,
Expanded( dynamic selectedValue,
child: Builder( String? selectedCondition,
builder: (context) { List<bool> selectedConditions,
final selectedFunction = acFunctions.firstWhere( Function(dynamic) onValueSelected,
(f) => selectedValues.containsKey(f.code), Function(String?) onConditionSelected,
orElse: () => acFunctions.first, List<ACFunction> acFunctions,
); ) {
return _buildValueSelector( if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') {
return Expanded(
child: _buildTemperatureSelector(
context, context,
selectedFunction, setState,
selectedValues[selectedFunction.code], selectedValue,
(value) { selectedCondition,
selectedValues[selectedFunction.code] = value; selectedConditions,
// Update or add the function data onValueSelected,
final functionData = DeviceFunctionData( onConditionSelected,
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(); 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,
), ),
], ],
), );
),
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);
/// 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;
} }
: null, 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( child: Text(
'Confirm', '${selectedValue ?? 20}°C',
style: Theme.of(context).textTheme.bodyMedium!.copyWith( style: context.textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity, color: ColorsManager.primaryColorWithOpacity,
), ),
), ),
),
],
),
],
),
),
);
},
); );
} }
static Widget _buildValueSelector( static Widget _buildTemperatureSlider(
BuildContext context, BuildContext context,
ACFunction function, StateSetter setState,
dynamic selectedValue, dynamic selectedValue,
Function(dynamic) onValueSelected, Function(dynamic) onValueSelected,
) { ) {
final values = function.getOperationalValues(); final currentValue = selectedValue is int ? selectedValue.toDouble() : 20.0;
return Container( return Slider(
height: 200, value: currentValue,
padding: const EdgeInsets.symmetric(horizontal: 20), min: 16,
child: ListView.builder( max: 30,
itemCount: values.length, divisions: 14,
itemBuilder: (context, index) { label: '${currentValue.toInt()}°C',
final value = values[index]; onChanged: (value) {
return RadioListTile<dynamic>( setState(() => onValueSelected(value.toInt()));
value: value.value,
groupValue: selectedValue,
onChanged: onValueSelected,
title: Text(value.description),
);
}, },
),
); );
} }
static String _getValueDescription(ACFunction function, dynamic value) { static Widget _buildOperationalValuesList(
final values = function.getOperationalValues(); BuildContext context,
final selectedValue = values.firstWhere((v) => v.value == value); StateSetter setState,
return selectedValue.description; 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,11 +55,15 @@ class OneGangSwitchHelper {
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final function = switchFunctions[index]; final function = switchFunctions[index];
return ValueListenableBuilder<String?>(
valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
final isSelected = final isSelected =
selectedValues.containsKey(function.code); selectedFunction == function.code;
return ListTile( return ListTile(
tileColor: tileColor: isSelected
isSelected ? Colors.grey.shade100 : null, ? Colors.grey.shade100
: null,
leading: SvgPicture.asset( leading: SvgPicture.asset(
function.icon, function.icon,
width: 24, width: 24,
@ -80,78 +73,168 @@ class OneGangSwitchHelper {
function.operationName, function.operationName,
style: context.textTheme.bodyMedium, style: context.textTheme.bodyMedium,
), ),
trailing: isSelected trailing: const Icon(
? Icon(
Icons.check_circle,
color: ColorsManager
.primaryColorWithOpacity,
size: 20,
)
: const Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 16, size: 16,
color: ColorsManager.textGray, color: ColorsManager.textGray,
), ),
onTap: () { onTap: () {
if (isSelected) { selectedFunctionNotifier.value =
selectedValues.remove(function.code); function.code;
selectedFunctions.removeWhere( selectedValueNotifier.value =
(f) => f.function == function.code); function is OneGangCountdownFunction
} ? 0
(context as Element).markNeedsBuild(); : null;
},
);
}, },
); );
}, },
), ),
), ),
// Right side: Value selector // Right side: Value selector
Expanded( if (selectedFunction != null)
child: Builder( ValueListenableBuilder<String?>(
builder: (context) { valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
final selectedFn = switchFunctions.firstWhere( final selectedFn = switchFunctions.firstWhere(
(f) => selectedValues.containsKey(f.code), (f) => f.code == selectedFunction,
orElse: () => switchFunctions.first, );
) as BaseSwitchFunction; return Expanded(
child: selectedFn is OneGangCountdownFunction
if (selectedFn is OneGangCountdownFunction) { ? _buildCountDownSelector(
return _buildCountDownSelector(
context, context,
setState, selectedValueNotifier,
selectedValues[selectedFn.code] ?? 0, selectedConditionNotifier,
selectedCondition, selectedConditionsNotifier,
selectedConditions, )
(value) { : _buildOperationalValuesList(
selectedValues[selectedFn.code] = value; context,
selectedFn as BaseSwitchFunction,
selectedValueNotifier,
),
);
},
),
],
),
),
Container(
height: 1,
width: double.infinity,
color: ColorsManager.greyColor,
),
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( final functionData = DeviceFunctionData(
entityId: selectedFn.deviceId, entityId: selectedFn.deviceId,
function: selectedFn.code, function: selectedFn.code,
operationName: selectedFn.operationName, operationName: selectedFn.operationName,
value: value, value: value,
condition: selectedCondition, condition: selectedConditionNotifier.value,
valueDescription: '${value} sec', valueDescription:
selectedFn is OneGangCountdownFunction
? '${value} sec'
: ((selectedFn as BaseSwitchFunction)
.getOperationalValues()
.firstWhere((v) => v.value == value)
.description),
); );
Navigator.pop(
final existingIndex = context, {selectedFn.code: functionData});
selectedFunctions.indexWhere((f) =>
f.function == selectedFn.code);
if (existingIndex != -1) {
selectedFunctions[existingIndex] =
functionData;
} else {
selectedFunctions.add(functionData);
} }
(context as Element).markNeedsBuild(); : null,
isConfirmEnabled:
selectedFunctionNotifier.value != null &&
selectedValueNotifier.value != null,
),
],
),
),
);
}, },
(condition) { );
setState(() {
selectedCondition = condition;
});
}, },
); );
} }
final values = static Widget _buildCountDownSelector(
selectedFn.getOperationalValues(); BuildContext context,
ValueNotifier<dynamic> valueNotifier,
ValueNotifier<String?> conditionNotifier,
ValueNotifier<List<bool>> conditionsNotifier,
) {
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();
},
),
],
);
},
);
}
static Widget _buildOperationalValuesList(
BuildContext context,
BaseSwitchFunction function,
ValueNotifier<dynamic> valueNotifier,
) {
final values = function.getOperationalValues();
return ValueListenableBuilder<dynamic>(
valueListenable: valueNotifier,
builder: (context, selectedValue, _) {
return ListView.builder( return ListView.builder(
itemCount: values.length, itemCount: values.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -168,166 +251,15 @@ class OneGangSwitchHelper {
), ),
trailing: Radio<dynamic>( trailing: Radio<dynamic>(
value: value.value, value: value.value,
groupValue: groupValue: selectedValue,
selectedValues[selectedFn.code],
onChanged: (newValue) { onChanged: (newValue) {
selectedValues[selectedFn.code] = valueNotifier.value = newValue;
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();
}, },
), ),
); );
}, },
); );
}, },
),
),
],
),
),
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,
),
),
),
],
),
],
),
),
);
},
);
},
);
}
/// 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,
) {
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());
});
},
),
],
);
}
/// 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(">")],
); );
} }
} }

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,11 +59,15 @@ class TwoGangSwitchHelper {
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final function = switchFunctions[index]; final function = switchFunctions[index];
return ValueListenableBuilder<String?>(
valueListenable: selectedFunctionNotifier,
builder: (context, selectedFunction, _) {
final isSelected = final isSelected =
selectedValues.containsKey(function.code); selectedFunction == function.code;
return ListTile( return ListTile(
tileColor: tileColor: isSelected
isSelected ? Colors.grey.shade100 : null, ? Colors.grey.shade100
: null,
leading: SvgPicture.asset( leading: SvgPicture.asset(
function.icon, function.icon,
width: 24, width: 24,
@ -83,79 +77,107 @@ class TwoGangSwitchHelper {
function.operationName, function.operationName,
style: context.textTheme.bodyMedium, style: context.textTheme.bodyMedium,
), ),
trailing: isSelected trailing: const Icon(
? Icon(
Icons.check_circle,
color: ColorsManager
.primaryColorWithOpacity,
size: 20,
)
: const Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 16, size: 16,
color: ColorsManager.textGray, color: ColorsManager.textGray,
), ),
onTap: () { onTap: () {
if (isSelected) { selectedFunctionNotifier.value =
selectedValues.remove(function.code); function.code;
selectedFunctions.removeWhere( selectedValueNotifier.value = function
(f) => f.function == function.code); is TwoGangCountdown1Function ||
} function
(context as Element).markNeedsBuild(); is TwoGangCountdown2Function
? 0
: null;
},
);
}, },
); );
}, },
), ),
), ),
// Right side: Value selector // Right side: Value selector
if (selectedFunction != null)
Expanded( Expanded(
child: Builder( child: ValueListenableBuilder<dynamic>(
builder: (context) { valueListenable: selectedValueNotifier,
builder: (context, selectedValue, _) {
final selectedFn = switchFunctions.firstWhere( final selectedFn = switchFunctions.firstWhere(
(f) => selectedValues.containsKey(f.code), (f) => f.code == selectedFunction,
orElse: () => switchFunctions.first, );
) as BaseSwitchFunction;
if (selectedFn is TwoGangCountdown1Function || if (selectedFn is TwoGangCountdown1Function ||
selectedFn is TwoGangCountdown2Function) { selectedFn is TwoGangCountdown2Function) {
return _buildCountDownSelector( return _buildCountDownSelector(
context, context,
setState, selectedValueNotifier,
selectedValues[selectedFn.code] ?? 0, selectedConditionNotifier,
selectedCondition, selectedConditionsNotifier,
selectedConditions, );
(value) { }
selectedValues[selectedFn.code] = value;
return _buildOperationalValuesList(
context,
selectedFn as BaseSwitchFunction,
selectedValueNotifier,
);
},
),
),
],
),
),
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( final functionData = DeviceFunctionData(
entityId: selectedFn.deviceId, entityId: selectedFn.deviceId,
function: selectedFn.code, function: selectedFn.code,
operationName: selectedFn.operationName, operationName: selectedFn.operationName,
value: value, value: value,
condition: selectedCondition, condition: selectedConditionNotifier.value,
valueDescription: '${value} sec', valueDescription: selectedFn
is TwoGangCountdown1Function ||
selectedFn is TwoGangCountdown2Function
? '${value} sec'
: ((selectedFn as BaseSwitchFunction)
.getOperationalValues()
.firstWhere((v) => v.value == value)
.description),
); );
Navigator.pop(
final existingIndex = context, {selectedFn.code: functionData});
selectedFunctions.indexWhere((f) =>
f.function == selectedFn.code);
if (existingIndex != -1) {
selectedFunctions[existingIndex] =
functionData;
} else {
selectedFunctions.add(functionData);
} }
(context as Element).markNeedsBuild(); : null,
isConfirmEnabled: selectedFunction != null,
),
],
),
),
);
}, },
(condition) { );
setState(() {
selectedCondition = condition;
});
}, },
); );
} }
final values = static Widget _buildOperationalValuesList(
selectedFn.getOperationalValues(); BuildContext context,
BaseSwitchFunction function,
ValueNotifier<dynamic> valueNotifier,
) {
final values = function.getOperationalValues();
return ValueListenableBuilder<dynamic>(
valueListenable: valueNotifier,
builder: (context, selectedValue, _) {
return ListView.builder( return ListView.builder(
itemCount: values.length, itemCount: values.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -172,154 +194,43 @@ class TwoGangSwitchHelper {
), ),
trailing: Radio<dynamic>( trailing: Radio<dynamic>(
value: value.value, value: value.value,
groupValue: groupValue: selectedValue,
selectedValues[selectedFn.code],
onChanged: (newValue) { onChanged: (newValue) {
selectedValues[selectedFn.code] = valueNotifier.value = newValue;
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();
}, },
), ),
); );
}, },
); );
}, },
),
),
],
),
),
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,
),
),
),
],
),
],
),
),
);
},
);
},
); );
} }
/// 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 ValueListenableBuilder<dynamic>(
valueListenable: valueNotifier,
builder: (context, value, _) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
_buildConditionToggle( ValueListenableBuilder<List<bool>>(
context, valueListenable: conditionsNotifier,
setState, builder: (context, selectedConditions, _) {
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());
});
},
),
],
);
}
/// Build condition toggle for AC functions dialog
static Widget _buildConditionToggle(
BuildContext context,
StateSetter setState,
List<bool> selectedConditions,
Function(String?) onConditionSelected,
) {
return ToggleButtons( return ToggleButtons(
onPressed: (int index) { onPressed: (int index) {
setState(() { final newConditions = List<bool>.filled(3, false);
for (int i = 0; i < selectedConditions.length; i++) { newConditions[index] = true;
selectedConditions[i] = i == index; conditionsNotifier.value = newConditions;
} conditionNotifier.value = index == 0
onConditionSelected(index == 0
? "<" ? "<"
: index == 1 : index == 1
? "==" ? "=="
: ">"); : ">";
});
}, },
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: ColorsManager.primaryColorWithOpacity, selectedBorderColor: ColorsManager.primaryColorWithOpacity,
@ -333,5 +244,26 @@ class TwoGangSwitchHelper {
isSelected: selectedConditions, isSelected: selectedConditions,
children: const [Text("<"), Text("="), Text(">")], 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();
},
),
],
);
},
);
} }
} }

View File

@ -12,9 +12,7 @@ class CreateNewRoutineView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return Container(
create: (context) => RoutineBloc(),
child: Container(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@ -88,7 +86,6 @@ class CreateNewRoutineView extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }

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,6 +10,11 @@ class RoutinesView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<SwitchTabsBloc, SwitchTabsState>(
builder: (context, state) {
if (state is ShowCreateRoutineState && state.showCreateRoutine) {
return const CreateNewRoutineView();
}
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@ -15,11 +22,13 @@ class RoutinesView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Text("Create New Routines", Text(
"Create New Routines",
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: ColorsManager.grayColor, color: ColorsManager.grayColor,
)), ),
),
SizedBox( SizedBox(
height: 200, height: 200,
width: 150, width: 150,
@ -40,7 +49,10 @@ class RoutinesView extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: ColorsManager.graysColor, color: ColorsManager.graysColor,
borderRadius: BorderRadius.circular(120), borderRadius: BorderRadius.circular(120),
border: Border.all(color: ColorsManager.greyColor, width: 2.0), border: Border.all(
color: ColorsManager.greyColor,
width: 2.0,
),
), ),
height: 70, height: 70,
width: 70, width: 70,
@ -49,7 +61,8 @@ class RoutinesView extends StatelessWidget {
color: ColorsManager.dialogBlueTitle, color: ColorsManager.dialogBlueTitle,
size: 40, size: 40,
), ),
)), ),
),
), ),
), ),
), ),
@ -57,5 +70,7 @@ class RoutinesView extends StatelessWidget {
], ],
), ),
); );
},
);
} }
} }

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,48 +13,64 @@ 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) {
return const Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const ConditionTitleAndSearchBar(), ConditionTitleAndSearchBar(),
const SizedBox( SizedBox(height: 10),
height: 10, Wrap(
),
const Wrap(
spacing: 10, spacing: 10,
runSpacing: 10, runSpacing: 10,
children: [ children: [
DraggableCard( DraggableCard(
imagePath: Assets.tabToRun, imagePath: Assets.tabToRun,
title: 'Tab to run', title: 'Tab to run',
deviceData: {
'deviceId': 'tab_to_run',
'type': 'trigger',
'name': 'Tab to run',
},
), ),
DraggableCard( DraggableCard(
imagePath: Assets.map, imagePath: Assets.map,
title: 'Location', title: 'Location',
deviceData: {
'deviceId': 'location',
'type': 'trigger',
'name': 'Location',
},
), ),
DraggableCard( DraggableCard(
imagePath: Assets.weather, imagePath: Assets.weather,
title: 'Weather', title: 'Weather',
deviceData: {
'deviceId': 'weather',
'type': 'trigger',
'name': 'Weather',
},
), ),
DraggableCard( DraggableCard(
imagePath: Assets.schedule, imagePath: Assets.schedule,
title: 'Schedule', title: 'Schedule',
deviceData: {
'deviceId': 'schedule',
'type': 'trigger',
'name': 'Schedule',
},
), ),
], ],
), ),
const SizedBox( const SizedBox(height: 10),
height: 10,
),
const TitleRoutine( const TitleRoutine(
title: 'Conditions', title: 'Conditions',
subtitle: '(THEN)', subtitle: '(THEN)',
), ),
const SizedBox( const SizedBox(height: 10),
height: 10,
),
const Wrap( const Wrap(
spacing: 10, spacing: 10,
runSpacing: 10, runSpacing: 10,
@ -60,38 +78,42 @@ class ConditionsRoutinesDevicesView extends StatelessWidget {
DraggableCard( DraggableCard(
imagePath: Assets.notification, imagePath: Assets.notification,
title: 'Send Notification', title: 'Send Notification',
deviceData: {
'deviceId': 'notification',
'type': 'action',
'name': 'Send Notification',
},
), ),
DraggableCard( DraggableCard(
imagePath: Assets.delay, imagePath: Assets.delay,
title: 'Delay the action', title: 'Delay the action',
deviceData: {
'deviceId': 'delay',
'type': 'action',
'name': 'Delay the action',
},
), ),
], ],
), ),
const SizedBox( const SizedBox(height: 10),
height: 10,
),
const TitleRoutine( const TitleRoutine(
title: 'Routines', title: 'Routines',
subtitle: '(THEN)', subtitle: '(THEN)',
), ),
const SizedBox( const SizedBox(height: 10),
height: 10,
),
const ScenesAndAutomations(), const ScenesAndAutomations(),
const SizedBox( const SizedBox(height: 10),
height: 10,
),
const TitleRoutine( const TitleRoutine(
title: 'Devices', title: 'Devices',
subtitle: '', subtitle: '',
), ),
const SizedBox( const SizedBox(height: 10),
height: 10,
),
const RoutineDevices(), 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,59 +2,45 @@ 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 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( return Card(
color: ColorsManager.whiteColors, color: ColorsManager.whiteColors,
child: SizedBox( child: SizedBox(
@ -95,7 +81,7 @@ class DraggableCard extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 2, maxLines: 2,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: titleColor ?? ColorsManager.blackColor, color: ColorsManager.blackColor,
fontSize: 12, fontSize: 12,
), ),
), ),
@ -108,8 +94,7 @@ class DraggableCard extends StatelessWidget {
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
padding: padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
itemCount: deviceFunctions.length, itemCount: deviceFunctions.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final function = deviceFunctions[index]; final function = deviceFunctions[index];
@ -151,8 +136,6 @@ class DraggableCard extends StatelessWidget {
), ),
), ),
); );
},
);
} }
Widget _buildGreyContainer() { Widget _buildGreyContainer() {

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,6 +24,12 @@ class RoutineDevices extends StatelessWidget {
device.productType == '2G' || device.productType == '2G' ||
device.productType == '3G') device.productType == '3G')
.toList(); .toList();
// Provide the RoutineBloc to the child widgets
return BlocProvider.value(
value: routineBloc,
child: BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, routineState) {
return Wrap( return Wrap(
spacing: 10, spacing: 10,
runSpacing: 10, runSpacing: 10,
@ -35,7 +40,8 @@ class RoutineDevices extends StatelessWidget {
title: device.name ?? '', title: device.name ?? '',
deviceData: { deviceData: {
'key': UniqueKey().toString(), 'key': UniqueKey().toString(),
'imagePath': device.getDefaultIcon(device.productType), 'imagePath':
device.getDefaultIcon(device.productType),
'title': device.name ?? '', 'title': device.name ?? '',
'deviceId': device.uuid, 'deviceId': device.uuid,
'productType': device.productType, 'productType': device.productType,
@ -44,6 +50,9 @@ class RoutineDevices extends StatelessWidget {
); );
}).toList(), }).toList(),
); );
},
),
);
} }
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
}, },

View File

@ -4,22 +4,27 @@ 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()
..add(
const LoadScenes(spaceId),
)
..add(
const LoadAutomation(spaceId),
),
child: BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) { builder: (context, state) {
if (state.scenes.isNotEmpty || state.automations.isNotEmpty) { if (state.scenes.isNotEmpty || state.automations.isNotEmpty) {
var scenes = [...state.scenes, ...state.automations]; var scenes = [...state.scenes, ...state.automations];
@ -31,13 +36,19 @@ class ScenesAndAutomations extends StatelessWidget {
return DraggableCard( return DraggableCard(
imagePath: Assets.logo, imagePath: Assets.logo,
title: scene.name, title: scene.name,
deviceData: {
'deviceId': scene.id,
'name': scene.name,
'status': scene.status,
'type': scene.type,
'icon': scene.icon,
},
); );
}).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(),
), ),