Complete refactoring SFM App Source Code

This commit is contained in:
anhtunz
2024-12-15 00:59:02 +07:00
parent caa73ca43c
commit 2e27d59278
247 changed files with 18390 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
// ignore_for_file: use_build_context_synchronously
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'group_detail_bloc.dart';
import '../../../product/constant/icon/icon_constants.dart';
import '../../../product/services/language_services.dart';
import '../../devices/device_model.dart';
import 'group_detail_model.dart';
addDeviceDialog(BuildContext context, DetailGroupBloc detailGroupBloc,
String groupID, List<DeviceOfGroup> devices) async {
List<Device> ownerDevices = await detailGroupBloc.getOwnerDevices();
List<String> selectedItems = [];
List<String> selectedDevices = [];
if (devices.isNotEmpty) {
ownerDevices.removeWhere((element) =>
devices.any((device) => device.thingId == element.thingId));
}
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Center(child: Text(appLocalization(context).add_device_title)),
content: DropdownButtonFormField2<String>(
isExpanded: true,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
),
),
hint: Text(
appLocalization(context).choose_device_dropdownButton,
style: const TextStyle(fontSize: 14),
),
items: ownerDevices
.map((item) => DropdownMenuItem<String>(
value: item.thingId,
child: StatefulBuilder(builder: (context, menuSetState) {
final isSelected = selectedItems.contains(item.thingId);
final isNameSelected = selectedItems.contains(item.name);
return InkWell(
onTap: () {
isSelected
? selectedItems.remove(item.thingId)
: selectedItems.add(item.thingId!);
isNameSelected
? selectedDevices.remove(item.name)
: selectedDevices.add(item.name!);
menuSetState(() {});
},
child: SizedBox(
height: double.infinity,
child: Row(
children: [
if (isSelected)
const Icon(Icons.check_box_outlined)
else
const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 10),
Expanded(
child: Text(
item.name!,
style: const TextStyle(fontSize: 14),
))
],
),
),
);
})))
.toList(),
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.only(right: 8),
),
onChanged: (value) {
// thingID = value!;
// setState(() {});
},
onSaved: (value) {},
selectedItemBuilder: (context) {
return selectedDevices.map(
(item) {
return Container(
alignment: AlignmentDirectional.center,
child: Text(
item,
style: const TextStyle(
fontSize: 14,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
);
},
).toList();
},
iconStyleData: IconStyleData(
icon: IconConstants.instance
.getMaterialIcon(Icons.arrow_drop_down)),
dropdownStyleData: DropdownStyleData(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(horizontal: 16),
),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: Text(
appLocalization(context).cancel_button_content,
style: const TextStyle(color: Colors.red),
),
),
TextButton(
onPressed: () async {
for (var device in selectedItems) {
await detailGroupBloc.addDeviceToGroup(
context, groupID, device);
await detailGroupBloc.getGroupDetail(groupID);
}
},
child: Text(appLocalization(context).add_device_title))
],
);
});
}

View File

