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;
}

View File

@@ -0,0 +1,46 @@
class Group {
String? id;
String? type;
String? name;
String? ownerId;
String? status;
String? visibility;
DateTime? createdAt;
DateTime? updatedAt;
bool? isOwner;
String? description;
Group({
required this.id,
this.type,
this.name,
this.ownerId,
this.status,
this.visibility,
this.createdAt,
this.updatedAt,
this.isOwner,
this.description,
});
Group.fromJson(Map<String, dynamic> json) {
id = json["id"];
type = json["type"];
name = json["name"];
ownerId = json["owner_id"];
status = json["status"];
visibility = json["visibility"];
createdAt = DateTime.parse(json["created_at"]);
updatedAt = DateTime.parse(json["updated_at"]);
isOwner = json["is_owner"];
description = json["description"];
}
static List<Group> fromJsonList(List list) {
return list.map((e) => Group.fromJson(e)).toList();
}
static List<Group> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => Group.fromJson(e)).toList();
}
}

View File

@@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../product/constant/enums/app_route_enums.dart';
import 'groups_model.dart';
import '../inter_family_bloc.dart';
import '../inter_family_widget.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/language_services.dart';
import 'groups_widget.dart';
class GroupsScreen extends StatefulWidget {
const GroupsScreen({super.key, required this.role});
final String role;
@override
State<GroupsScreen> createState() => _GroupsScreenState();
}
class _GroupsScreenState extends State<GroupsScreen> {
late InterFamilyBloc interFamilyBloc;
@override
void initState() {
super.initState();
interFamilyBloc = BlocProvider.of(context);
// interFamilyBloc.getAllGroup(widget.role);
}
@override
Widget build(BuildContext context) {
if (widget.role == ApplicationConstants.OWNER_GROUP ||
widget.role == ApplicationConstants.PARTICIPANT_GROUP) {
interFamilyBloc.getAllGroup(widget.role);
return StreamBuilder<List<Group>>(
stream: interFamilyBloc.streamCurrentGroups,
builder: (context, groupsSnapshot) {
return Scaffold(
body: groupsSnapshot.data?.isEmpty ?? true
? const Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: groupsSnapshot.data!.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
context.pushNamed(AppRoutes.GROUP_DETAIL.name,
pathParameters: {
"groupId": groupsSnapshot.data![index].id!
},
extra: widget.role);
},
leading: IconConstants.instance
.getMaterialIcon(Icons.diversity_2),
title: Text(
groupsSnapshot.data![index].name ?? '',
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
groupsSnapshot.data![index].description ?? ""),
trailing:
widget.role == ApplicationConstants.OWNER_GROUP
? PopupMenuButton(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8.0),
bottomRight: Radius.circular(8.0),
topLeft: Radius.circular(8.0),
topRight: Radius.circular(8.0),
),
),
itemBuilder: (ctx) => [
_buildPopupMenuItem(
groupsSnapshot.data![index],
context,
appLocalization(context)
.share_group_title,
Icons.share,
4),
_buildPopupMenuItem(
groupsSnapshot.data![index],
context,
appLocalization(context)
.change_group_infomation_title,
Icons.settings_backup_restore,
2),
_buildPopupMenuItem(
groupsSnapshot.data![index],
context,
appLocalization(context)
.delete_group_title,
Icons.delete_forever_rounded,
3),
],
icon: const Icon(Icons.more_horiz),
)
: const SizedBox.shrink(),
);
},
),
);
});
} else {
return const SizedBox.shrink();
}
}
PopupMenuItem _buildPopupMenuItem(Group group, BuildContext context,
String title, IconData iconData, int position) {
return PopupMenuItem(
onTap: () {
if (title == appLocalization(context).share_group_title) {
Future.delayed(context.lowDuration, () {
shareGroup(context, group);
});
} else if (title ==
appLocalization(context).change_group_infomation_title) {
Future.delayed(context.lowDuration, () {
createOrJoinGroupDialog(
context,
interFamilyBloc,
widget.role,
appLocalization(context).change_group_infomation_content,
appLocalization(context).group_name_title,
group.name!,
false,
group.id!,
appLocalization(context).description_group,
group.description ?? "");
});
} else if (title == appLocalization(context).delete_group_title) {
Future.delayed(context.lowDuration, () {
showActionDialog(
context,
widget.role,
interFamilyBloc,
appLocalization(context).delete_group_title,
appLocalization(context).delete_group_content,
group);
});
} else {}
},
value: position,
child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
iconData,
color: Colors.black,
),
const SizedBox(width: 10),
Text(title),
],
),
);
}
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../inter_family_bloc.dart';
import '../../../product/constant/icon/icon_constants.dart';
import '../../../product/extention/context_extention.dart';
import '../../../product/services/language_services.dart';
import 'groups_model.dart';
shareGroup(BuildContext context, Group group) {
showGeneralDialog(
barrierColor: Colors.black.withOpacity(0.5),
transitionBuilder: (context, a1, a2, widget) {
return Material(
child: Scaffold(
appBar: AppBar(
title:
Center(child: Text(appLocalization(context).share_group_title)),
leading: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: IconConstants.instance.getMaterialIcon(Icons.arrow_back),
),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: QrImageView(
data: '${group.id}.${group.name!}',
size: 250,
),
),
const SizedBox(height: 10),
Text(group.name!),
const SizedBox(height: 10),
OutlinedButton(
onPressed: () {},
child: const Text('Capture and Save as Image'),
),
],
),
),
);
},
transitionDuration: context.lowDuration,
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {
return const SizedBox();
},
);
}
showActionDialog(
BuildContext context,
String role,
InterFamilyBloc interFamilyBloc,
String dialogTitle,
String dialogContent,
Group group) {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Center(child: Text(dialogTitle)),
content: Text(dialogContent),
actions: [
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: Text(appLocalization(context).cancel_button_content),
),
TextButton(
onPressed: () async {
if (dialogTitle == appLocalization(context).delete_group_title) {
Navigator.of(dialogContext).pop();
await interFamilyBloc.deleteGroup(context, group.id!);
interFamilyBloc.getAllGroup(role);
} else {}
},
child: Text(
appLocalization(context).confirm_button_content,
style: const TextStyle(color: Colors.red),
),
),
],
);
},
);
}

