Add new grey color constant and new icons for settings in assets

Update CreateNewRoutineView to use const constructor
Add SubSpaceModel class for device settings
Add DefaultContainer widget for web layout
Add events and states for device settings bloc
Update API endpoints for device settings
This commit is contained in:
mohammad
2025-05-29 14:26:24 +03:00
parent 010960c89b
commit a44d4231f1
17 changed files with 1031 additions and 44 deletions

View File

@ -0,0 +1,182 @@
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'],
category: json['category'],
categoryName: json['categoryName'],
createTime: json['createTime'],
gatewayId: json['gatewayId'],
icon: json['icon'],
ip: json['ip'] ?? "",
lat: json['lat'],
localKey: json['localKey'],
lon: json['lon'],
model: json['model'],
name: json['name'],
nodeId: json['nodeId'],
online: json['online'],
ownerId: json['ownerId'],
productName: json['productName'],
sub: json['sub'],
timeZone: json['timeZone'],
updateTime: json['updateTime'],
uuid: json['uuid'],
productUuid: json['productUuid'],
productType: json['productType'],
permissionType: json['permissionType'] ?? '',
macAddress: json['macAddress'],
subspace: Subspace.fromJson(json['subspace']),
);
}
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'],
createdAt: json['createdAt'],
updatedAt: json['updatedAt'],
subspaceName: json['subspaceName'],
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'createdAt': createdAt,
'updatedAt': updatedAt,
'subspaceName': subspaceName,
};
}
}

View File

@ -0,0 +1,149 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/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/bloc/sub_space_model.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
import 'package:syncrow_web/utils/snack_bar.dart';
part 'setting_bloc_event.dart';
class SettingBlocBloc extends Bloc<SettingBlocEvent, DeviceSettingsState> {
final String deviceId;
SettingBlocBloc({
required this.deviceId,
}) : super(const SettingBlocInitial()) {
on<DeviceSettingInitialInfo>(fetchDeviceInfo);
on<SaveNameEvent>(saveName);
on<ChangeNameEvent>(_changeName);
on<DeleteDeviceEvent>(deleteDevice);
//on<FetchRoomsEvent>(_fetchRoomsAndDevices);
}
static String deviceName = '';
final TextEditingController nameController =
TextEditingController(text: deviceName);
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(
SaveNameEvent event, Emitter<DeviceSettingsState> emit) async {
if (_validateInputs()) return;
try {
emit(SettingLoadingState());
var response = await DevicesManagementApi.putDeviceName(
deviceId: deviceId, deviceName: nameController.text);
add(DeviceSettingInitialInfo());
CustomSnackBar.displaySnackBar('Save Successfully');
emit(UpdateSettingState(deviceName: nameController.text));
} catch (e) {
emit(ErrorState(message: e.toString()));
} finally {
// isSaving = false;
}
}
DeviceInfoModel deviceInfo = 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: "",
),
);
Future fetchDeviceInfo(
DeviceSettingInitialInfo event, Emitter<DeviceSettingsState> emit) async {
try {
emit(SettingLoadingState());
var response = await DevicesManagementApi.getDeviceInfo(deviceId);
deviceInfo = DeviceInfoModel.fromJson(response);
nameController.text = deviceInfo.name;
emit(UpdateSettingState(
deviceName: nameController.text,
deviceInfo: deviceInfo,
));
} catch (e) {
emit(ErrorState(message: e.toString()));
}
}
bool editName = false;
final FocusNode focusNode = FocusNode();
void _changeName(ChangeNameEvent event, Emitter<DeviceSettingsState> emit) {
emit(SettingLoadingState());
editName = event.value!;
if (editName) {
Future.delayed(const Duration(milliseconds: 500), () {
focusNode.requestFocus();
});
} else {
add(const SaveNameEvent());
focusNode.unfocus();
}
emit(UpdateSettingState(deviceName: deviceName, deviceInfo: deviceInfo));
}
void deleteDevice(
DeleteDeviceEvent event, Emitter<DeviceSettingsState> emit) async {
try {
emit(SettingLoadingState());
var response =
await DevicesManagementApi.resetDevise(devicesUuid: deviceId);
CustomSnackBar.displaySnackBar('Reset Successfully');
emit(UpdateSettingState(
deviceName: nameController.text,
deviceInfo: deviceInfo,
));
} catch (e) {
emit(ErrorState(message: e.toString()));
return;
}
}
}

View File

