@ -1,7 +1,5 @@
|
||||
# syncrow_web
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
@ -42,39 +42,36 @@ class DefaultButton extends StatelessWidget {
|
||||
? null
|
||||
: customButtonStyle ??
|
||||
ButtonStyle(
|
||||
textStyle: MaterialStateProperty.all(
|
||||
textStyle: WidgetStateProperty.all(
|
||||
customTextStyle ??
|
||||
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 13,
|
||||
color: foregroundColor,
|
||||
fontWeight: FontWeight.normal),
|
||||
fontSize: 13, color: foregroundColor, fontWeight: FontWeight.normal),
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
foregroundColor: WidgetStateProperty.all(
|
||||
isSecondary
|
||||
? Colors.black
|
||||
: enabled
|
||||
? foregroundColor ?? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
backgroundColor: WidgetStateProperty.resolveWith<Color>((Set<WidgetState> states) {
|
||||
return enabled
|
||||
? backgroundColor ?? ColorsManager.primaryColor
|
||||
: Colors.black.withOpacity(0.2);
|
||||
}),
|
||||
shape: MaterialStateProperty.all(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
side: BorderSide(color: borderColor ?? Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 20),
|
||||
),
|
||||
),
|
||||
fixedSize: MaterialStateProperty.all(
|
||||
fixedSize: WidgetStateProperty.all(
|
||||
const Size.fromHeight(50),
|
||||
),
|
||||
padding: MaterialStateProperty.all(
|
||||
padding: WidgetStateProperty.all(
|
||||
EdgeInsets.all(padding ?? 10),
|
||||
),
|
||||
minimumSize: MaterialStateProperty.all(
|
||||
minimumSize: WidgetStateProperty.all(
|
||||
const Size.fromHeight(50),
|
||||
),
|
||||
),
|
||||
|
@ -17,6 +17,7 @@ class DynamicTable extends StatefulWidget {
|
||||
final void Function(int, bool, dynamic)? onRowSelected;
|
||||
final List<String>? initialSelectedIds;
|
||||
final int uuidIndex;
|
||||
final Function(dynamic selectedRows)? onSelectionChanged;
|
||||
const DynamicTable({
|
||||
super.key,
|
||||
required this.headers,
|
||||
@ -32,6 +33,7 @@ class DynamicTable extends StatefulWidget {
|
||||
this.onRowSelected,
|
||||
this.initialSelectedIds,
|
||||
required this.uuidIndex,
|
||||
this.onSelectionChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -39,7 +41,7 @@ class DynamicTable extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DynamicTableState extends State<DynamicTable> {
|
||||
late List<bool> _selected;
|
||||
late List<bool> _selectedRows;
|
||||
bool _selectAll = false;
|
||||
|
||||
@override
|
||||
@ -51,47 +53,30 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
@override
|
||||
void didUpdateWidget(DynamicTable oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.data != widget.data) {
|
||||
if (oldWidget.data.length != widget.data.length) {
|
||||
_initializeSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeSelection() {
|
||||
if (widget.data.isEmpty) {
|
||||
_selected = [];
|
||||
_selectAll = false;
|
||||
} else {
|
||||
_selected = List<bool>.generate(widget.data.length, (index) {
|
||||
// Check if the initialSelectedIds contains the deviceUuid
|
||||
// uuidIndex is the index of the column containing the deviceUuid
|
||||
final deviceUuid = widget.data[index][widget.uuidIndex];
|
||||
return widget.initialSelectedIds != null &&
|
||||
widget.initialSelectedIds!.contains(deviceUuid);
|
||||
});
|
||||
_selectAll = _selected.every((element) => element == true);
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleRowSelection(int index) {
|
||||
setState(() {
|
||||
_selected[index] = !_selected[index];
|
||||
|
||||
if (widget.onRowSelected != null) {
|
||||
widget.onRowSelected!(index, _selected[index], widget.data[index]);
|
||||
}
|
||||
});
|
||||
_selectedRows = List<bool>.filled(widget.data.length, false);
|
||||
_selectAll = false;
|
||||
}
|
||||
|
||||
void _toggleSelectAll(bool? value) {
|
||||
setState(() {
|
||||
_selectAll = value ?? false;
|
||||
_selected = List<bool>.filled(widget.data.length, _selectAll);
|
||||
for (int i = 0; i < widget.data.length; i++) {
|
||||
if (widget.onRowSelected != null) {
|
||||
widget.onRowSelected!(i, _selectAll, widget.data[i]);
|
||||
}
|
||||
}
|
||||
_selectedRows = List<bool>.filled(widget.data.length, _selectAll);
|
||||
});
|
||||
widget.onSelectionChanged?.call(_selectedRows);
|
||||
}
|
||||
|
||||
void _toggleRowSelection(int index) {
|
||||
setState(() {
|
||||
_selectedRows[index] = !_selectedRows[index];
|
||||
_selectAll = _selectedRows.every((isSelected) => isSelected);
|
||||
});
|
||||
widget.onSelectionChanged?.call(_selectedRows);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -211,7 +196,7 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Center(
|
||||
child: Checkbox(
|
||||
value: _selected[index],
|
||||
value: _selectedRows[index],
|
||||
onChanged: (bool? value) {
|
||||
_toggleRowSelection(index);
|
||||
},
|
||||
|
@ -46,6 +46,7 @@ class AcDeviceBatchControlView extends StatelessWidget
|
||||
),
|
||||
children: [
|
||||
ToggleWidget(
|
||||
icon: Assets.ac,
|
||||
deviceId: devicesIds.first,
|
||||
code: 'switch',
|
||||
value: state.status.acSwitch,
|
||||
|
@ -40,6 +40,7 @@ class BatchFanSpeedControl extends StatelessWidget {
|
||||
value == FanSpeeds.low),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
|
@ -43,7 +43,7 @@ class AcToggle extends StatelessWidget {
|
||||
child: Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: SvgPicture.asset(
|
||||
icon ?? Assets.acDevice,
|
||||
icon ?? Assets.lightPulp,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
|
@ -40,6 +40,7 @@ class FanSpeedControl extends StatelessWidget {
|
||||
value == FanSpeeds.low),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
|
@ -25,6 +25,7 @@ class DeviceManagementBloc
|
||||
on<SelectDevice>(_onSelectDevice);
|
||||
on<ResetFilters>(_onResetFilters);
|
||||
on<ResetSelectedDevices>(_onResetSelectedDevices);
|
||||
on<UpdateSelection>(_onUpdateSelection);
|
||||
}
|
||||
|
||||
Future<void> _onFetchDevices(
|
||||
@ -172,6 +173,48 @@ class DeviceManagementBloc
|
||||
}
|
||||
}
|
||||
|
||||
void _onUpdateSelection(
|
||||
UpdateSelection event, Emitter<DeviceManagementState> emit) {
|
||||
List<AllDevicesModel> selectedDevices = [];
|
||||
List<AllDevicesModel> devicesToSelectFrom = [];
|
||||
|
||||
if (state is DeviceManagementLoaded) {
|
||||
devicesToSelectFrom = (state as DeviceManagementLoaded).devices;
|
||||
} else if (state is DeviceManagementFiltered) {
|
||||
devicesToSelectFrom = (state as DeviceManagementFiltered).filteredDevices;
|
||||
}
|
||||
|
||||
for (int i = 0; i < event.selectedRows.length; i++) {
|
||||
if (event.selectedRows[i]) {
|
||||
selectedDevices.add(devicesToSelectFrom[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (state is DeviceManagementLoaded) {
|
||||
final loadedState = state as DeviceManagementLoaded;
|
||||
emit(DeviceManagementLoaded(
|
||||
devices: loadedState.devices,
|
||||
selectedIndex: loadedState.selectedIndex,
|
||||
onlineCount: loadedState.onlineCount,
|
||||
offlineCount: loadedState.offlineCount,
|
||||
lowBatteryCount: loadedState.lowBatteryCount,
|
||||
selectedDevice: selectedDevices,
|
||||
isControlButtonEnabled: _checkIfControlButtonEnabled(selectedDevices),
|
||||
));
|
||||
} else if (state is DeviceManagementFiltered) {
|
||||
final filteredState = state as DeviceManagementFiltered;
|
||||
emit(DeviceManagementFiltered(
|
||||
filteredDevices: filteredState.filteredDevices,
|
||||
selectedIndex: filteredState.selectedIndex,
|
||||
onlineCount: filteredState.onlineCount,
|
||||
offlineCount: filteredState.offlineCount,
|
||||
lowBatteryCount: filteredState.lowBatteryCount,
|
||||
selectedDevice: selectedDevices,
|
||||
isControlButtonEnabled: _checkIfControlButtonEnabled(selectedDevices),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
bool _checkIfControlButtonEnabled(List<AllDevicesModel> selectedDevices) {
|
||||
if (selectedDevices.length > 1) {
|
||||
final productTypes =
|
||||
|
@ -54,3 +54,9 @@ class SelectDevice extends DeviceManagementEvent {
|
||||
class ResetFilters extends DeviceManagementEvent {}
|
||||
|
||||
class ResetSelectedDevices extends DeviceManagementEvent {}
|
||||
|
||||
class UpdateSelection extends DeviceManagementEvent {
|
||||
final List<bool> selectedRows;
|
||||
|
||||
const UpdateSelection(this.selectedRows);
|
||||
}
|
||||
|
@ -37,8 +37,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
offlineCount = state.offlineCount;
|
||||
lowBatteryCount = state.lowBatteryCount;
|
||||
isControlButtonEnabled = state.isControlButtonEnabled;
|
||||
selectedDevices = state.selectedDevice ??
|
||||
context.read<DeviceManagementBloc>().selectedDevices;
|
||||
selectedDevices =
|
||||
state.selectedDevice ?? context.read<DeviceManagementBloc>().selectedDevices;
|
||||
} else if (state is DeviceManagementFiltered) {
|
||||
devicesToShow = state.filteredDevices;
|
||||
selectedIndex = state.selectedIndex;
|
||||
@ -46,8 +46,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
offlineCount = state.offlineCount;
|
||||
lowBatteryCount = state.lowBatteryCount;
|
||||
isControlButtonEnabled = state.isControlButtonEnabled;
|
||||
selectedDevices = state.selectedDevice ??
|
||||
context.read<DeviceManagementBloc>().selectedDevices;
|
||||
selectedDevices =
|
||||
state.selectedDevice ?? context.read<DeviceManagementBloc>().selectedDevices;
|
||||
} else if (state is DeviceManagementInitial) {
|
||||
devicesToShow = [];
|
||||
selectedIndex = 0;
|
||||
@ -61,15 +61,13 @@ 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 Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: isLargeScreenSize(context)
|
||||
? const EdgeInsets.all(30)
|
||||
: const EdgeInsets.all(15),
|
||||
padding:
|
||||
isLargeScreenSize(context) ? const EdgeInsets.all(30) : const EdgeInsets.all(15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -78,9 +76,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
tabs: tabs,
|
||||
selectedIndex: selectedIndex,
|
||||
onTabChanged: (index) {
|
||||
context
|
||||
.read<DeviceManagementBloc>()
|
||||
.add(SelectedFilterChanged(index));
|
||||
context.read<DeviceManagementBloc>().add(SelectedFilterChanged(index));
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@ -88,7 +84,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
height: 45,
|
||||
width: 100,
|
||||
width: 125,
|
||||
decoration: containerDecoration,
|
||||
child: Center(
|
||||
child: DefaultButton(
|
||||
@ -102,14 +98,12 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
),
|
||||
);
|
||||
} else if (selectedDevices.length > 1) {
|
||||
final productTypes = selectedDevices
|
||||
.map((device) => device.productType)
|
||||
.toSet();
|
||||
final productTypes =
|
||||
selectedDevices.map((device) => device.productType).toSet();
|
||||
if (productTypes.length == 1) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
DeviceBatchControlDialog(
|
||||
builder: (context) => DeviceBatchControlDialog(
|
||||
devices: selectedDevices,
|
||||
),
|
||||
);
|
||||
@ -120,11 +114,10 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
borderRadius: 9,
|
||||
child: Text(
|
||||
buttonLabel,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isControlButtonEnabled
|
||||
? Colors.white
|
||||
: Colors.grey,
|
||||
color: isControlButtonEnabled ? Colors.white : Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -143,9 +136,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
cellDecoration: containerDecoration,
|
||||
onRowSelected: (index, isSelected, row) {
|
||||
final selectedDevice = devicesToShow[index];
|
||||
context
|
||||
.read<DeviceManagementBloc>()
|
||||
.add(SelectDevice(selectedDevice));
|
||||
context.read<DeviceManagementBloc>().add(SelectDevice(selectedDevice));
|
||||
},
|
||||
withCheckBox: true,
|
||||
size: context.screenSize,
|
||||
@ -168,16 +159,17 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
device.uuid ?? '',
|
||||
device.unit?.name ?? '',
|
||||
device.room?.name ?? '',
|
||||
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)),
|
||||
];
|
||||
}).toList(),
|
||||
onSelectionChanged: (selectedRows) {
|
||||
context.read<DeviceManagementBloc>().add(UpdateSelection(selectedRows));
|
||||
},
|
||||
initialSelectedIds: context
|
||||
.read<DeviceManagementBloc>()
|
||||
.selectedDevices
|
||||
|
@ -47,12 +47,15 @@ class ToggleWidget extends StatelessWidget {
|
||||
)
|
||||
: ClipOval(
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: 60,
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: ColorsManager.whiteColors,
|
||||
child: SvgPicture.asset(
|
||||
icon ?? Assets.lightPulp,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
width: 35,
|
||||
height: 35,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
)),
|
||||
Text(
|
||||
|
@ -21,8 +21,10 @@ String formatTimeOfDayToISO(TimeOfDay time, {DateTime? currentDate}) {
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
// Convert DateTime to Unix timestamp (in seconds)
|
||||
final unixTimestamp = dateTime.millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
return dateTime.toUtc().toIso8601String();
|
||||
return unixTimestamp.toString();
|
||||
}
|
||||
|
||||
String formatIsoStringToTime(String isoString, BuildContext context) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: syncrow_web
|
||||
description: "A new Flutter project."
|
||||
description: "Smart Home Solutions"
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
BIN
web/favicon.png
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 26 KiB |
@ -18,7 +18,7 @@
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
<meta name="description" content="Smart Home Solutions.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
@ -5,7 +5,7 @@
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"description": "Smart Home Solutions",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
|