@@ -0,0 +1,118 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'dart:convert';
import 'package:flutter/widgets.dart';
import '../../devices/device_model.dart';
import '../../../product/base/bloc/base_bloc.dart';
import '../../../product/services/api_services.dart';
import '../../../product/services/language_services.dart';
import '../../../product/utils/response_status_utils.dart';
import 'group_detail_model.dart';
class DetailGroupBloc extends BlocBase {
APIServices apiServices = APIServices();
final detailGroup = StreamController<GroupDetail>.broadcast();
StreamSink<GroupDetail> get sinkDetailGroup => detailGroup.sink;
Stream<GroupDetail> get streamDetailGroup => detailGroup.stream;
final warningDevice = StreamController<List<DeviceOfGroup>>.broadcast();
StreamSink<List<DeviceOfGroup>> get sinkWarningDevice => warningDevice.sink;
Stream<List<DeviceOfGroup>> get streamWarningDevice => warningDevice.stream;
final message = StreamController<String>.broadcast();
StreamSink<String> get sinkMessage => message.sink;
Stream<String> get streamMessage => message.stream;
@override
void dispose() {}
Future<void> getGroupDetail(String groupID) async {
final body = await apiServices.getGroupDetail(groupID);
final data = jsonDecode(body);
List<DeviceOfGroup> warningDevices = [];
if (data != null) {
GroupDetail group = GroupDetail.fromJson(data);
sinkDetailGroup.add(group);
if (group.devices != null) {
for (var device in group.devices!) {
if (device.state == 1) {
warningDevices.add(device);
}
}
sinkWarningDevice.add(warningDevices);
}
}
}
Future<void> approveUserToGroup(BuildContext context, String groupID,
String userID, String userName) async {
Map<String, dynamic> body = {"group_id": groupID, "user_id": userID};
int statusCode = await apiServices.approveGroup(body);
showSnackBarResponseByStatusCode(context, statusCode,
"Đã duyệt $userName vào nhóm!", "Duyệt $userName thất bại!");
}
Future<void> deleteOrUnapproveUser(BuildContext context, String groupID,
String userID, String userName) async {
int statusCode = await apiServices.deleteUserInGroup(groupID, userID);
showSnackBarResponseByStatusCode(context, statusCode,
"Đã xóa người dùng $userName", "Xóa người dùng $userName thất bại");
}
Future<void> deleteDevice(BuildContext context, String groupID,
String thingID, String deviceName) async {
int statusCode = await apiServices.deleteDeviceInGroup(groupID, thingID);
showSnackBarResponseByStatusCode(context, statusCode,
"Đã xóa thiết bị $deviceName", "Xóa thiết bị $deviceName thất bại");
}
Future<void> leaveGroup(
BuildContext context, String groupID, String userID) async {
int statusCode = await apiServices.deleteUserInGroup(groupID, userID);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_leave_group_success,
appLocalization(context).notification_leave_group_failed);
}
Future<void> updateDeviceNameInGroup(
BuildContext context, String thingID, String newAlias) async {
Map<String, dynamic> body = {"thing_id": thingID, "alias": newAlias};
int statusCode = await apiServices.updateDeviceAlias(body);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_update_device_success,
appLocalization(context).notification_update_device_failed,
);
}
Future<List<Device>> getOwnerDevices() async {
List<Device> allDevices = [];
String body = await apiServices.getOwnerDevices();
if (body != "") {
final data = jsonDecode(body);
List<dynamic> items = data['items'];
allDevices = Device.fromJsonDynamicList(items);
}
return allDevices;
}
Future<void> addDeviceToGroup(
BuildContext context, String groupID, String thingID) async {
Map<String, dynamic> body = {
"thing_id": thingID,
};
int statusCode = await apiServices.addDeviceToGroup(groupID, body);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_add_device_success,
appLocalization(context).notification_add_device_failed,
);
}
}

View File

@@ -0,0 +1,133 @@
class GroupDetail {
String? id;
String? type;
String? name;
String? description;
String? ownerId;
String? status;
String? visibility;
DateTime? createdAt;
DateTime? updatedAt;
List<GroupUser>? users;
List<DeviceOfGroup>? devices;
GroupDetail({
required this.id,
this.type,
this.name,
this.description,
this.ownerId,
this.status,
this.visibility,
this.createdAt,
this.updatedAt,
this.users,
this.devices,
});
GroupDetail.fromJson(Map<String, dynamic> json) {
id = json["id"];
type = json["type"];
name = json["name"];
description = json["description"];
ownerId = json["ownerId"];
status = json["status"];
visibility = json["visibility"];
createdAt = DateTime.parse(json["created_at"]);
updatedAt = DateTime.parse(json['updated_at']);
if (json['users'] != null) {
users = [];
json["users"].forEach((v) {
final user = GroupUser.fromJson(v);
users!.add(user);
});
}
if (json['devices'] != null) {
devices = [];
json["devices"].forEach((v) {
final device = DeviceOfGroup.fromJson(v);
devices!.add(device);
});
}
}
// static List<GroupDetail> fromJsonDynamicList(List<dynamic> list) {
// return list.map((e) => GroupDetail.fromJson(e)).toList();
// }
}
class GroupUser {
String? id;
String? username;
String? role;
String? name;
String? email;
String? phone;
bool? isOwner;
String? status;
String? address;
String? latitude;
String? longitude;
GroupUser({
required this.id,
this.username,
this.role,
this.name,
this.email,
this.phone,
this.isOwner,
this.status,
this.address,
this.latitude,
this.longitude,
});
GroupUser.fromJson(Map<String, dynamic> json) {
id = json["id"];
username = json["username"];
role = json["role"];
name = json["name"];
email = json['email'];
phone = json['phone'];
isOwner = json['is_owner'];
status = json['status'];
address = json['address'];
latitude = json['latitude'];
longitude = json['longitude'];
}
static List<GroupUser> fromJsonList(List list) {
return list.map((e) => GroupUser.fromJson(e)).toList();
}
static List<GroupUser> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => GroupUser.fromJson(e)).toList();
}
}
class DeviceOfGroup {
String? thingId;
String? name;
String? alias;
DateTime? connectionTime;
int? state;
String? visibility;
DeviceOfGroup({
required this.thingId,
this.name,
this.alias,
this.connectionTime,
this.state,
this.visibility,
});
DeviceOfGroup.fromJson(Map<String, dynamic> json) {
thingId = json['thing_id'];
name = json['name'];
alias = json['alias'];
connectionTime = DateTime.parse(json['connection_time']);
state = json['state'];
visibility = json['visibility'];
}
}

