mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-09 22:57:21 +00:00
push value notifers
This commit is contained in:
@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
|
||||
import 'package:syncrow_web/services/locator.dart';
|
||||
import 'package:syncrow_web/utils/app_routes.dart';
|
||||
@ -14,7 +15,8 @@ import 'package:syncrow_web/utils/theme/theme.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
try {
|
||||
const environment = String.fromEnvironment('FLAVOR', defaultValue: 'development');
|
||||
const environment =
|
||||
String.fromEnvironment('FLAVOR', defaultValue: 'development');
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
initialSetup();
|
||||
@ -46,10 +48,14 @@ class MyApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider(
|
||||
create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
)
|
||||
),
|
||||
BlocProvider<RoutineBloc>(
|
||||
create: (context) => RoutineBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
@ -1,219 +1,330 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/device_functions.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ACHelper {
|
||||
static Future<void> showACFunctionsDialog(
|
||||
static Future<Map<String, dynamic>?> showACFunctionsDialog(
|
||||
BuildContext context,
|
||||
List<DeviceFunction<dynamic>> functions,
|
||||
) async {
|
||||
List<ACFunction> acFunctions = functions.whereType<ACFunction>().toList();
|
||||
// Track multiple selections using a map
|
||||
Map<String, dynamic> selectedValues = {};
|
||||
List<DeviceFunctionData> selectedFunctions = [];
|
||||
String? selectedFunction;
|
||||
dynamic selectedValue = 20;
|
||||
String? selectedCondition = "==";
|
||||
List<bool> _selectedConditions = [false, true, false];
|
||||
|
||||
await showDialog(
|
||||
return showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 600,
|
||||
height: 450,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'AC Functions',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 15, horizontal: 50),
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: ColorsManager.greyColor,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: acFunctions.length,
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
color: ColorsManager.dividerColor,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final function = acFunctions[index];
|
||||
final isSelected =
|
||||
selectedValues.containsKey(function.code);
|
||||
return ListTile(
|
||||
tileColor:
|
||||
isSelected ? Colors.grey.shade100 : null,
|
||||
leading: SvgPicture.asset(
|
||||
function.icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: Text(
|
||||
function.operationName,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color:
|
||||
ColorsManager.primaryColorWithOpacity,
|
||||
size: 20,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
onTap: () {
|
||||
if (isSelected) {
|
||||
selectedValues.remove(function.code);
|
||||
selectedFunctions.removeWhere(
|
||||
(f) => f.function == function.code);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final selectedFunction = acFunctions.firstWhere(
|
||||
(f) => selectedValues.containsKey(f.code),
|
||||
orElse: () => acFunctions.first,
|
||||
);
|
||||
return _buildValueSelector(
|
||||
context,
|
||||
selectedFunction,
|
||||
selectedValues[selectedFunction.code],
|
||||
(value) {
|
||||
selectedValues[selectedFunction.code] = value;
|
||||
// Update or add the function data
|
||||
final functionData = DeviceFunctionData(
|
||||
entityId: selectedFunction.deviceId,
|
||||
function: selectedFunction.code,
|
||||
operationName: selectedFunction.operationName,
|
||||
value: value,
|
||||
valueDescription: _getValueDescription(
|
||||
selectedFunction, value),
|
||||
);
|
||||
|
||||
final existingIndex =
|
||||
selectedFunctions.indexWhere((f) =>
|
||||
f.function == selectedFunction.code);
|
||||
if (existingIndex != -1) {
|
||||
selectedFunctions[existingIndex] =
|
||||
functionData;
|
||||
} else {
|
||||
selectedFunctions.add(functionData);
|
||||
}
|
||||
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: ColorsManager.greyColor,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: ColorsManager.greyColor),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: selectedFunctions.isNotEmpty
|
||||
? () {
|
||||
// Add all selected functions to the bloc
|
||||
for (final function in selectedFunctions) {
|
||||
context
|
||||
.read<RoutineBloc>()
|
||||
.add(AddFunction(function));
|
||||
}
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
'Confirm',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: _buildDialogContent(
|
||||
context,
|
||||
setState,
|
||||
acFunctions,
|
||||
selectedFunction,
|
||||
selectedValue,
|
||||
selectedCondition,
|
||||
_selectedConditions,
|
||||
(fn) => selectedFunction = fn,
|
||||
(val) => selectedValue = val,
|
||||
(cond) => selectedCondition = cond,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildValueSelector(
|
||||
/// Build dialog content for AC functions dialog
|
||||
static Widget _buildDialogContent(
|
||||
BuildContext context,
|
||||
ACFunction function,
|
||||
StateSetter setState,
|
||||
List<ACFunction> acFunctions,
|
||||
String? selectedFunction,
|
||||
dynamic selectedValue,
|
||||
String? selectedCondition,
|
||||
List<bool> selectedConditions,
|
||||
Function(String?) onFunctionSelected,
|
||||
Function(dynamic) onValueSelected,
|
||||
Function(String?) onConditionSelected,
|
||||
) {
|
||||
final values = function.getOperationalValues();
|
||||
return Container(
|
||||
height: 200,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: ListView.builder(
|
||||
itemCount: values.length,
|
||||
width: selectedFunction != null ? 600 : 360,
|
||||
height: 450,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const DialogHeader('AC Functions'),
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFunctionsList(
|
||||
context,
|
||||
setState,
|
||||
acFunctions,
|
||||
selectedFunction,
|
||||
onFunctionSelected,
|
||||
),
|
||||
if (selectedFunction != null)
|
||||
_buildValueSelector(
|
||||
context,
|
||||
setState,
|
||||
selectedFunction,
|
||||
selectedValue,
|
||||
selectedCondition,
|
||||
selectedConditions,
|
||||
onValueSelected,
|
||||
onConditionSelected,
|
||||
acFunctions,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DialogFooter(
|
||||
onCancel: () => Navigator.pop(context),
|
||||
onConfirm: selectedFunction != null && selectedValue != null
|
||||
? () => Navigator.pop(context, {
|
||||
'function': selectedFunction,
|
||||
'value': selectedValue,
|
||||
'condition': selectedCondition ?? "==",
|
||||
})
|
||||
: null,
|
||||
isConfirmEnabled: selectedFunction != null && selectedValue != null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build functions list for AC functions dialog
|
||||
static Widget _buildFunctionsList(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
List<ACFunction> acFunctions,
|
||||
String? selectedFunction,
|
||||
Function(String?) onFunctionSelected,
|
||||
) {
|
||||
return Expanded(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: false,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: acFunctions.length,
|
||||
separatorBuilder: (context, index) => const Divider(
|
||||
color: ColorsManager.dividerColor,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final value = values[index];
|
||||
return RadioListTile<dynamic>(
|
||||
value: value.value,
|
||||
groupValue: selectedValue,
|
||||
onChanged: onValueSelected,
|
||||
title: Text(value.description),
|
||||
final function = acFunctions[index];
|
||||
return ListTile(
|
||||
leading: SvgPicture.asset(
|
||||
function.icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: Text(
|
||||
function.operationName,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
onTap: () => setState(() => onFunctionSelected(function.code)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _getValueDescription(ACFunction function, dynamic value) {
|
||||
final values = function.getOperationalValues();
|
||||
final selectedValue = values.firstWhere((v) => v.value == value);
|
||||
return selectedValue.description;
|
||||
/// Build value selector for AC functions dialog
|
||||
static Widget _buildValueSelector(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
String selectedFunction,
|
||||
dynamic selectedValue,
|
||||
String? selectedCondition,
|
||||
List<bool> selectedConditions,
|
||||
Function(dynamic) onValueSelected,
|
||||
Function(String?) onConditionSelected,
|
||||
List<ACFunction> acFunctions,
|
||||
) {
|
||||
if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') {
|
||||
return Expanded(
|
||||
child: _buildTemperatureSelector(
|
||||
context,
|
||||
setState,
|
||||
selectedValue,
|
||||
selectedCondition,
|
||||
selectedConditions,
|
||||
onValueSelected,
|
||||
onConditionSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final selectedFn =
|
||||
acFunctions.firstWhere((f) => f.code == selectedFunction);
|
||||
final values = selectedFn.getOperationalValues();
|
||||
return Expanded(
|
||||
child: _buildOperationalValuesList(
|
||||
context,
|
||||
setState,
|
||||
values,
|
||||
selectedValue,
|
||||
onValueSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build temperature selector for AC functions dialog
|
||||
static Widget _buildTemperatureSelector(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
dynamic selectedValue,
|
||||
String? selectedCondition,
|
||||
List<bool> selectedConditions,
|
||||
Function(dynamic) onValueSelected,
|
||||
Function(String?) onConditionSelected,
|
||||
) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildConditionToggle(
|
||||
context,
|
||||
setState,
|
||||
selectedConditions,
|
||||
onConditionSelected,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildTemperatureDisplay(context, selectedValue),
|
||||
const SizedBox(height: 20),
|
||||
_buildTemperatureSlider(
|
||||
context,
|
||||
setState,
|
||||
selectedValue,
|
||||
onValueSelected,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Build condition toggle for AC functions dialog
|
||||
static Widget _buildConditionToggle(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
List<bool> selectedConditions,
|
||||
Function(String?) onConditionSelected,
|
||||
) {
|
||||
return ToggleButtons(
|
||||
onPressed: (int index) {
|
||||
setState(() {
|
||||
for (int i = 0; i < selectedConditions.length; i++) {
|
||||
selectedConditions[i] = i == index;
|
||||
}
|
||||
onConditionSelected(index == 0
|
||||
? "<"
|
||||
: index == 1
|
||||
? "=="
|
||||
: ">");
|
||||
});
|
||||
},
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
|
||||
selectedColor: Colors.white,
|
||||
fillColor: ColorsManager.primaryColorWithOpacity,
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40.0,
|
||||
minWidth: 40.0,
|
||||
),
|
||||
isSelected: selectedConditions,
|
||||
children: const [Text("<"), Text("="), Text(">")],
|
||||
);
|
||||
}
|
||||
|
||||
/// Build temperature display for AC functions dialog
|
||||
static Widget _buildTemperatureDisplay(
|
||||
BuildContext context, dynamic selectedValue) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'${selectedValue ?? 20}°C',
|
||||
style: context.textTheme.headlineMedium!.copyWith(
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildTemperatureSlider(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
dynamic selectedValue,
|
||||
Function(dynamic) onValueSelected,
|
||||
) {
|
||||
final currentValue = selectedValue is int ? selectedValue.toDouble() : 20.0;
|
||||
return Slider(
|
||||
value: currentValue,
|
||||
min: 16,
|
||||
max: 30,
|
||||
divisions: 14,
|
||||
label: '${currentValue.toInt()}°C',
|
||||
onChanged: (value) {
|
||||
setState(() => onValueSelected(value.toInt()));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildOperationalValuesList(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
List<dynamic> values,
|
||||
dynamic selectedValue,
|
||||
Function(dynamic) onValueSelected,
|
||||
) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: false,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: values.length,
|
||||
itemBuilder: (context, index) {
|
||||
final value = values[index];
|
||||
return ListTile(
|
||||
leading: SvgPicture.asset(
|
||||
value.icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: Text(
|
||||
value.description,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: Radio<dynamic>(
|
||||
value: value.value,
|
||||
groupValue: selectedValue,
|
||||
onChanged: (newValue) {
|
||||
setState(() => onValueSelected(newValue));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/device_functions.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class OneGangSwitchHelper {
|
||||
static Future<void> showSwitchFunctionsDialog(
|
||||
BuildContext context, List<DeviceFunction<dynamic>> functions) async {
|
||||
static Future<Map<String, dynamic>?> showSwitchFunctionsDialog(
|
||||
BuildContext context,
|
||||
List<DeviceFunction<dynamic>> functions,
|
||||
) async {
|
||||
List<DeviceFunction<dynamic>> switchFunctions = functions
|
||||
.where(
|
||||
(f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction)
|
||||
.toList();
|
||||
Map<String, dynamic> selectedValues = {};
|
||||
List<DeviceFunctionData> selectedFunctions = [];
|
||||
String? selectedCondition = "<";
|
||||
List<bool> selectedConditions = [true, false, false];
|
||||
final selectedFunctionNotifier = ValueNotifier<String?>(null);
|
||||
final selectedValueNotifier = ValueNotifier<dynamic>(null);
|
||||
final selectedConditionNotifier = ValueNotifier<String?>("<");
|
||||
final selectedConditionsNotifier =
|
||||
ValueNotifier<List<bool>>([true, false, false]);
|
||||
|
||||
await showDialog(
|
||||
return showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return ValueListenableBuilder<String?>(
|
||||
valueListenable: selectedFunctionNotifier,
|
||||
builder: (context, selectedFunction, _) {
|
||||
return AlertDialog(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: Container(
|
||||
width: 600,
|
||||
width: selectedFunction != null ? 600 : 360,
|
||||
height: 450,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
@ -38,22 +42,7 @@ class OneGangSwitchHelper {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'1 Gang Light Switch Condition',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 15, horizontal: 50),
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: ColorsManager.greyColor,
|
||||
),
|
||||
),
|
||||
const DialogHeader('1 Gang Light Switch Condition'),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
@ -66,143 +55,67 @@ class OneGangSwitchHelper {
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final function = switchFunctions[index];
|
||||
final isSelected =
|
||||
selectedValues.containsKey(function.code);
|
||||
return ListTile(
|
||||
tileColor:
|
||||
isSelected ? Colors.grey.shade100 : null,
|
||||
leading: SvgPicture.asset(
|
||||
function.icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: Text(
|
||||
function.operationName,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: ColorsManager
|
||||
.primaryColorWithOpacity,
|
||||
size: 20,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
onTap: () {
|
||||
if (isSelected) {
|
||||
selectedValues.remove(function.code);
|
||||
selectedFunctions.removeWhere(
|
||||
(f) => f.function == function.code);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Right side: Value selector
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final selectedFn = switchFunctions.firstWhere(
|
||||
(f) => selectedValues.containsKey(f.code),
|
||||
orElse: () => switchFunctions.first,
|
||||
) as BaseSwitchFunction;
|
||||
|
||||
if (selectedFn is OneGangCountdownFunction) {
|
||||
return _buildCountDownSelector(
|
||||
context,
|
||||
setState,
|
||||
selectedValues[selectedFn.code] ?? 0,
|
||||
selectedCondition,
|
||||
selectedConditions,
|
||||
(value) {
|
||||
selectedValues[selectedFn.code] = value;
|
||||
final functionData = DeviceFunctionData(
|
||||
entityId: selectedFn.deviceId,
|
||||
function: selectedFn.code,
|
||||
operationName: selectedFn.operationName,
|
||||
value: value,
|
||||
condition: selectedCondition,
|
||||
valueDescription: '${value} sec',
|
||||
);
|
||||
|
||||
final existingIndex =
|
||||
selectedFunctions.indexWhere((f) =>
|
||||
f.function == selectedFn.code);
|
||||
if (existingIndex != -1) {
|
||||
selectedFunctions[existingIndex] =
|
||||
functionData;
|
||||
} else {
|
||||
selectedFunctions.add(functionData);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
(condition) {
|
||||
setState(() {
|
||||
selectedCondition = condition;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final values =
|
||||
selectedFn.getOperationalValues();
|
||||
return ListView.builder(
|
||||
itemCount: values.length,
|
||||
itemBuilder: (context, index) {
|
||||
final value = values[index];
|
||||
return ValueListenableBuilder<String?>(
|
||||
valueListenable: selectedFunctionNotifier,
|
||||
builder: (context, selectedFunction, _) {
|
||||
final isSelected =
|
||||
selectedFunction == function.code;
|
||||
return ListTile(
|
||||
tileColor: isSelected
|
||||
? Colors.grey.shade100
|
||||
: null,
|
||||
leading: SvgPicture.asset(
|
||||
value.icon,
|
||||
function.icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: Text(
|
||||
value.description,
|
||||
function.operationName,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: Radio<dynamic>(
|
||||
value: value.value,
|
||||
groupValue:
|
||||
selectedValues[selectedFn.code],
|
||||
onChanged: (newValue) {
|
||||
selectedValues[selectedFn.code] =
|
||||
newValue;
|
||||
final functionData =
|
||||
DeviceFunctionData(
|
||||
entityId: selectedFn.deviceId,
|
||||
function: selectedFn.code,
|
||||
operationName:
|
||||
selectedFn.operationName,
|
||||
value: newValue,
|
||||
valueDescription: value.description,
|
||||
);
|
||||
|
||||
final existingIndex =
|
||||
selectedFunctions.indexWhere(
|
||||
(f) =>
|
||||
f.function ==
|
||||
selectedFn.code);
|
||||
if (existingIndex != -1) {
|
||||
selectedFunctions[existingIndex] =
|
||||
functionData;
|
||||
} else {
|
||||
selectedFunctions.add(functionData);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
trailing: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
onTap: () {
|
||||
selectedFunctionNotifier.value =
|
||||
function.code;
|
||||
selectedValueNotifier.value =
|
||||
function is OneGangCountdownFunction
|
||||
? 0
|
||||
: null;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Right side: Value selector
|
||||
if (selectedFunction != null)
|
||||
ValueListenableBuilder<String?>(
|
||||
valueListenable: selectedFunctionNotifier,
|
||||
builder: (context, selectedFunction, _) {
|
||||
final selectedFn = switchFunctions.firstWhere(
|
||||
(f) => f.code == selectedFunction,
|
||||
);
|
||||
return Expanded(
|
||||
child: selectedFn is OneGangCountdownFunction
|
||||
? _buildCountDownSelector(
|
||||
context,
|
||||
selectedValueNotifier,
|
||||
selectedConditionNotifier,
|
||||
selectedConditionsNotifier,
|
||||
)
|
||||
: _buildOperationalValuesList(
|
||||
context,
|
||||
selectedFn as BaseSwitchFunction,
|
||||
selectedValueNotifier,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -211,41 +124,36 @@ class OneGangSwitchHelper {
|
||||
width: double.infinity,
|
||||
color: ColorsManager.greyColor,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: ColorsManager.greyColor),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: selectedFunctions.isNotEmpty
|
||||
? () {
|
||||
for (final function in selectedFunctions) {
|
||||
context
|
||||
.read<RoutineBloc>()
|
||||
.add(AddFunction(function));
|
||||
}
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
'Confirm',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
DialogFooter(
|
||||
onCancel: () => Navigator.pop(context),
|
||||
onConfirm: selectedFunctionNotifier.value != null &&
|
||||
selectedValueNotifier.value != null
|
||||
? () {
|
||||
final selectedFn = switchFunctions.firstWhere(
|
||||
(f) => f.code == selectedFunctionNotifier.value,
|
||||
);
|
||||
final value = selectedValueNotifier.value;
|
||||
final functionData = DeviceFunctionData(
|
||||
entityId: selectedFn.deviceId,
|
||||
function: selectedFn.code,
|
||||
operationName: selectedFn.operationName,
|
||||
value: value,
|
||||
condition: selectedConditionNotifier.value,
|
||||
valueDescription:
|
||||
selectedFn is OneGangCountdownFunction
|
||||
? '${value} sec'
|
||||
: ((selectedFn as BaseSwitchFunction)
|
||||
.getOperationalValues()
|
||||
.firstWhere((v) => v.value == value)
|
||||
.description),
|
||||
);
|
||||
Navigator.pop(
|
||||
context, {selectedFn.code: functionData});
|
||||
}
|
||||
: null,
|
||||
isConfirmEnabled:
|
||||
selectedFunctionNotifier.value != null &&
|
||||
selectedValueNotifier.value != null,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -257,77 +165,101 @@ class OneGangSwitchHelper {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build countdown selector for switch functions dialog
|
||||
static Widget _buildCountDownSelector(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
dynamic selectedValue,
|
||||
String? selectedCondition,
|
||||
List<bool> selectedConditions,
|
||||
Function(dynamic) onValueSelected,
|
||||
Function(String?) onConditionSelected,
|
||||
ValueNotifier<dynamic> valueNotifier,
|
||||
ValueNotifier<String?> conditionNotifier,
|
||||
ValueNotifier<List<bool>> conditionsNotifier,
|
||||
) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildConditionToggle(
|
||||
context,
|
||||
setState,
|
||||
selectedConditions,
|
||||
onConditionSelected,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'${selectedValue.toString()} sec',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Slider(
|
||||
value: selectedValue.toDouble(),
|
||||
min: 0,
|
||||
max: 300, // 5 minutes in seconds
|
||||
divisions: 300,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
onValueSelected(value.toInt());
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
return ValueListenableBuilder<dynamic>(
|
||||
valueListenable: valueNotifier,
|
||||
builder: (context, value, _) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ValueListenableBuilder<List<bool>>(
|
||||
valueListenable: conditionsNotifier,
|
||||
builder: (context, selectedConditions, _) {
|
||||
return ToggleButtons(
|
||||
onPressed: (int index) {
|
||||
final newConditions = List<bool>.filled(3, false);
|
||||
newConditions[index] = true;
|
||||
conditionsNotifier.value = newConditions;
|
||||
conditionNotifier.value = index == 0
|
||||
? "<"
|
||||
: index == 1
|
||||
? "=="
|
||||
: ">";
|
||||
},
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
|
||||
selectedColor: Colors.white,
|
||||
fillColor: ColorsManager.primaryColorWithOpacity,
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40.0,
|
||||
minWidth: 40.0,
|
||||
),
|
||||
isSelected: selectedConditions,
|
||||
children: const [Text("<"), Text("="), Text(">")],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'${value ?? 0} sec',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Slider(
|
||||
value: (value ?? 0).toDouble(),
|
||||
min: 0,
|
||||
max: 300,
|
||||
divisions: 300,
|
||||
onChanged: (newValue) {
|
||||
valueNotifier.value = newValue.toInt();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Build condition toggle for AC functions dialog
|
||||
static Widget _buildConditionToggle(
|
||||
static Widget _buildOperationalValuesList(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
List<bool> selectedConditions,
|
||||
Function(String?) onConditionSelected,
|
||||
BaseSwitchFunction function,
|
||||
ValueNotifier<dynamic> valueNotifier,
|
||||
) {
|
||||
return ToggleButtons(
|
||||
onPressed: (int index) {
|
||||
setState(() {
|
||||
for (int i = 0; i < selectedConditions.length; i++) {
|
||||
selectedConditions[i] = i == index;
|
||||
}
|
||||
onConditionSelected(index == 0
|
||||
? "<"
|
||||
: index == 1
|
||||
? "=="
|
||||
: ">");
|
||||
});
|
||||
final values = function.getOperationalValues();
|
||||
return ValueListenableBuilder<dynamic>(
|
||||
valueListenable: valueNotifier,
|
||||
builder: (context, selectedValue, _) {
|
||||
return ListView.builder(
|
||||
itemCount: values.length,
|
||||
itemBuilder: (context, index) {
|
||||
final value = values[index];
|
||||
return ListTile(
|
||||
leading: SvgPicture.asset(
|
||||
value.icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: Text(
|
||||
value.description,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: Radio<dynamic>(
|
||||
value: value.value,
|
||||
groupValue: selectedValue,
|
||||
onChanged: (newValue) {
|
||||
valueNotifier.value = newValue;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
|
||||
selectedColor: Colors.white,
|
||||
fillColor: ColorsManager.primaryColorWithOpacity,
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40.0,
|
||||
minWidth: 40.0,
|
||||
),
|
||||
isSelected: selectedConditions,
|
||||
children: const [Text("<"), Text("="), Text(">")],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/device_functions.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
@ -18,20 +20,23 @@ class TwoGangSwitchHelper {
|
||||
f is TwoGangCountdown1Function ||
|
||||
f is TwoGangCountdown2Function)
|
||||
.toList();
|
||||
Map<String, dynamic> selectedValues = {};
|
||||
List<DeviceFunctionData> selectedFunctions = [];
|
||||
String? selectedCondition = "<";
|
||||
List<bool> selectedConditions = [true, false, false];
|
||||
|
||||
final selectedFunctionNotifier = ValueNotifier<String?>(null);
|
||||
final selectedValueNotifier = ValueNotifier<dynamic>(null);
|
||||
final selectedConditionNotifier = ValueNotifier<String?>('<');
|
||||
final selectedConditionsNotifier =
|
||||
ValueNotifier<List<bool>>([true, false, false]);
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return ValueListenableBuilder<String?>(
|
||||
valueListenable: selectedFunctionNotifier,
|
||||
builder: (context, selectedFunction, _) {
|
||||
return AlertDialog(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: Container(
|
||||
width: 600,
|
||||
width: selectedFunction != null ? 600 : 300,
|
||||
height: 450,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
@ -41,22 +46,7 @@ class TwoGangSwitchHelper {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'2 Gangs Light Switch Condition',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 15, horizontal: 50),
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: ColorsManager.greyColor,
|
||||
),
|
||||
),
|
||||
const DialogHeader('2 Gangs Light Switch Condition'),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
@ -69,187 +59,105 @@ class TwoGangSwitchHelper {
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final function = switchFunctions[index];
|
||||
final isSelected =
|
||||
selectedValues.containsKey(function.code);
|
||||
return ListTile(
|
||||
tileColor:
|
||||
isSelected ? Colors.grey.shade100 : null,
|
||||
leading: SvgPicture.asset(
|
||||
function.icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: Text(
|
||||
function.operationName,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: ColorsManager
|
||||
.primaryColorWithOpacity,
|
||||
size: 20,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
onTap: () {
|
||||
if (isSelected) {
|
||||
selectedValues.remove(function.code);
|
||||
selectedFunctions.removeWhere(
|
||||
(f) => f.function == function.code);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Right side: Value selector
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final selectedFn = switchFunctions.firstWhere(
|
||||
(f) => selectedValues.containsKey(f.code),
|
||||
orElse: () => switchFunctions.first,
|
||||
) as BaseSwitchFunction;
|
||||
|
||||
if (selectedFn is TwoGangCountdown1Function ||
|
||||
selectedFn is TwoGangCountdown2Function) {
|
||||
return _buildCountDownSelector(
|
||||
context,
|
||||
setState,
|
||||
selectedValues[selectedFn.code] ?? 0,
|
||||
selectedCondition,
|
||||
selectedConditions,
|
||||
(value) {
|
||||
selectedValues[selectedFn.code] = value;
|
||||
final functionData = DeviceFunctionData(
|
||||
entityId: selectedFn.deviceId,
|
||||
function: selectedFn.code,
|
||||
operationName: selectedFn.operationName,
|
||||
value: value,
|
||||
condition: selectedCondition,
|
||||
valueDescription: '${value} sec',
|
||||
);
|
||||
|
||||
final existingIndex =
|
||||
selectedFunctions.indexWhere((f) =>
|
||||
f.function == selectedFn.code);
|
||||
if (existingIndex != -1) {
|
||||
selectedFunctions[existingIndex] =
|
||||
functionData;
|
||||
} else {
|
||||
selectedFunctions.add(functionData);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
(condition) {
|
||||
setState(() {
|
||||
selectedCondition = condition;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final values =
|
||||
selectedFn.getOperationalValues();
|
||||
return ListView.builder(
|
||||
itemCount: values.length,
|
||||
itemBuilder: (context, index) {
|
||||
final value = values[index];
|
||||
return ValueListenableBuilder<String?>(
|
||||
valueListenable: selectedFunctionNotifier,
|
||||
builder: (context, selectedFunction, _) {
|
||||
final isSelected =
|
||||
selectedFunction == function.code;
|
||||
return ListTile(
|
||||
tileColor: isSelected
|
||||
? Colors.grey.shade100
|
||||
: null,
|
||||
leading: SvgPicture.asset(
|
||||
value.icon,
|
||||
function.icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: Text(
|
||||
value.description,
|
||||
function.operationName,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: Radio<dynamic>(
|
||||
value: value.value,
|
||||
groupValue:
|
||||
selectedValues[selectedFn.code],
|
||||
onChanged: (newValue) {
|
||||
selectedValues[selectedFn.code] =
|
||||
newValue;
|
||||
final functionData =
|
||||
DeviceFunctionData(
|
||||
entityId: selectedFn.deviceId,
|
||||
function: selectedFn.code,
|
||||
operationName:
|
||||
selectedFn.operationName,
|
||||
value: newValue,
|
||||
valueDescription: value.description,
|
||||
);
|
||||
|
||||
final existingIndex =
|
||||
selectedFunctions.indexWhere(
|
||||
(f) =>
|
||||
f.function ==
|
||||
selectedFn.code);
|
||||
if (existingIndex != -1) {
|
||||
selectedFunctions[existingIndex] =
|
||||
functionData;
|
||||
} else {
|
||||
selectedFunctions.add(functionData);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
trailing: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
onTap: () {
|
||||
selectedFunctionNotifier.value =
|
||||
function.code;
|
||||
selectedValueNotifier.value = function
|
||||
is TwoGangCountdown1Function ||
|
||||
function
|
||||
is TwoGangCountdown2Function
|
||||
? 0
|
||||
: null;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Right side: Value selector
|
||||
if (selectedFunction != null)
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<dynamic>(
|
||||
valueListenable: selectedValueNotifier,
|
||||
builder: (context, selectedValue, _) {
|
||||
final selectedFn = switchFunctions.firstWhere(
|
||||
(f) => f.code == selectedFunction,
|
||||
);
|
||||
|
||||
if (selectedFn is TwoGangCountdown1Function ||
|
||||
selectedFn is TwoGangCountdown2Function) {
|
||||
return _buildCountDownSelector(
|
||||
context,
|
||||
selectedValueNotifier,
|
||||
selectedConditionNotifier,
|
||||
selectedConditionsNotifier,
|
||||
);
|
||||
}
|
||||
|
||||
return _buildOperationalValuesList(
|
||||
context,
|
||||
selectedFn as BaseSwitchFunction,
|
||||
selectedValueNotifier,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: ColorsManager.greyColor,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: ColorsManager.greyColor),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: selectedFunctions.isNotEmpty
|
||||
? () {
|
||||
for (final function in selectedFunctions) {
|
||||
context
|
||||
.read<RoutineBloc>()
|
||||
.add(AddFunction(function));
|
||||
}
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
'Confirm',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
DialogFooter(
|
||||
onCancel: () => Navigator.pop(context),
|
||||
onConfirm: selectedFunction != null &&
|
||||
selectedValueNotifier.value != null
|
||||
? () {
|
||||
final selectedFn = switchFunctions.firstWhere(
|
||||
(f) => f.code == selectedFunction,
|
||||
);
|
||||
final value = selectedValueNotifier.value;
|
||||
final functionData = DeviceFunctionData(
|
||||
entityId: selectedFn.deviceId,
|
||||
function: selectedFn.code,
|
||||
operationName: selectedFn.operationName,
|
||||
value: value,
|
||||
condition: selectedConditionNotifier.value,
|
||||
valueDescription: selectedFn
|
||||
is TwoGangCountdown1Function ||
|
||||
selectedFn is TwoGangCountdown2Function
|
||||
? '${value} sec'
|
||||
: ((selectedFn as BaseSwitchFunction)
|
||||
.getOperationalValues()
|
||||
.firstWhere((v) => v.value == value)
|
||||
.description),
|
||||
);
|
||||
Navigator.pop(
|
||||
context, {selectedFn.code: functionData});
|
||||
}
|
||||
: null,
|
||||
isConfirmEnabled: selectedFunction != null,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -261,77 +169,101 @@ class TwoGangSwitchHelper {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build countdown selector for switch functions dialog
|
||||
static Widget _buildCountDownSelector(
|
||||
static Widget _buildOperationalValuesList(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
dynamic selectedValue,
|
||||
String? selectedCondition,
|
||||
List<bool> selectedConditions,
|
||||
Function(dynamic) onValueSelected,
|
||||
Function(String?) onConditionSelected,
|
||||
BaseSwitchFunction function,
|
||||
ValueNotifier<dynamic> valueNotifier,
|
||||
) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildConditionToggle(
|
||||
context,
|
||||
setState,
|
||||
selectedConditions,
|
||||
onConditionSelected,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'${selectedValue.toString()} sec',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Slider(
|
||||
value: selectedValue.toDouble(),
|
||||
min: 0,
|
||||
max: 300, // 5 minutes in seconds
|
||||
divisions: 300,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
onValueSelected(value.toInt());
|
||||
});
|
||||
final values = function.getOperationalValues();
|
||||
return ValueListenableBuilder<dynamic>(
|
||||
valueListenable: valueNotifier,
|
||||
builder: (context, selectedValue, _) {
|
||||
return ListView.builder(
|
||||
itemCount: values.length,
|
||||
itemBuilder: (context, index) {
|
||||
final value = values[index];
|
||||
return ListTile(
|
||||
leading: SvgPicture.asset(
|
||||
value.icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: Text(
|
||||
value.description,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: Radio<dynamic>(
|
||||
value: value.value,
|
||||
groupValue: selectedValue,
|
||||
onChanged: (newValue) {
|
||||
valueNotifier.value = newValue;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Build condition toggle for AC functions dialog
|
||||
static Widget _buildConditionToggle(
|
||||
static Widget _buildCountDownSelector(
|
||||
BuildContext context,
|
||||
StateSetter setState,
|
||||
List<bool> selectedConditions,
|
||||
Function(String?) onConditionSelected,
|
||||
ValueNotifier<dynamic> valueNotifier,
|
||||
ValueNotifier<String?> conditionNotifier,
|
||||
ValueNotifier<List<bool>> conditionsNotifier,
|
||||
) {
|
||||
return ToggleButtons(
|
||||
onPressed: (int index) {
|
||||
setState(() {
|
||||
for (int i = 0; i < selectedConditions.length; i++) {
|
||||
selectedConditions[i] = i == index;
|
||||
}
|
||||
onConditionSelected(index == 0
|
||||
? "<"
|
||||
: index == 1
|
||||
? "=="
|
||||
: ">");
|
||||
});
|
||||
return ValueListenableBuilder<dynamic>(
|
||||
valueListenable: valueNotifier,
|
||||
builder: (context, value, _) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ValueListenableBuilder<List<bool>>(
|
||||
valueListenable: conditionsNotifier,
|
||||
builder: (context, selectedConditions, _) {
|
||||
return ToggleButtons(
|
||||
onPressed: (int index) {
|
||||
final newConditions = List<bool>.filled(3, false);
|
||||
newConditions[index] = true;
|
||||
conditionsNotifier.value = newConditions;
|
||||
conditionNotifier.value = index == 0
|
||||
? "<"
|
||||
: index == 1
|
||||
? "=="
|
||||
: ">";
|
||||
},
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
|
||||
selectedColor: Colors.white,
|
||||
fillColor: ColorsManager.primaryColorWithOpacity,
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40.0,
|
||||
minWidth: 40.0,
|
||||
),
|
||||
isSelected: selectedConditions,
|
||||
children: const [Text("<"), Text("="), Text(">")],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'${value ?? 0} sec',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Slider(
|
||||
value: (value ?? 0).toDouble(),
|
||||
min: 0,
|
||||
max: 300,
|
||||
divisions: 300,
|
||||
onChanged: (newValue) {
|
||||
valueNotifier.value = newValue.toInt();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
|
||||
selectedColor: Colors.white,
|
||||
fillColor: ColorsManager.primaryColorWithOpacity,
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40.0,
|
||||
minWidth: 40.0,
|
||||
),
|
||||
isSelected: selectedConditions,
|
||||
children: const [Text("<"), Text("="), Text(">")],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,82 +12,79 @@ class CreateNewRoutineView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => RoutineBloc(),
|
||||
child: Container(
|
||||
alignment: Alignment.topLeft,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const RoutineSearchAndButtons(),
|
||||
const SizedBox(height: 20),
|
||||
Flexible(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: const ConditionsRoutinesDevicesView()),
|
||||
),
|
||||
return Container(
|
||||
alignment: Alignment.topLeft,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const RoutineSearchAndButtons(),
|
||||
const SizedBox(height: 20),
|
||||
Flexible(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: const ConditionsRoutinesDevicesView()),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
/// IF Container
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(15),
|
||||
topRight: Radius.circular(15),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
/// IF Container
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(15),
|
||||
topRight: Radius.circular(15),
|
||||
),
|
||||
child: const IfContainer(),
|
||||
),
|
||||
child: const IfContainer(),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 2,
|
||||
width: double.infinity,
|
||||
color: ColorsManager.dialogBlueTitle,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 2,
|
||||
width: double.infinity,
|
||||
color: ColorsManager.dialogBlueTitle,
|
||||
),
|
||||
|
||||
/// THEN Container
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(15),
|
||||
bottomRight: Radius.circular(15),
|
||||
),
|
||||
/// THEN Container
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(15),
|
||||
bottomRight: Radius.circular(15),
|
||||
),
|
||||
child: const ThenContainer(),
|
||||
),
|
||||
child: const ThenContainer(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class RoutinesView extends StatelessWidget {
|
||||
@ -8,54 +10,67 @@ class RoutinesView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Create New Routines",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorsManager.grayColor,
|
||||
)),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
width: 150,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<SwitchTabsBloc>(context).add(
|
||||
const CreateNewRoutineViewEvent(true),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
color: ColorsManager.whiteColors,
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.graysColor,
|
||||
borderRadius: BorderRadius.circular(120),
|
||||
border: Border.all(color: ColorsManager.greyColor, width: 2.0),
|
||||
),
|
||||
height: 70,
|
||||
width: 70,
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.dialogBlueTitle,
|
||||
size: 40,
|
||||
),
|
||||
)),
|
||||
return BlocBuilder<SwitchTabsBloc, SwitchTabsState>(
|
||||
builder: (context, state) {
|
||||
if (state is ShowCreateRoutineState && state.showCreateRoutine) {
|
||||
return const CreateNewRoutineView();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Create New Routines",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorsManager.grayColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
width: 150,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<SwitchTabsBloc>(context).add(
|
||||
const CreateNewRoutineViewEvent(true),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
color: ColorsManager.whiteColors,
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.graysColor,
|
||||
borderRadius: BorderRadius.circular(120),
|
||||
border: Border.all(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
height: 70,
|
||||
width: 70,
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.dialogBlueTitle,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/routine_devices.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart';
|
||||
@ -11,87 +13,107 @@ class ConditionsRoutinesDevicesView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ConditionTitleAndSearchBar(),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
return BlocBuilder<RoutineBloc, RoutineState>(
|
||||
builder: (context, state) {
|
||||
return const Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DraggableCard(
|
||||
imagePath: Assets.tabToRun,
|
||||
title: 'Tab to run',
|
||||
ConditionTitleAndSearchBar(),
|
||||
SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
DraggableCard(
|
||||
imagePath: Assets.tabToRun,
|
||||
title: 'Tab to run',
|
||||
deviceData: {
|
||||
'deviceId': 'tab_to_run',
|
||||
'type': 'trigger',
|
||||
'name': 'Tab to run',
|
||||
},
|
||||
),
|
||||
DraggableCard(
|
||||
imagePath: Assets.map,
|
||||
title: 'Location',
|
||||
deviceData: {
|
||||
'deviceId': 'location',
|
||||
'type': 'trigger',
|
||||
'name': 'Location',
|
||||
},
|
||||
),
|
||||
DraggableCard(
|
||||
imagePath: Assets.weather,
|
||||
title: 'Weather',
|
||||
deviceData: {
|
||||
'deviceId': 'weather',
|
||||
'type': 'trigger',
|
||||
'name': 'Weather',
|
||||
},
|
||||
),
|
||||
DraggableCard(
|
||||
imagePath: Assets.schedule,
|
||||
title: 'Schedule',
|
||||
deviceData: {
|
||||
'deviceId': 'schedule',
|
||||
'type': 'trigger',
|
||||
'name': 'Schedule',
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
DraggableCard(
|
||||
imagePath: Assets.map,
|
||||
title: 'Location',
|
||||
const SizedBox(height: 10),
|
||||
const TitleRoutine(
|
||||
title: 'Conditions',
|
||||
subtitle: '(THEN)',
|
||||
),
|
||||
DraggableCard(
|
||||
imagePath: Assets.weather,
|
||||
title: 'Weather',
|
||||
const SizedBox(height: 10),
|
||||
const Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
DraggableCard(
|
||||
imagePath: Assets.notification,
|
||||
title: 'Send Notification',
|
||||
deviceData: {
|
||||
'deviceId': 'notification',
|
||||
'type': 'action',
|
||||
'name': 'Send Notification',
|
||||
},
|
||||
),
|
||||
DraggableCard(
|
||||
imagePath: Assets.delay,
|
||||
title: 'Delay the action',
|
||||
deviceData: {
|
||||
'deviceId': 'delay',
|
||||
'type': 'action',
|
||||
'name': 'Delay the action',
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
DraggableCard(
|
||||
imagePath: Assets.schedule,
|
||||
title: 'Schedule',
|
||||
const SizedBox(height: 10),
|
||||
const TitleRoutine(
|
||||
title: 'Routines',
|
||||
subtitle: '(THEN)',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const ScenesAndAutomations(),
|
||||
const SizedBox(height: 10),
|
||||
const TitleRoutine(
|
||||
title: 'Devices',
|
||||
subtitle: '',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const RoutineDevices(),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const TitleRoutine(
|
||||
title: 'Conditions',
|
||||
subtitle: '(THEN)',
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
DraggableCard(
|
||||
imagePath: Assets.notification,
|
||||
title: 'Send Notification',
|
||||
),
|
||||
DraggableCard(
|
||||
imagePath: Assets.delay,
|
||||
title: 'Delay the action',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const TitleRoutine(
|
||||
title: 'Routines',
|
||||
subtitle: '(THEN)',
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const ScenesAndAutomations(),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const TitleRoutine(
|
||||
title: 'Devices',
|
||||
subtitle: '',
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const RoutineDevices(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
76
lib/pages/routiens/widgets/dialog_footer.dart
Normal file
76
lib/pages/routiens/widgets/dialog_footer.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
31
lib/pages/routiens/widgets/dialog_header.dart
Normal file
31
lib/pages/routiens/widgets/dialog_header.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -2,156 +2,139 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/device_functions.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class DraggableCard extends StatelessWidget {
|
||||
final String imagePath;
|
||||
final String title;
|
||||
final Map<String, dynamic> deviceData;
|
||||
|
||||
const DraggableCard({
|
||||
super.key,
|
||||
required this.imagePath,
|
||||
required this.title,
|
||||
this.titleColor,
|
||||
this.isDragged = false,
|
||||
this.isDisabled = false,
|
||||
this.deviceData,
|
||||
required this.deviceData,
|
||||
});
|
||||
|
||||
final String imagePath;
|
||||
final String title;
|
||||
final Color? titleColor;
|
||||
final bool isDragged;
|
||||
final bool isDisabled;
|
||||
final Map<String, dynamic>? deviceData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget card = Draggable<Map<String, dynamic>>(
|
||||
data: deviceData ??
|
||||
{
|
||||
'key': UniqueKey().toString(),
|
||||
'imagePath': imagePath,
|
||||
'title': title,
|
||||
},
|
||||
feedback: Transform.rotate(
|
||||
angle: -0.1,
|
||||
child: _buildCardContent(context),
|
||||
),
|
||||
childWhenDragging: _buildGreyContainer(),
|
||||
child: _buildCardContent(context),
|
||||
);
|
||||
|
||||
if (isDisabled) {
|
||||
card = AbsorbPointer(child: card);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
Widget _buildCardContent(BuildContext context) {
|
||||
return BlocBuilder<RoutineBloc, RoutineState>(
|
||||
builder: (context, state) {
|
||||
// Filter functions for this device
|
||||
final deviceFunctions = state.selectedFunctions
|
||||
.where((f) => f.entityId == deviceData?['deviceId'])
|
||||
.where((f) => f.entityId == deviceData['deviceId'])
|
||||
.toList();
|
||||
|
||||
return Card(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: SizedBox(
|
||||
width: 90,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 123,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 50,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.CircleImageBackground,
|
||||
borderRadius: BorderRadius.circular(90),
|
||||
border: Border.all(
|
||||
color: ColorsManager.graysColor,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: imagePath.contains('.svg')
|
||||
? SvgPicture.asset(
|
||||
imagePath,
|
||||
)
|
||||
: Image.network(imagePath),
|
||||
return Draggable<Map<String, dynamic>>(
|
||||
data: deviceData,
|
||||
feedback: Transform.rotate(
|
||||
angle: -0.1,
|
||||
child: _buildCardContent(context, deviceFunctions),
|
||||
),
|
||||
childWhenDragging: _buildGreyContainer(),
|
||||
child: _buildCardContent(context, deviceFunctions),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardContent(
|
||||
BuildContext context, List<DeviceFunctionData> deviceFunctions) {
|
||||
return Card(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: SizedBox(
|
||||
width: 90,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 123,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 50,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.CircleImageBackground,
|
||||
borderRadius: BorderRadius.circular(90),
|
||||
border: Border.all(
|
||||
color: ColorsManager.graysColor,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: imagePath.contains('.svg')
|
||||
? SvgPicture.asset(
|
||||
imagePath,
|
||||
)
|
||||
: Image.network(imagePath),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (deviceFunctions.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
||||
itemCount: deviceFunctions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final function = deviceFunctions[index];
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
'${function.operationName}: ${function.valueDescription}',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: titleColor ?? ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
fontSize: 9,
|
||||
color: ColorsManager.textGray,
|
||||
height: 1.2,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.read<RoutineBloc>().add(
|
||||
RemoveFunction(function),
|
||||
);
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 12,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (deviceFunctions.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
||||
itemCount: deviceFunctions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final function = deviceFunctions[index];
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${function.operationName}: ${function.valueDescription}',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 9,
|
||||
color: ColorsManager.textGray,
|
||||
height: 1.2,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.read<RoutineBloc>().add(
|
||||
RemoveFunction(function),
|
||||
);
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 12,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ class IfContainer extends StatelessWidget {
|
||||
key: Key(item['key']!),
|
||||
imagePath: item['imagePath']!,
|
||||
title: item['title']!,
|
||||
deviceData: item,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
|
@ -1,20 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart';
|
||||
|
||||
class RoutineDevices extends StatelessWidget {
|
||||
const RoutineDevices({
|
||||
super.key,
|
||||
});
|
||||
const RoutineDevices({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the RoutineBloc instance from the parent
|
||||
final routineBloc = context.read<RoutineBloc>();
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => DeviceManagementBloc()
|
||||
..add(
|
||||
FetchDevices(),
|
||||
),
|
||||
create: (context) => DeviceManagementBloc()..add(FetchDevices()),
|
||||
child: BlocBuilder<DeviceManagementBloc, DeviceManagementState>(
|
||||
builder: (context, state) {
|
||||
if (state is DeviceManagementLoaded) {
|
||||
@ -25,24 +24,34 @@ class RoutineDevices extends StatelessWidget {
|
||||
device.productType == '2G' ||
|
||||
device.productType == '3G')
|
||||
.toList();
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: deviceList.asMap().entries.map((entry) {
|
||||
final device = entry.value;
|
||||
return DraggableCard(
|
||||
imagePath: device.getDefaultIcon(device.productType),
|
||||
title: device.name ?? '',
|
||||
deviceData: {
|
||||
'key': UniqueKey().toString(),
|
||||
'imagePath': device.getDefaultIcon(device.productType),
|
||||
'title': device.name ?? '',
|
||||
'deviceId': device.uuid,
|
||||
'productType': device.productType,
|
||||
'functions': device.functions,
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
// Provide the RoutineBloc to the child widgets
|
||||
return BlocProvider.value(
|
||||
value: routineBloc,
|
||||
child: BlocBuilder<RoutineBloc, RoutineState>(
|
||||
builder: (context, routineState) {
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: deviceList.asMap().entries.map((entry) {
|
||||
final device = entry.value;
|
||||
return DraggableCard(
|
||||
imagePath: device.getDefaultIcon(device.productType),
|
||||
title: device.name ?? '',
|
||||
deviceData: {
|
||||
'key': UniqueKey().toString(),
|
||||
'imagePath':
|
||||
device.getDefaultIcon(device.productType),
|
||||
'title': device.name ?? '',
|
||||
'deviceId': device.uuid,
|
||||
'productType': device.productType,
|
||||
'functions': device.functions,
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
|
@ -4,40 +4,51 @@ import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class ScenesAndAutomations extends StatelessWidget {
|
||||
class ScenesAndAutomations extends StatefulWidget {
|
||||
const ScenesAndAutomations({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ScenesAndAutomations> createState() => _ScenesAndAutomationsState();
|
||||
}
|
||||
|
||||
class _ScenesAndAutomationsState extends State<ScenesAndAutomations> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<RoutineBloc>()
|
||||
..add(const LoadScenes(spaceId))
|
||||
..add(const LoadAutomation(spaceId));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => RoutineBloc()
|
||||
..add(
|
||||
const LoadScenes(spaceId),
|
||||
)
|
||||
..add(
|
||||
const LoadAutomation(spaceId),
|
||||
),
|
||||
child: BlocBuilder<RoutineBloc, RoutineState>(
|
||||
builder: (context, state) {
|
||||
if (state.scenes.isNotEmpty || state.automations.isNotEmpty) {
|
||||
var scenes = [...state.scenes, ...state.automations];
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: scenes.asMap().entries.map((entry) {
|
||||
final scene = entry.value;
|
||||
return DraggableCard(
|
||||
imagePath: Assets.logo,
|
||||
title: scene.name,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
),
|
||||
return BlocBuilder<RoutineBloc, RoutineState>(
|
||||
builder: (context, state) {
|
||||
if (state.scenes.isNotEmpty || state.automations.isNotEmpty) {
|
||||
var scenes = [...state.scenes, ...state.automations];
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: scenes.asMap().entries.map((entry) {
|
||||
final scene = entry.value;
|
||||
return DraggableCard(
|
||||
imagePath: Assets.logo,
|
||||
title: scene.name,
|
||||
deviceData: {
|
||||
'deviceId': scene.id,
|
||||
'name': scene.name,
|
||||
'status': scene.status,
|
||||
'type': scene.type,
|
||||
'icon': scene.icon,
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ class ThenContainer extends StatelessWidget {
|
||||
key: Key(item['key']!),
|
||||
imagePath: item['imagePath']!,
|
||||
title: item['title']!,
|
||||
deviceData: item,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
|
Reference in New Issue
Block a user