@ -0,0 +1,50 @@
part of 'setting_bloc_bloc.dart';
abstract class SettingBlocEvent extends Equatable {
const SettingBlocEvent();
@override
List<Object?> get props => [];
}
class SaveDeviceName extends SettingBlocEvent {
final String deviceName;
final String deviceId;
const SaveDeviceName({required this.deviceName, required this.deviceId});
@override
List<Object?> get props => [deviceName, deviceId];
}
class StartEditingName extends SettingBlocEvent {}
class CancelEditingName extends SettingBlocEvent {}
class ChangeEditingNameValue extends SettingBlocEvent {
final String value;
const ChangeEditingNameValue(this.value);
@override
List<Object?> get props => [value];
}
class FetchRoomsEvent extends SettingBlocEvent {
final String deviceId;
const FetchRoomsEvent(this.deviceId);
@override
List<Object?> get props => [deviceId];
}
class SaveNameEvent extends SettingBlocEvent {
const SaveNameEvent();
}
class DeviceSettingInitialInfo extends SettingBlocEvent {}
class ChangeNameEvent extends SettingBlocEvent {
final bool? value;
const ChangeNameEvent({this.value});
}
class DeleteDeviceEvent extends SettingBlocEvent {}

View File

@ -0,0 +1,69 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/device_info_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart';
abstract class DeviceSettingsState extends Equatable {
const DeviceSettingsState();
@override
List<Object?> get props => [];
}
class SettingBlocInitial extends DeviceSettingsState {
final String deviceName;
final String deviceId;
final bool isEditingName;
final String editingNameValue;
const SettingBlocInitial({
this.deviceName = '',
this.deviceId = '',
this.isEditingName = false,
this.editingNameValue = '',
});
SettingBlocInitial copyWith({
String? deviceName,
String? deviceId,
bool? isEditingName,
String? editingNameValue,
}) =>
SettingBlocInitial(
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 SettingLoadingState extends DeviceSettingsState {}
class UpdateSettingState extends DeviceSettingsState {
final String deviceName;
final DeviceInfoModel? deviceInfo;
const UpdateSettingState({required this.deviceName, this.deviceInfo});
@override
List<Object?> get props => [deviceName, deviceInfo];
}
class ErrorState extends DeviceSettingsState {
final String message;
const ErrorState({required this.message});
@override
List<Object?> get props => [message];
}
class FetchRoomsState extends DeviceSettingsState {
final List<SubSpaceModel> roomsList;
const FetchRoomsState({required this.roomsList});
@override
List<Object?> get props => [roomsList];
}

View File

@ -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'],
name: json['subspaceName'],
devices: devices,
);
}
}

View File

@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.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/bloc/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/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({this.onClose, super.key, required this.device});
@override
Widget build(BuildContext context) {
final sectionTitle = context.theme.textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.grayColor,
);
Widget infoRow(
{required String label, required String value, Widget? trailing}) {
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: ColorsManager.blackColor,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
trailing ?? const SizedBox.shrink(),
],
),
);
}
return BlocProvider(
create: (context) => SettingBlocBloc(
deviceId: device.uuid ?? '',
)..add(DeviceSettingInitialInfo()),
child: BlocBuilder<SettingBlocBloc, DeviceSettingsState>(
builder: (context, state) {
final iconPath =
DeviceTypeHelper.getDeviceIconByTypeCode(device.productType);
final _bloc = BlocProvider.of<SettingBlocBloc>(context);
DeviceInfoModel deviceInfo = DeviceInfoModel.empty();
if (state is UpdateSettingState) {
deviceInfo = state.deviceInfo!;
}
return Container(
width: MediaQuery.of(context).size.width * 0.3,
color: ColorsManager.grey25,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: ListView(
children: [
/// Header
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),
/// Device Name + Icon
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: TextFormField(
maxLength: 30,
style: const TextStyle(
color: ColorsManager.blackColor,
),
textAlign: TextAlign.center,
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),
_bloc.editName == true
? const SizedBox()
: GestureDetector(
onTap: () {
_bloc.add(const ChangeNameEvent(value: true));
},
child: SvgPicture.asset(
Assets.editNameIconSettings,
color: ColorsManager.grayColor,
height: 20,
width: 20,
),
),
],
),
),
const SizedBox(height: 32),
/// Device Management
Text('Device Management', style: sectionTitle),
DefaultContainer(
padding: EdgeInsets.zero,
child: Column(
children: [
const SizedBox(
height: 5,
),
Padding(
padding: const EdgeInsets.all(10.0),
child: infoRow(
label: 'Sub-Space:',
value: device.subspace!.subspaceName,
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,
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:',
value: deviceInfo.macAddress),
),
const SizedBox(
height: 5,
),
],
),
),
const SizedBox(height: 32),
/// Remove Device Button
SizedBox(
width: double.infinity,
child: InkWell(
onTap: () {
_bloc.add(DeleteDeviceEvent());
},
child: const DefaultContainer(
padding: EdgeInsets.all(25),
child: Center(
child: Text(
'Remove Device',
style: TextStyle(color: ColorsManager.red),
),
),
),
),
)
],
),
);
}));
}
}
class DeviceTypeHelper {
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;
}
}