View File

@@ -0,0 +1,552 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'group_detail_bloc.dart';
import 'group_detail_model.dart';
import '../../../product/base/bloc/base_bloc.dart';
import '../../../product/constant/app/app_constants.dart';
import '../../../product/constant/icon/icon_constants.dart';
import '../../../product/extention/context_extention.dart';
import '../../../product/services/api_services.dart';
import '../../../product/services/language_services.dart';
import '../../../product/utils/device_utils.dart';
import '../../../product/utils/response_status_utils.dart';
import 'add_device_to_group_dialog.dart';
class DetailGroupScreen extends StatefulWidget {
const DetailGroupScreen({super.key, required this.group, required this.role});
final String group;
final String role;
@override
State<DetailGroupScreen> createState() => _DetailGroupScreenState();
}
class _DetailGroupScreenState extends State<DetailGroupScreen> {
late DetailGroupBloc detailGroupBloc;
final scaffoldKey = GlobalKey<ScaffoldState>();
@override
void initState() {
super.initState();
detailGroupBloc = BlocProvider.of(context);
}
@override
Widget build(BuildContext context) {
return StreamBuilder<GroupDetail>(
stream: detailGroupBloc.streamDetailGroup,
builder: (context, detailGroupSnapshot) {
if (detailGroupSnapshot.data?.id == null) {
detailGroupBloc.getGroupDetail(widget.group);
return const Center(
child: CircularProgressIndicator(),
);
} else {
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: Center(
child: Text(
detailGroupSnapshot.data?.name ??
appLocalization(context).loading_message,
),
),
actions: [
IconButton(
onPressed: () {
if (scaffoldKey.currentState != null) {
scaffoldKey.currentState?.openEndDrawer();
} else {
// log("_scaffoldKey.currentState is null");
}
},
icon: const Icon(Icons.info_outline_rounded),
),
],
),
endDrawer: endDrawer(detailGroupSnapshot.data!),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
addDeviceDialog(context, detailGroupBloc, widget.group,
detailGroupSnapshot.data?.devices ?? []);
},
icon: const Icon(Icons.add),
// backgroundColor: const Color.fromARGB(255, 109, 244, 96),
label: Text(appLocalization(context).add_device_title),
),
body: SafeArea(
child: groupBody(
detailGroupSnapshot.data?.devices ?? [],
),
),
);
}
});
}
Widget endDrawer(GroupDetail groupDetail) {
APIServices apiServices = APIServices();
return Drawer(
width: context.dynamicWidth(0.75),
child: ListView(
children: [
Center(
child: Text(groupDetail.name ?? "",
style: Theme.of(context).textTheme.titleLarge)),
Center(
child: Text(groupDetail.description ?? "",
style: Theme.of(context).textTheme.bodyMedium)),
Visibility(
visible: widget.role == "owner",
child: Theme(
data:
Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
title: Text(
"${appLocalization(context).approve_user} (${getNumberInGroup(groupDetail, "PENDING")})",
style: const TextStyle(fontWeight: FontWeight.bold),
),
children: <Widget>[
if (groupDetail.users != null &&
groupDetail.users!.isNotEmpty)
for (var user in groupDetail.users!)
if (user.status == "PENDING")
ListTile(
title: Text(user.name!),
subtitle: Text(user.email!),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 48,
child: IconButton(
onPressed: () async {
await detailGroupBloc.approveUserToGroup(
context,
widget.group,
user.id!,
user.name!);
detailGroupBloc
.getGroupDetail(widget.group);
},
icon: const Icon(
Icons.check,
color: Colors.green,
),
),
),
SizedBox(
width: 48,
child: IconButton(
onPressed: () async {
await detailGroupBloc.deleteOrUnapproveUser(
context,
widget.group,
user.id!,
user.name!);
await detailGroupBloc
.getGroupDetail(widget.group);
},
icon: const Icon(
Icons.close,
color: Colors.red,
),
),
),
],
),
),
],
),
),
),
Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
title: Text(
"${appLocalization(context).member_title} (${getNumberInGroup(groupDetail, "JOINED")})",
style: const TextStyle(fontWeight: FontWeight.bold),
),
children: <Widget>[
if (groupDetail.users != null && groupDetail.users!.isNotEmpty)
for (var user in groupDetail.users!)
if (user.status == "JOINED")
ListTile(
title: Text(user.name ?? 'Error'),
subtitle: Text(user.email ?? 'Error'),
trailing: widget.role == "owner"
? PopupMenuButton(
icon: IconConstants.instance
.getMaterialIcon(Icons.more_horiz),
itemBuilder: (contex) => [
PopupMenuItem(
value: 1,
child: Text(appLocalization(context)
.change_button_content)),
PopupMenuItem(
onTap: () async {
await detailGroupBloc
.deleteOrUnapproveUser(
context,
widget.group,
user.id!,
user.name!);
await detailGroupBloc
.getGroupDetail(widget.group);
},
value: 2,
child: Text(appLocalization(context)
.delete_button_content)),
],
)
: null,
),
],
),
),
Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
title: Text(
"${appLocalization(context).devices_title} (${getNumberInGroup(groupDetail, "DEVICES")})",
style: const TextStyle(fontWeight: FontWeight.bold),
),
children: <Widget>[
if (groupDetail.devices != null &&
groupDetail.devices!.isNotEmpty)
for (var device in groupDetail.devices!)
if (device.visibility == "PUBLIC")
ListTile(
title: Text(
device.alias != "" ? device.alias! : device.name!),
// subtitle: Text(device.thingId ?? 'Error'),
trailing: widget.role ==
ApplicationConstants.OWNER_GROUP
? PopupMenuButton(
icon: IconConstants.instance
.getMaterialIcon(Icons.more_horiz),
itemBuilder: (contex) => [
PopupMenuItem(
onTap: () {
Navigator.pop(context);
showChangeAlias(device);
},
value: 1,
child: Text(appLocalization(context)
.change_button_content)),
PopupMenuItem(
onTap: () async {
await detailGroupBloc.deleteDevice(
context,
widget.group,
device.thingId!,
device.name!,
);
},
value: 2,
child: Text(
appLocalization(context)
.delete_button_content,
),
),
],
)
: PopupMenuButton(
icon: IconConstants.instance.getMaterialIcon(
Icons.more_horiz,
),
itemBuilder: ((itemContext) => [
PopupMenuItem(
onTap: () async {
Navigator.pop(itemContext);
Future.delayed(
context.normalDuration,
() => showChangeAlias(device),
);
},
value: 1,
child: Text(
appLocalization(context)
.change_button_content,
),
),
]),
),
),
],
),
),
SizedBox(height: context.dynamicHeight(0.3)),
if (widget.role == "owner")
TextButton(
onPressed: () {
Future.delayed(context.normalDuration)
.then((value) => Navigator.pop(context))
.then(
(value) => {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Center(
child: Text(appLocalization(context)
.delete_group_title)),
content: Text(
appLocalization(context)
.delete_device_dialog_content,
),
actions: [
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: Text(
appLocalization(context)
.cancel_button_content,
),
),
TextButton(
onPressed: () async {
Navigator.of(dialogContext).pop();
Future.delayed(context.lowDuration).then(
(value) => Navigator.pop(context),
);
int statusCode = await apiServices
.deleteGroup(widget.group);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context)
.notification_delete_group_success,
appLocalization(context)
.notification_delete_group_failed);
},
child: Text(
appLocalization(context)
.confirm_button_content,
style: const TextStyle(color: Colors.red),
),
),
],
);
},
),
},
);
},
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.red),
foregroundColor: MaterialStatePropertyAll(Colors.white),
),
child: Text(
appLocalization(context).delete_group_title,
),
)
else
TextButton(
onPressed: () async {
String uid = await apiServices.getUID();
Future.delayed(context.lowDuration)
.then((value) => Navigator.pop(context))
.then((value) => {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Center(
child: Text(
appLocalization(context)
.leave_group_title,
),
),
content: Text(appLocalization(context)
.leave_group_content),
actions: [
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: Text(
appLocalization(context)
.cancel_button_content,
),
),
TextButton(
onPressed: () async {
Navigator.of(dialogContext).pop();
Future.delayed(context.normalDuration)
.then((value) =>
Navigator.pop(context));
await detailGroupBloc.leaveGroup(
context, widget.group, uid);
},
child: Text(
appLocalization(context)
.confirm_button_content,
style: const TextStyle(color: Colors.red),
))
],
);
})
});
},
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.red),
foregroundColor: MaterialStatePropertyAll(Colors.white),
),
child: Text(
appLocalization(context).leave_group_title,
),
),
],
),
);
}
Widget groupBody(List<DeviceOfGroup> devices) {
devices = softDeviceByState(devices);
List<String> deviceState = [];
if (devices.isNotEmpty) {
for (var device in devices) {
String state =
DeviceUtils.instance.checkStateDevice(context, device.state!);
deviceState.add(state);
}
}
return Expanded(
child: Center(
child: devices.isEmpty
? Center(
child: Text(appLocalization(context).dont_have_device),
)
: StreamBuilder<List<DeviceOfGroup>>(
stream: detailGroupBloc.streamWarningDevice,
builder: (context, warningDeviceSnapshot) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${appLocalization(context).warning_message} ${warningDeviceSnapshot.data?.length ?? '0'}",
style: const TextStyle(
color: Colors.red,
),
),
Expanded(
child: ListView.builder(
itemCount: devices.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(devices[index].alias != ""
? devices[index].alias!
: devices[index].name!,),
trailing: Text(
DeviceUtils.instance.checkStateDevice(
context, devices[index].state!),
style: TextStyle(
color: DeviceUtils.instance
.getTableRowColor(
devices[index].state!,),),
),
);
},
),
),
],
);
},),
),
);
}
showChangeAlias(DeviceOfGroup device) async {
TextEditingController aliasController =
TextEditingController(text: device.alias);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.white,
dismissDirection: DismissDirection.none,
duration: const Duration(minutes: 1),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${appLocalization(context).map_result}: ',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black)),
Container(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: () async {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
icon: const Icon(Icons.close)),
)
],
),
TextField(
controller: aliasController,
decoration: InputDecoration(
label:
Text(appLocalization(context).input_name_device_device),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16.0)))),
),
const SizedBox(height: 5),
Center(
child: TextButton(
style: const ButtonStyle(
foregroundColor: MaterialStatePropertyAll(Colors.white),
backgroundColor: MaterialStatePropertyAll(Colors.green)),
onPressed: () async {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
String alias = aliasController.text;
await detailGroupBloc.updateDeviceNameInGroup(
context, device.thingId!, alias);
await detailGroupBloc.getGroupDetail(widget.group);
},
child: Text(appLocalization(context).confirm_button_content)),
)
],
),
),
);
}
}
List<DeviceOfGroup> softDeviceByState(List<DeviceOfGroup> devices) {
List<DeviceOfGroup> sortedDevices = List.from(devices);
sortedDevices.sort((a, b) {
int stateOrder = [2, 1, 3, 0, -1, 3].indexOf(a.state!) -
[2, 1, 3, 0, -1, 3].indexOf(b.state!);
return stateOrder;
});
return sortedDevices;
}
int getNumberInGroup(GroupDetail group, String name) {
int number = 0;
if (group.id != "") {
if (name == "PENDING" || name == "JOINED") {
for (var user in group.users ?? []) {
if (user.status == name) {
number++;
}
}
} else {
for (var device in group.devices ?? []) {
if (device.visibility == "PUBLIC") {
number++;
}
}
}
}
return number;
}