View File

@@ -0,0 +1,119 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import '../../product/constant/app/app_constants.dart';
import '../../product/services/api_services.dart';
import '../../product/base/bloc/base_bloc.dart';
import '../../product/services/language_services.dart';
import '../../product/utils/response_status_utils.dart';
import 'groups/groups_model.dart';
class InterFamilyBloc extends BlocBase {
APIServices apiServices = APIServices();
final isLoading = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsLoading => isLoading.sink;
Stream<bool> get streamIsLoading => isLoading.stream;
final titleScreen = StreamController<String>.broadcast();
StreamSink<String> get sinkTitleScreen => titleScreen.sink;
Stream<String> get streamTitleScreen => titleScreen.stream;
final selectedScreen = StreamController<int>.broadcast();
StreamSink<int> get sinkSelectedScreen => selectedScreen.sink;
Stream<int> get streamSelectedScreen => selectedScreen.stream;
final currentGroups = StreamController<List<Group>>.broadcast();
StreamSink<List<Group>> get sinkCurrentGroups => currentGroups.sink;
Stream<List<Group>> get streamCurrentGroups => currentGroups.stream;
@override
void dispose() {}
void getAllGroup(String role) async {
List<Group> groups = [];
sinkCurrentGroups.add(groups);
final body = await apiServices.getAllGroups();
if (body.isNotEmpty) {
final data = jsonDecode(body);
List<dynamic> items = data["items"];
groups = Group.fromJsonDynamicList(items);
groups = sortGroupByName(groups);
List<Group> currentGroups = groups.where(
(group) {
bool isPublic = group.visibility == "PUBLIC";
if (role == ApplicationConstants.OWNER_GROUP) {
return group.isOwner == true && isPublic;
}
if (role == ApplicationConstants.PARTICIPANT_GROUP) {
return group.isOwner == null && isPublic;
}
return false;
},
).toList();
sinkCurrentGroups.add(currentGroups);
} else {
log("Get groups from API failed");
}
log("Inter Family Role: $role");
}
Future<void> createGroup(
BuildContext context, String name, String description) async {
APIServices apiServices = APIServices();
Map<String, dynamic> body = {"name": name, "description": description};
int? statusCode = await apiServices.createGroup(body);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_add_group_success,
appLocalization(context).notification_add_group_failed);
}
Future<void> changeGroupInfomation(BuildContext context, String groupID,
String name, String description) async {
Map<String, dynamic> body = {"name": name, "description": description};
int statusCode = await apiServices.updateGroup(body, groupID);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_update_group_success,
appLocalization(context).notification_update_group_failed);
}
Future<void> joinGroup(BuildContext context, String groupID) async {
Map<String, dynamic> body = {
"group_id": groupID,
};
int statusCode = await apiServices.joinGroup(groupID, body);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_join_request_group_success,
appLocalization(context).notification_join_request_group_failed);
}
Future<void> deleteGroup(BuildContext context, String groupID) async {
int statusCode = await apiServices.deleteGroup(groupID);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_delete_group_success,
appLocalization(context).notification_delete_group_failed);
}
List<Group> sortGroupByName(List<Group> groups) {
return groups..sort((a, b) => (a.name ?? '').compareTo(b.name ?? ''));
}
}

