Merge pull request #224 from SyncrowIOT/SP-1597-FE-Add-Device-Settings-Column-and-Build-Device-Settings-Dialog-UI

Sp 1597 fe add device settings column and build device settings dialog UI
This commit is contained in:
mohammadnemer1
2025-06-03 16:56:24 +03:00
committed by GitHub
22 changed files with 1490 additions and 44 deletions
+4
View File
@@ -211,6 +211,7 @@ class _DynamicTableState extends State<DynamicTable> {
onChanged: widget.withSelectAll && widget.data.isNotEmpty
? _toggleSelectAll
: null,
),
);
}
@@ -281,6 +282,7 @@ class _DynamicTableState extends State<DynamicTable> {
padding: EdgeInsets.symmetric(
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
vertical: 4),
child: Text(
title,
style: context.textTheme.titleSmall!.copyWith(
@@ -301,6 +303,7 @@ class _DynamicTableState extends State<DynamicTable> {
required int rowIndex,
required int columnIndex,
}) {
bool isBatteryLevel = content.endsWith('%');
double? batteryLevel;
@@ -312,6 +315,7 @@ class _DynamicTableState extends State<DynamicTable> {
if (isSettingsColumn) {
return _buildSettingsIcon(rowIndex, size);
}
Color? statusColor;
switch (content) {
@@ -95,7 +95,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
return const RoutinesView();
}
if (state.createRoutineView) {
return CreateNewRoutineView();
return const CreateNewRoutineView();
}
return BlocBuilder<DeviceManagementBloc, DeviceManagementState>(
@@ -6,9 +6,11 @@ import 'package:syncrow_web/pages/common/filter/filter_widget.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/models/devices_model.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_search_filters.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/device_settings_panel.dart';
import 'package:syncrow_web/pages/device_managment/shared/device_batch_control_dialog.dart';
import 'package:syncrow_web/pages/device_managment/shared/device_control_dialog.dart';
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/format_date_time.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import 'package:syncrow_web/utils/style.dart';
@@ -58,7 +60,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
'Low Battery ($lowBatteryCount)',
];
final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control';
final buttonLabel =
(selectedDevices.length > 1) ? 'Batch Control' : 'Control';
return Row(
children: [
@@ -105,18 +108,23 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
if (selectedDevices.length == 1) {
showDialog(
context: context,
builder: (context) => DeviceControlDialog(
builder: (context) =>
DeviceControlDialog(
device: selectedDevices.first,
),
);
} else if (selectedDevices.length > 1) {
final productTypes = selectedDevices
.map((device) => device.productType)
.toSet();
} else if (selectedDevices.length >
1) {
final productTypes =
selectedDevices
.map((device) =>
device.productType)
.toSet();
if (productTypes.length == 1) {
showDialog(
context: context,
builder: (context) => DeviceBatchControlDialog(
builder: (context) =>
DeviceBatchControlDialog(
devices: selectedDevices,
),
);
@@ -130,7 +138,9 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: isControlButtonEnabled ? Colors.white : Colors.grey,
color: isControlButtonEnabled
? Colors.white
: Colors.grey,
),
),
),
@@ -166,29 +176,40 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
'Installation Date and Time',
'Status',
'Last Offline Date and Time',
'Settings'
],
data: devicesToShow.map((device) {
final combinedSpaceNames = device.spaces != null
? device.spaces!.map((space) => space.spaceName).join(' > ') +
? device.spaces!
.map((space) => space.spaceName)
.join(' > ') +
(device.community != null
? ' > ${device.community!.name}'
: '')
: (device.community != null ? device.community!.name : '');
: (device.community != null
? device.community!.name
: '');
return [
device.name ?? '',
device.productName ?? '',
device.uuid ?? '',
(device.spaces != null && device.spaces!.isNotEmpty)
(device.spaces != null &&
device.spaces!.isNotEmpty)
? device.spaces![0].spaceName
: '',
combinedSpaceNames,
device.batteryLevel != null ? '${device.batteryLevel}%' : '-',
formatDateTime(DateTime.fromMillisecondsSinceEpoch(
(device.createTime ?? 0) * 1000)),
device.batteryLevel != null
? '${device.batteryLevel}%'
: '-',
formatDateTime(
DateTime.fromMillisecondsSinceEpoch(
(device.createTime ?? 0) * 1000)),
device.online == true ? 'Online' : 'Offline',
formatDateTime(DateTime.fromMillisecondsSinceEpoch(
(device.updateTime ?? 0) * 1000)),
formatDateTime(
DateTime.fromMillisecondsSinceEpoch(
(device.updateTime ?? 0) * 1000)),
'Settings',
];
}).toList(),
onSelectionChanged: (selectedRows) {
@@ -202,6 +223,10 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
.map((device) => device.uuid!)
.toList(),
isEmpty: devicesToShow.isEmpty,
onSettingsPressed: (rowIndex) {
final device = devicesToShow[rowIndex];
showDeviceSettingsSidebar(context, device);
},
),
),
)
@@ -213,4 +238,37 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
},
);
}
void showDeviceSettingsSidebar(BuildContext context, AllDevicesModel device) {
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: "Device Settings",
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, anim1, anim2) {
return Align(
alignment: Alignment.centerRight,
child: Material(
child: Container(
width: MediaQuery.of(context).size.width * 0.3,
color: ColorsManager.whiteColors,
child: DeviceSettingsPanel(
device: device,
onClose: () => Navigator.of(context).pop(),
),
),
),
);
},
transitionBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(anim1),
child: child,
);
},
);
}
}
@@ -0,0 +1,165 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
import 'package:syncrow_web/services/space_mana_api.dart';
import 'package:syncrow_web/utils/snack_bar.dart';
part 'setting_bloc_event.dart';
class SettingDeviceBloc extends Bloc<DeviceSettingEvent, DeviceSettingsState> {
final String deviceId;
SettingDeviceBloc({
required this.deviceId,
}) : super(const DeviceSettingsInitial()) {
on<DeviceSettingInitialInfo>(_fetchDeviceInfo);
on<SettingBlocSaveName>(_saveName);
on<ChangeNameEvent>(_changeName);
on<SettingBlocDeleteDevice>(_deleteDevice);
on<SettingBlocFetchRooms>(_fetchRooms);
on<SettingBlocAssignRoom>(_onAssignDevice);
}
final TextEditingController nameController = TextEditingController();
List<SubSpaceModel> roomsList = [];
bool isEditingName = false;
bool _validateInputs() {
final nameError = _fullNameValidator(nameController.text);
if (nameError != null) {
CustomSnackBar.displaySnackBar(nameError);
return true;
}
return false;
}
String? _fullNameValidator(String? value) {
if (value == null) return 'name is required';
final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim();
if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) {
return 'name must be between 2 and 30 characters long';
}
if (RegExp(r"/[^ a-zA-Z0-9-\']/").hasMatch(withoutExtraSpaces)) {
return 'Only alphanumeric characters, space, dash and single quote are allowed';
}
return null;
}
Future<void> _saveName(
SettingBlocSaveName event, Emitter<DeviceSettingsState> emit) async {
if (_validateInputs()) return;
try {
emit(DeviceSettingsLoading());
await DevicesManagementApi.putDeviceName(
deviceId: deviceId, deviceName: nameController.text);
add(DeviceSettingInitialInfo());
CustomSnackBar.displaySnackBar('Save Successfully');
emit(DeviceSettingsUpdate(deviceName: nameController.text));
} catch (e) {
emit(DeviceSettingsError(message: e.toString()));
}
}
Future<void> _fetchDeviceInfo(
DeviceSettingInitialInfo event, Emitter<DeviceSettingsState> emit) async {
try {
emit(DeviceSettingsLoading());
var response = await DevicesManagementApi.getDeviceInfo(deviceId);
DeviceInfoModel deviceInfo = DeviceInfoModel.fromJson(response);
nameController.text = deviceInfo.name;
emit(DeviceSettingsUpdate(
deviceName: nameController.text,
deviceInfo: deviceInfo,
roomsList: roomsList,
));
} catch (e) {
emit(DeviceSettingsError(message: e.toString()));
}
}
bool editName = false;
final FocusNode focusNode = FocusNode();
void _changeName(ChangeNameEvent event, Emitter<DeviceSettingsState> emit) {
emit(DeviceSettingsInitial(
deviceName: nameController.text,
deviceId: deviceId,
isEditingName: event.value ?? false,
editingNameValue: event.value?.toString() ?? '',
deviceInfo: state.deviceInfo,
));
editName = event.value!;
if (editName) {
Future.delayed(const Duration(milliseconds: 500), () {
focusNode.requestFocus();
});
} else {
add(const SettingBlocSaveName());
focusNode.unfocus();
}
emit(DeviceSettingsUpdate(
deviceName: nameController.text,
deviceInfo: state.deviceInfo,
roomsList: roomsList,
));
}
void _deleteDevice(
SettingBlocDeleteDevice event, Emitter<DeviceSettingsState> emit) async {
try {
emit(DeviceSettingsLoading());
await DevicesManagementApi.resetDevice(devicesUuid: deviceId);
CustomSnackBar.displaySnackBar('Reset Successfully');
emit(DeviceSettingsUpdate(
deviceName: nameController.text,
deviceInfo: state.deviceInfo,
roomsList: roomsList,
));
} catch (e) {
emit(DeviceSettingsError(message: e.toString()));
return;
}
}
void _onAssignDevice(
SettingBlocAssignRoom event, Emitter<DeviceSettingsState> emit) async {
try {
emit(DeviceSettingsLoading());
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
await CommunitySpaceManagementApi.assignDeviceToRoom(
communityId: event.communityUuid,
spaceId: event.spaceUuid,
subSpaceId: event.subSpaceUuid,
deviceId: deviceId,
projectId: projectUuid);
add(DeviceSettingInitialInfo());
CustomSnackBar.displaySnackBar('Save Successfully');
emit(DeviceSettingsSaveSelectionSuccess());
} catch (e) {
emit(DeviceSettingsError(message: e.toString()));
return;
}
}
void _fetchRooms(
SettingBlocFetchRooms event, Emitter<DeviceSettingsState> emit) async {
try {
emit(DeviceSettingsLoading());
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
roomsList = await CommunitySpaceManagementApi.getSubSpaceBySpaceId(
communityId: event.communityUuid,
spaceId: event.spaceUuid,
projectId: projectUuid);
emit(DeviceSettingsUpdate(
deviceName: nameController.text,
deviceInfo: state.deviceInfo,
roomsList: roomsList,
));
} catch (e) {
emit(DeviceSettingsError(message: e.toString()));
return;
}
}
}
@@ -0,0 +1,69 @@
part of 'setting_bloc_bloc.dart';
abstract class DeviceSettingEvent extends Equatable {
const DeviceSettingEvent();
@override
List<Object?> get props => [];
}
class SettingBlocSaveDeviceName extends DeviceSettingEvent {
final String deviceName;
final String deviceId;
const SettingBlocSaveDeviceName(
{required this.deviceName, required this.deviceId});
@override
List<Object?> get props => [deviceName, deviceId];
}
class SettingBlocStartEditingName extends DeviceSettingEvent {}
class SettingBlocCancelEditingName extends DeviceSettingEvent {}
class SettingBlocChangeEditingNameValue extends DeviceSettingEvent {
final String value;
const SettingBlocChangeEditingNameValue(this.value);
@override
List<Object?> get props => [value];
}
class SettingBlocFetchRooms extends DeviceSettingEvent {
final String communityUuid;
final String spaceUuid;
const SettingBlocFetchRooms(
{required this.communityUuid, required this.spaceUuid});
@override
List<Object?> get props => [communityUuid, spaceUuid];
}
class SettingBlocSaveName extends DeviceSettingEvent {
const SettingBlocSaveName();
}
class DeviceSettingInitialInfo extends DeviceSettingEvent {}
class ChangeNameEvent extends DeviceSettingEvent {
final bool? value;
const ChangeNameEvent({this.value});
}
class SettingBlocDeleteDevice extends DeviceSettingEvent {}
class SettingBlocAssignRoom extends DeviceSettingEvent {
final String communityUuid;
final String spaceUuid;
final String subSpaceUuid;
const SettingBlocAssignRoom({
required this.communityUuid,
required this.spaceUuid,
required this.subSpaceUuid,
});
@override
List<Object?> get props => [spaceUuid, communityUuid, subSpaceUuid];
}
@@ -0,0 +1,81 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
abstract class DeviceSettingsState extends Equatable {
const DeviceSettingsState({this.deviceInfo});
final DeviceInfoModel? deviceInfo;
@override
List<Object?> get props => [deviceInfo];
}
class DeviceSettingsInitial extends DeviceSettingsState {
final String deviceName;
final String deviceId;
final bool isEditingName;
final String editingNameValue;
const DeviceSettingsInitial({
this.deviceName = '',
this.deviceId = '',
this.isEditingName = false,
this.editingNameValue = '',
super.deviceInfo,
});
DeviceSettingsInitial copyWith({
String? deviceName,
String? deviceId,
bool? isEditingName,
String? editingNameValue,
}) =>
DeviceSettingsInitial(
deviceName: deviceName ?? this.deviceName,
deviceId: deviceId ?? this.deviceId,
isEditingName: isEditingName ?? this.isEditingName,
editingNameValue: editingNameValue ?? this.editingNameValue,
);
@override
List<Object?> get props =>
[deviceName, deviceId, isEditingName, editingNameValue];
}
class DeviceSettingsLoading extends DeviceSettingsState {}
class DeviceSettingsUpdate extends DeviceSettingsState {
final String? deviceName;
final List<SubSpaceModel> roomsList;
const DeviceSettingsUpdate({
this.deviceName,
super.deviceInfo,
this.roomsList = const [],
});
@override
List<Object?> get props => [deviceName, deviceInfo, roomsList];
}
class DeviceSettingsError extends DeviceSettingsState {
final String message;
const DeviceSettingsError({required this.message});
@override
List<Object?> get props => [message];
}
class DeviceSettingsFetchRooms extends DeviceSettingsState {
final List<SubSpaceModel> roomsList;
const DeviceSettingsFetchRooms({required this.roomsList});
@override
List<Object?> get props => [roomsList];
}
class DeviceSettingsSaveSelectionSuccess extends DeviceSettingsState {}
class ChangeNameState extends DeviceSettingsState {}
@@ -0,0 +1,28 @@
import 'package:syncrow_web/utils/constants/assets.dart';
class DeviceIconTypeHelper {
static const Map<String, String> _iconMap = {
'AC': Assets.ac,
'GW': Assets.gateway,
'CPS': Assets.sensors,
'DL': Assets.doorLock,
'WPS': Assets.sensors,
'3G': Assets.gangSwitch,
'2G': Assets.twoGang,
'1G': Assets.oneGang,
'CUR': Assets.curtain,
'WH': Assets.waterHeater,
'DS': Assets.doorSensor,
'1GT': Assets.oneTouchSwitch,
'2GT': Assets.twoTouchSwitch,
'3GT': Assets.threeTouchSwitch,
'GD': Assets.garageDoor,
'WL': Assets.waterLeakNormal,
'NCPS': Assets.sensors,
};
static String getDeviceIconByTypeCode(String? typeCode) {
if (typeCode == null) return Assets.logoHorizontal;
return _iconMap[typeCode] ?? Assets.logoHorizontal;
}
}
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/web_layout/default_container.dart';
class DeviceManagementContent extends StatelessWidget {
const DeviceManagementContent({
super.key,
required this.device,
required this.subSpaces,
required this.deviceInfo,
});
final AllDevicesModel device;
final List<SubSpaceModel> subSpaces;
final DeviceInfoModel deviceInfo;
@override
Widget build(BuildContext context) {
Widget infoRow(
{required String label,
required String value,
Widget? trailing,
required Color? valueColor}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: context.theme.textTheme.bodyMedium!.copyWith(
fontSize: 14,
color: ColorsManager.grayColor,
),
),
Expanded(
child: Text(
value,
textAlign: TextAlign.end,
style: context.theme.textTheme.bodyMedium!
.copyWith(fontSize: 14, color: valueColor),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
trailing ?? const SizedBox.shrink(),
],
),
);
}
return DefaultContainer(
padding: EdgeInsets.zero,
child: Column(
children: [
const SizedBox(height: 5),
Padding(
padding: const EdgeInsets.all(10.0),
child: InkWell(
onTap: () {
showSubSpaceDialog(
context,
communityUuid: device.community!.uuid!,
spaceUuid: device.spaces!.first.uuid!,
subSpaces: subSpaces,
selected: device.subspace!.uuid,
);
},
child: infoRow(
label: 'Sub-Space:',
value: deviceInfo.subspace.subspaceName,
valueColor: ColorsManager.textGray,
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.greyColor,
),
),
),
),
const Divider(color: ColorsManager.dividerColor),
Padding(
padding: const EdgeInsets.all(10.0),
child: infoRow(
label: 'Virtual Address:',
value: deviceInfo.productUuid,
valueColor: ColorsManager.blackColor,
trailing: InkWell(
onTap: () {
Clipboard.setData(
ClipboardData(text: device.productUuid ?? ''),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Virtual Address copied to clipboard'),
),
);
},
child: const Icon(
Icons.copy,
size: 16,
color: ColorsManager.greyColor,
),
),
),
),
const Divider(color: ColorsManager.dividerColor),
Padding(
padding: const EdgeInsets.all(10.0),
child: infoRow(
label: 'MAC Address:',
valueColor: ColorsManager.blackColor,
value: deviceInfo.macAddress,
),
),
const SizedBox(height: 5),
],
),
);
}
}
@@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/device_icon_type_helper.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/device_management_content.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/remove_device_widget.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/web_layout/default_container.dart';
class DeviceSettingsPanel extends StatelessWidget {
final VoidCallback? onClose;
final AllDevicesModel device;
const DeviceSettingsPanel({super.key, this.onClose, required this.device});
@override
Widget build(BuildContext context) {
final sectionTitle = context.theme.textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.grayColor,
);
return BlocProvider(
create: (context) => SettingDeviceBloc(
deviceId: device.uuid ?? '',
)
..add(DeviceSettingInitialInfo())
..add(SettingBlocFetchRooms(
communityUuid: device.community!.uuid!,
spaceUuid: device.spaces!.first.uuid!,
)),
child: Builder(
builder: (context) {
return BlocBuilder<SettingDeviceBloc, DeviceSettingsState>(
builder: (context, state) {
final _bloc = context.read<SettingDeviceBloc>();
final iconPath = DeviceIconTypeHelper.getDeviceIconByTypeCode(
device.productType);
final deviceInfo = state is DeviceSettingsUpdate
? state.deviceInfo ?? DeviceInfoModel.empty()
: DeviceInfoModel.empty();
final subSpaces =
state is DeviceSettingsUpdate ? state.roomsList ?? [] : [];
return Stack(
children: [
Container(
width: MediaQuery.of(context).size.width * 0.3,
color: ColorsManager.grey25,
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 24),
child: ListView(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: SvgPicture.asset(Assets.closeSettingsIcon),
onPressed:
onClose ?? () => Navigator.of(context).pop(),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Device Settings',
style:
context.theme.textTheme.titleLarge!.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.primaryColor,
),
),
],
),
const SizedBox(height: 24),
DefaultContainer(
child: Row(
children: [
CircleAvatar(
radius: 40,
backgroundColor:
const Color.fromARGB(177, 213, 213, 213),
child: CircleAvatar(
backgroundColor: ColorsManager.whiteColors,
radius: 36,
child: SvgPicture.asset(
iconPath,
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Device Name:',
style: context.textTheme.bodyMedium!
.copyWith(
color: ColorsManager.grayColor,
),
),
TextFormField(
maxLength: 30,
style: const TextStyle(
color: ColorsManager.blackColor,
),
textAlign: TextAlign.start,
focusNode: _bloc.focusNode,
controller: _bloc.nameController,
enabled: _bloc.editName,
onFieldSubmitted: (value) {
_bloc.add(const ChangeNameEvent(
value: false));
},
decoration: const InputDecoration(
border: InputBorder.none,
fillColor: Colors.white10,
counterText: '',
),
),
],
),
),
const SizedBox(width: 8),
Visibility(
visible: _bloc.editName != true,
replacement: const SizedBox(),
child: GestureDetector(
onTap: () {
_bloc.add(
const ChangeNameEvent(value: true));
},
child: SvgPicture.asset(
Assets.editNameIconSettings,
color: ColorsManager.grayColor,
height: 20,
width: 20,
),
),
)
],
),
),
const SizedBox(height: 32),
Text('Device Management', style: sectionTitle),
DeviceManagementContent(
device: device,
subSpaces: subSpaces.cast<SubSpaceModel>(),
deviceInfo: deviceInfo,
),
const SizedBox(height: 32),
RemoveDeviceWidget(bloc: _bloc),
],
),
),
if (state is DeviceSettingsLoading)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.1),
child: const Center(
child: CircularProgressIndicator(
color: ColorsManager.primaryColor,
),
),
),
),
],
);
},
);
},
),
);
}
}
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/web_layout/default_container.dart';
class RemoveDeviceWidget extends StatelessWidget {
const RemoveDeviceWidget({
super.key,
required SettingDeviceBloc bloc,
}) : _bloc = bloc;
final SettingDeviceBloc _bloc;
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: InkWell(
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(
'Remove Device',
style: context.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w700,
color: ColorsManager.red,
),
),
content: Text(
'Are you sure you want to remove this device?',
style: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.grayColor,
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.grayColor,
),
),
),
TextButton(
onPressed: () {
_bloc.add(SettingBlocDeleteDevice());
Navigator.of(context).pop();
},
child: Text(
'Remove',
style: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.red,
),
),
),
],
);
},
);
},
child: DefaultContainer(
padding: const EdgeInsets.all(25),
child: Center(
child: Text(
'Remove Device',
style: context.textTheme.bodyMedium!.copyWith(
fontSize: 14,
color: ColorsManager.red,
fontWeight: FontWeight.w700),
),
),
),
),
);
}
}
@@ -0,0 +1,183 @@
class DeviceInfoModel {
final int activeTime;
final String category;
final String categoryName;
final int createTime;
final String gatewayId;
final String icon;
final String ip;
final String lat;
final String localKey;
final String lon;
final String model;
final String name;
final String nodeId;
final bool online;
final String ownerId;
final String productName;
final bool sub;
final String timeZone;
final int updateTime;
final String uuid;
final String productUuid;
final String productType;
final String permissionType;
final String macAddress;
final Subspace subspace;
DeviceInfoModel({
required this.activeTime,
required this.category,
required this.categoryName,
required this.createTime,
required this.gatewayId,
required this.icon,
required this.ip,
required this.lat,
required this.localKey,
required this.lon,
required this.model,
required this.name,
required this.nodeId,
required this.online,
required this.ownerId,
required this.productName,
required this.sub,
required this.timeZone,
required this.updateTime,
required this.uuid,
required this.productUuid,
required this.productType,
required this.permissionType,
required this.macAddress,
required this.subspace,
});
factory DeviceInfoModel.fromJson(Map<String, dynamic> json) {
return DeviceInfoModel(
activeTime: json['activeTime'] as int? ?? 0,
category: json['category'] ?? '',
categoryName: json['categoryName'] as String? ?? '',
createTime: json['createTime'] as int? ?? 0,
gatewayId: json['gatewayId'] as String? ?? '',
icon: json['icon'] as String? ?? '',
ip: json['ip'] as String? ?? '',
lat: json['lat'] as String? ?? '',
localKey: json['localKey'] as String? ?? '',
lon: json['lon'] as String? ?? '',
model: json['model'] as String? ?? '',
name: json['name'] as String? ?? '',
nodeId: json['nodeId'] as String? ?? '',
online: json['online'] as bool? ?? false,
ownerId: json['ownerId'] as String? ?? '',
productName: json['productName'] as String? ?? '',
sub: json['sub'] as bool? ?? false,
timeZone: json['timeZone'] as String? ?? '',
updateTime: json['updateTime'] as int? ?? 0,
uuid: json['uuid'] as String? ?? '',
productUuid: json['productUuid'] as String? ?? '',
productType: json['productType'] as String? ?? '',
permissionType: json['permissionType'] as String? ?? '',
macAddress: json['macAddress'] as String? ?? '',
subspace:
Subspace.fromJson(json['subspace'] as Map<String, dynamic>? ?? {}),
);
}
Map<String, dynamic> toJson() {
return {
'activeTime': activeTime,
'category': category,
'categoryName': categoryName,
'createTime': createTime,
'gatewayId': gatewayId,
'icon': icon,
'ip': ip,
'lat': lat,
'localKey': localKey,
'lon': lon,
'model': model,
'name': name,
'nodeId': nodeId,
'online': online,
'ownerId': ownerId,
'productName': productName,
'sub': sub,
'timeZone': timeZone,
'updateTime': updateTime,
'uuid': uuid,
'productUuid': productUuid,
'productType': productType,
'permissionType': permissionType,
'macAddress': macAddress,
'subspace': subspace.toJson(),
};
}
static DeviceInfoModel empty() {
return DeviceInfoModel(
activeTime: 0,
category: '',
categoryName: '',
createTime: 0,
gatewayId: '',
icon: '',
ip: '',
lat: '',
localKey: '',
lon: '',
model: '',
name: '',
nodeId: '',
online: false,
ownerId: '',
productName: '',
sub: false,
timeZone: '',
updateTime: 0,
uuid: '',
productUuid: '',
productType: '',
permissionType: '',
macAddress: '',
subspace: Subspace(
uuid: '',
createdAt: '',
updatedAt: '',
subspaceName: '',
),
);
}
}
class Subspace {
final String uuid;
final String createdAt;
final String updatedAt;
final String subspaceName;
Subspace({
required this.uuid,
required this.createdAt,
required this.updatedAt,
required this.subspaceName,
});
factory Subspace.fromJson(Map<String, dynamic> json) {
return Subspace(
uuid: json['uuid'] as String? ?? '',
createdAt: json['createdAt'] as String? ?? '',
updatedAt: json['updatedAt'] as String? ?? '',
subspaceName: json['subspaceName'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'createdAt': createdAt,
'updatedAt': updatedAt,
'subspaceName': subspaceName,
};
}
}
@@ -0,0 +1,35 @@
import 'package:syncrow_web/pages/visitor_password/model/device_model.dart';
class SubSpaceModel {
final String? id;
final String? name;
List<DeviceModel>? devices;
SubSpaceModel({
required this.id,
required this.name,
required this.devices,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'devices': devices?.map((device) => device.toJson()).toList(),
};
}
factory SubSpaceModel.fromJson(Map<String, dynamic> json) {
List<DeviceModel> devices = [];
if (json['devices'] != null) {
for (var device in json['devices']) {
devices.add(DeviceModel.fromJson(device));
}
}
return SubSpaceModel(
id: json['uuid'] as String? ?? '',
name: json['subspaceName'] as String? ?? '',
devices: devices.isNotEmpty ? devices : null as List<DeviceModel>?,
);
}
}
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/subspace_dialog_buttons.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubSpaceDialog extends StatefulWidget {
final List<SubSpaceModel> subSpaces;
final String? selected;
final void Function(SubSpaceModel?) onConfirmed;
const SubSpaceDialog({
Key? key,
required this.subSpaces,
this.selected,
required this.onConfirmed,
}) : super(key: key);
@override
State<SubSpaceDialog> createState() => _SubSpaceDialogState();
}
class _SubSpaceDialogState extends State<SubSpaceDialog> {
String? _selectedId;
@override
void initState() {
super.initState();
_selectedId = widget.selected;
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: ColorsManager.whiteColors,
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 60),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
child: Container(
width: MediaQuery.of(context).size.width * 0.35,
padding: const EdgeInsets.fromLTRB(0, 24, 0, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Sub-Space',
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700,
color: ColorsManager.blueColor,
fontSize: 20),
),
const Divider(),
const SizedBox(height: 10),
...widget.subSpaces.map((space) {
return RadioListTile<String>(
value: space.id!,
groupValue: _selectedId,
onChanged: (value) {
setState(() {
_selectedId = value;
});
},
activeColor: Color(0xFF2962FF),
title: Text(
space.name ?? 'Unnamed Sub-Space',
style: context.textTheme.bodyMedium?.copyWith(
fontSize: 15,
color: ColorsManager.grayColor,
fontWeight: FontWeight.w400,
),
),
controlAffinity: ListTileControlAffinity.trailing,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
);
}).toList(),
const SizedBox(height: 12),
const Divider(height: 1, thickness: 1),
SubSpaceDialogButtons(selectedId: _selectedId, widget: widget),
],
),
),
);
}
}
void showSubSpaceDialog(
BuildContext context, {
required List<SubSpaceModel> subSpaces,
String? selected,
required String communityUuid,
required String spaceUuid,
}) {
showDialog(
context: context,
barrierDismissible: true,
builder: (ctx) => SubSpaceDialog(
subSpaces: subSpaces,
selected: selected,
onConfirmed: (selectedModel) {
if (selectedModel != null) {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
subSpaceUuid: selectedModel.id ?? '',
),
);
}
},
),
);
}
@@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubSpaceDialogButtons extends StatelessWidget {
const SubSpaceDialogButtons({
super.key,
required String? selectedId,
required this.widget,
}) : _selectedId = selectedId;
final String? _selectedId;
final SubSpaceDialog widget;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 50,
child: Row(
children: [
Expanded(
child: Container(
decoration: const BoxDecoration(
border: Border(
right: BorderSide(
color: ColorsManager.dividerColor,
width: 0.5,
),
),
),
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.textGray,
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
),
),
Expanded(
child: Container(
decoration: const BoxDecoration(
border: Border(
left: BorderSide(
color: ColorsManager.dividerColor,
width: 0.5,
),
),
),
child: TextButton(
onPressed: _selectedId == null
? null
: () {
final selectedModel = widget.subSpaces.firstWhere(
(space) => space.id == _selectedId,
orElse: () =>
SubSpaceModel(id: null, name: '', devices: []));
widget.onConfirmed(selectedModel);
Navigator.of(context).pop();
},
child: Text(
'Confirm',
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.secondaryColor,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
),
),
),
],
),
);
}
}
void showSubSpaceDialog(
BuildContext context, {
required List<SubSpaceModel> subSpaces,
String? selected,
required String communityUuid,
required String spaceUuid,
}) {
showDialog(
context: context,
barrierDismissible: true,
builder: (ctx) => SubSpaceDialog(
subSpaces: subSpaces,
selected: selected,
onConfirmed: (selectedModel) {
if (selectedModel != null) {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
subSpaceUuid: selectedModel.id ?? '',
),
);
}
},
),
);
}