View File

@@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import 'groups/groups_screen.dart';
import 'inter_family_bloc.dart';
import 'inter_family_widget.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/language_services.dart';
class InterFamilyScreen extends StatefulWidget {
const InterFamilyScreen({super.key});
@override
State<InterFamilyScreen> createState() => _InterFamilyScreenState();
}
class _InterFamilyScreenState extends State<InterFamilyScreen> {
late InterFamilyBloc interFamilyBloc;
String title = "";
int _selectedIndex = 0;
bool isLoading = false;
@override
void initState() {
super.initState();
interFamilyBloc = BlocProvider.of(context);
}
final _widgetOptions = <Widget>[
BlocProvider(
blocBuilder: () => InterFamilyBloc(),
child: const GroupsScreen(
role: ApplicationConstants.OWNER_GROUP,
),
),
BlocProvider(
blocBuilder: () => InterFamilyBloc(),
child: const GroupsScreen(
role: ApplicationConstants.PARTICIPANT_GROUP,
),
),
];
@override
Widget build(BuildContext context) {
checkTitle(_selectedIndex);
return StreamBuilder<int>(
stream: interFamilyBloc.streamSelectedScreen,
initialData: _selectedIndex,
builder: (context, selectSnapshot) {
return Scaffold(
appBar: AppBar(
actions: [
ElevatedButton(
onPressed: () {
if (selectSnapshot.data == 0) {
createOrJoinGroupDialog(
context,
interFamilyBloc,
selectSnapshot.data! == 0
? ApplicationConstants.OWNER_GROUP
: ApplicationConstants.PARTICIPANT_GROUP,
appLocalization(context).add_new_group,
appLocalization(context).group_name_title,
"",
false,
"",
"",
"");
} else {
createOrJoinGroupDialog(
context,
interFamilyBloc,
selectSnapshot.data! == 0
? ApplicationConstants.OWNER_GROUP
: ApplicationConstants.PARTICIPANT_GROUP,
appLocalization(context).join_group,
appLocalization(context).group_id_title,
'',
true,
"",
appLocalization(context).group_name_title,
"");
}
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
child: IconConstants.instance.getMaterialIcon(Icons.add),
),
],
leading: Builder(
builder: (context) {
return IconButton(
onPressed: () {
Scaffold.of(context).openDrawer();
},
icon: const Icon(Icons.menu),
);
},
),
title: StreamBuilder<String>(
stream: interFamilyBloc.streamTitleScreen,
initialData: title,
builder: (context, titleSnapshot) {
return Center(
child: Text(titleSnapshot.data ?? title),
);
},
),
),
drawer: Drawer(
width: context.dynamicWidth(0.4),
child: ListView(
padding: EdgeInsets.zero,
children: [
ListTile(
title: Text(appLocalization(context).my_group_title),
selected: _selectedIndex == 0,
onTap: () {
_onItemTapped(0);
title = appLocalization(context).my_group_title;
interFamilyBloc.sinkTitleScreen.add(title);
Navigator.pop(context);
},
),
ListTile(
title: Text(appLocalization(context).invite_group),
selected: _selectedIndex == 1,
onTap: () {
_onItemTapped(1);
title = appLocalization(context).invite_group;
interFamilyBloc.sinkTitleScreen.add(title);
Navigator.pop(context);
},
),
],
),
),
body: _widgetOptions[selectSnapshot.data ?? _selectedIndex],
);
},
);
}
void checkTitle(int index) {
if (index == 0) {
title = appLocalization(context).my_group_title;
interFamilyBloc.sinkTitleScreen.add(title);
} else {
title = appLocalization(context).invite_group;
interFamilyBloc.sinkTitleScreen.add(title);
}
}
void _onItemTapped(int index) {
_selectedIndex = index;
interFamilyBloc.sinkSelectedScreen.add(_selectedIndex);
isLoading = false;
interFamilyBloc.sinkIsLoading.add(isLoading);
}
}

View File

@@ -0,0 +1,152 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'inter_family_bloc.dart';
import '../../product/constant/icon/icon_constants.dart';
import '../../product/services/language_services.dart';
import '../../product/utils/qr_utils.dart';
createOrJoinGroupDialog(
BuildContext context,
InterFamilyBloc interFamilyBloc,
String role,
String titleDialogName,
String titleTextField1,
String textFieldContent1,
bool icon,
String groupID,
String titleTextField2,
String textFieldContent2) {
TextEditingController editingController = TextEditingController();
TextEditingController readOnlycontroller = TextEditingController();
TextEditingController descriptionController = TextEditingController();
if (textFieldContent1 != "") {
editingController.text = textFieldContent1;
}
if (textFieldContent2 != "") {
descriptionController.text = textFieldContent2;
}
// final byte = b
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Center(child: Text(titleDialogName)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
icon
? Column(
children: [
TextField(
controller: editingController,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () async {
String data = await QRScanUtils.instance
.scanQR(dialogContext);
Map<String, dynamic> items =
QRScanUtils.instance.getQRData(data);
String groupID = items['group_id'];
String groupName = items['group_name'];
editingController.text = groupID;
readOnlycontroller.text = groupName;
},
icon: IconConstants.instance
.getMaterialIcon(Icons.qr_code_outlined)),
label: Text(titleTextField1),
border: const OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(16.0))),
),
),
const SizedBox(height: 10),
TextField(
readOnly: true,
controller: readOnlycontroller,
decoration: InputDecoration(
label: Text(titleTextField2),
border: const OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(16.0)))),
),
],
)
: Column(
children: [
TextField(
textInputAction: TextInputAction.next,
controller: editingController,
decoration: InputDecoration(
label: Text(titleTextField1),
border: const OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(16.0)))),
),
const SizedBox(height: 10),
TextField(
onChanged: (value) {},
maxLines: 3,
textInputAction: TextInputAction.done,
controller: descriptionController,
decoration: InputDecoration(
labelText:
appLocalization(context).description_group,
border: const OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(16.0)))),
),
],
),
],
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: Text(
appLocalization(context).cancel_button_content,
style: const TextStyle(color: Colors.red),
),
),
TextButton(
onPressed: () async {
if (!icon) {
String groupName = editingController.text;
String description = descriptionController.text;
if (titleDialogName ==
appLocalization(context).add_new_group) {
try {
await interFamilyBloc.createGroup(
context, groupName, description);
interFamilyBloc.getAllGroup(role);
Navigator.of(dialogContext).pop();
} catch (e) {
// log("Lỗi khi tạo nhóm: $e");
}
} else if (titleDialogName ==
appLocalization(context)
.change_group_infomation_content) {
try {
await interFamilyBloc.changeGroupInfomation(
context, groupID, groupName, description);
interFamilyBloc.getAllGroup(role);
Navigator.of(dialogContext).pop();
} catch (e) {
// log("Lỗi khi sửa nhóm: $e");
}
}
} else {
String groupId = editingController.text;
await interFamilyBloc.joinGroup(context, groupId);
Navigator.of(dialogContext).pop();
}
},
child: Text(appLocalization(context).confirm_button_content))
],
);
});
}