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,18 @@
import 'dart:async';
import '../../../../product/base/bloc/base_bloc.dart';
class LoginBloc extends BlocBase{
final loginRequest = StreamController<Map<String,dynamic>>.broadcast();
StreamSink<Map<String,dynamic>> get sinkLoginRequest => loginRequest.sink;
Stream<Map<String,dynamic>> get streamLoginRequest => loginRequest.stream;
final isShowPassword = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsShowPassword => isShowPassword.sink;
Stream<bool> get streamIsShowPassword => isShowPassword.stream;
@override
void dispose() {
}
}

View File

@@ -0,0 +1,17 @@
class LoginModel {
int? exp;
int? iat;
String? id;
String? iss;
String? role;
LoginModel({this.exp, this.iat, this.id, this.iss, this.role});
LoginModel.fromJson(Map<String, dynamic> json) {
exp = json['exp'];
iat = json['iat'];
id = json['id'];
iss = json['iss'];
role = json['role'];
}
}

View File

@@ -0,0 +1,234 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../../product/constant/app/api_path_constant.dart';
import '../../../../product/constant/enums/app_route_enums.dart';
import '../model/login_model.dart';
import '../../../../product/cache/local_manager.dart';
import '../../../../product/constant/enums/local_keys_enums.dart';
import '../../../../product/services/api_services.dart';
import '../../../../product/shared/shared_snack_bar.dart';
import '../../../../product/constant/icon/icon_constants.dart';
import '../../../../product/extention/context_extention.dart';
import '../../../../product/constant/image/image_constants.dart';
import '../../../../product/services/language_services.dart';
import '../bloc/login_bloc.dart';
import '../../../../product/base/bloc/base_bloc.dart';
import '../../../../product/shared/shared_background.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
late LoginBloc loginBloc;
Map<String, dynamic> loginRequest = {"username": "", "password": ""};
final _formKey = GlobalKey<FormState>();
bool isShowPassword = true;
APIServices apiServices = APIServices();
@override
void initState() {
super.initState();
loginBloc = BlocProvider.of(context);
checkLogin(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SharedBackground(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
const Spacer(),
Expanded(
flex: 4,
child: Image.asset(
ImageConstants.instance.getImage("logo"),
height: context.dynamicHeight(0.2),
),
),
const Spacer(),
],
),
SizedBox(
height: context.mediumValue,
),
StreamBuilder<Map<String, dynamic>>(
stream: loginBloc.streamLoginRequest,
builder: (context, loginResquestSnapshot) {
return Padding(
padding: context.paddingLow,
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
textInputAction: TextInputAction.next,
validator: (value) {
if (value == "null" || value!.isEmpty) {
return appLocalization(context)
.login_account_not_empty;
}
return null;
},
onSaved: (username) {
loginRequest["username"] = username!;
loginBloc.sinkLoginRequest.add(loginRequest);
},
decoration: InputDecoration(
hintText:
appLocalization(context).login_account_hint,
prefixIcon: Padding(
padding: context.dynamicPadding(16),
child: IconConstants.instance
.getMaterialIcon(Icons.person),
),
),
),
SizedBox(height: context.lowValue),
StreamBuilder<bool>(
stream: loginBloc.streamIsShowPassword,
builder: (context, isShowPassSnapshot) {
return TextFormField(
textInputAction: TextInputAction.done,
obscureText:
isShowPassSnapshot.data ?? isShowPassword,
validator: (value) {
if (value == null || value.isEmpty) {
return appLocalization(context)
.login_password_not_empty;
}
return null;
},
onSaved: (password) {
loginRequest["password"] = password!;
loginBloc.sinkLoginRequest.add(loginRequest);
},
decoration: InputDecoration(
hintText: appLocalization(context)
.login_password_hint,
prefixIcon: Padding(
padding: context.dynamicPadding(16),
child: IconConstants.instance
.getMaterialIcon(Icons.lock),
),
suffixIcon: IconButton(
onPressed: () {
isShowPassword = !isShowPassword;
loginBloc.sinkIsShowPassword
.add(isShowPassword);
},
icon: isShowPassword
? IconConstants.instance
.getMaterialIcon(
Icons.visibility,
)
: IconConstants.instance
.getMaterialIcon(
Icons.visibility_off,
),
),
),
);
},
),
SizedBox(
height: context.lowValue,
),
ElevatedButton(
style: const ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue),
foregroundColor:
MaterialStatePropertyAll(Colors.white),
),
onPressed: () {
validate();
},
child: Text(
appLocalization(context).login_button_content),
),
],
),
),
);
},
)
],
),
),
),
);
}
void validate() async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
var data =
await apiServices.login(APIPathConstants.LOGIN_PATH, loginRequest);
if (data != "") {
Map<String, dynamic> tokenData = jsonDecode(data);
String token = tokenData['token'];
LocaleManager.instance.setString(PreferencesKeys.TOKEN, token);
String userToken = getBaseToken(token);
var decode = decodeBase64Token(userToken);
LoginModel loginModel =
LoginModel.fromJson(jsonDecode(decode) as Map<String, dynamic>);
LocaleManager.instance.setString(PreferencesKeys.UID, loginModel.id!);
LocaleManager.instance.setInt(PreferencesKeys.EXP, loginModel.exp!);
LocaleManager.instance
.setString(PreferencesKeys.ROLE, loginModel.role!);
context.goNamed(AppRoutes.HOME.name);
} else {
showErrorTopSnackBarCustom(
context, appLocalization(context).login_incorrect_usernameOrPass);
}
}
}
void checkLogin(BuildContext context) async {
// ThemeNotifier themeNotifier = context.watch<ThemeNotifier>();
await LocaleManager.prefrencesInit();
// String theme = LocaleManager.instance.getStringValue(PreferencesKeys.THEME);
String token = LocaleManager.instance.getStringValue(PreferencesKeys.TOKEN);
int exp = LocaleManager.instance.getIntValue(PreferencesKeys.EXP);
log("Token cu: ${LocaleManager.instance.getStringValue(PreferencesKeys.TOKEN)}");
log("UID: ${LocaleManager.instance.getStringValue(PreferencesKeys.UID)}");
log("EXP: ${LocaleManager.instance.getIntValue(PreferencesKeys.EXP)}");
log("Role: ${LocaleManager.instance.getStringValue(PreferencesKeys.ROLE)}");
log("Theme: ${LocaleManager.instance.getStringValue(PreferencesKeys.THEME)}");
log("Lang: ${LocaleManager.instance.getStringValue(PreferencesKeys.LANGUAGE_CODE)}");
// log("Theme: $theme");
// if (theme == AppThemes.DARK.name) {
// themeNotifier.changeValue(AppThemes.DARK);
// } else {
// themeNotifier.changeValue(AppThemes.LIGHT);
// }
int timeNow = DateTime.now().millisecondsSinceEpoch ~/ 1000;
if (token != "" && (exp - timeNow) > 7200) {
context.goNamed(AppRoutes.HOME.name);
}
}
getBaseToken(String token) {
List<String> parts = token.split('.');
String userToken = parts[1];
return userToken;
}
decodeBase64Token(String value) {
List<int> res = base64.decode(base64.normalize(value));
return utf8.decode(res);
}
}

View File

@@ -0,0 +1,22 @@
import 'dart:async';
import '../../product/base/bloc/base_bloc.dart';
import 'bell_model.dart';
class BellBloc extends BlocBase {
final bellItems = StreamController<List<BellItems>>.broadcast();
StreamSink<List<BellItems>> get sinkBellItems => bellItems.sink;
Stream<List<BellItems>> get streamBellItems => bellItems.stream;
final isLoading = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsLoading => isLoading.sink;
Stream<bool> get streamIsLoading => isLoading.stream;
final hasMore = StreamController<bool>.broadcast();
StreamSink<bool> get sinkHasMore => hasMore.sink;
Stream<bool> get streamHasMore => hasMore.stream;
@override
void dispose() {}
}

View File

@@ -0,0 +1,75 @@
class Bell {
int? offset;
String? result;
List<BellItems>? items;
Bell({
this.offset,
this.result,
this.items,
});
Bell.fromJson(Map<String, dynamic> json) {
offset = json["offset"];
result = json["result"];
if (json['items'] != null) {
items = [];
json['items'].forEach((v) {
items!.add(BellItems.fromJson(v));
});
} else {
items = [];
}
}
}
class BellItems {
String? id;
String? notifiType;
String? userId;
String? eventType;
ItemDetail? itemDetail;
int? status;
DateTime? createdAt;
DateTime? updatedAt;
BellItems(
{this.id, this.eventType, this.status, this.createdAt, this.updatedAt});
BellItems.fromJson(Map<String, dynamic> json) {
id = json["id"];
notifiType = json["notifi_type"];
userId = json["user_id"];
eventType = json["event_type"];
status = json["status"];
itemDetail =
json['detail'] != null ? ItemDetail.fromJson(json['detail']) : null;
createdAt = DateTime.parse(json["created_at"]);
updatedAt = DateTime.parse(json["updated_at"]);
}
static List<BellItems> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => BellItems.fromJson(e)).toList();
}
}
class ItemDetail {
String? sourceId;
String? sourceName;
String? targetId;
String? targetName;
ItemDetail({
this.sourceId,
this.sourceName,
this.targetId,
this.targetName,
});
ItemDetail.fromJson(Map<String, dynamic> json) {
sourceId = json["source_id"];
sourceName = json["source_name"];
targetId = json["target_id"];
targetName = json["target_name"];
}
}

View File

@@ -0,0 +1,253 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../../product/extention/context_extention.dart';
import '../../product/services/language_services.dart';
import '../../product/constant/enums/app_theme_enums.dart';
import 'bell_bloc.dart';
import '../../product/base/bloc/base_bloc.dart';
import '../../product/services/api_services.dart';
import 'bell_model.dart';
class BellScreen extends StatefulWidget {
const BellScreen({super.key});
@override
State<BellScreen> createState() => _BellScreenState();
}
class _BellScreenState extends State<BellScreen> {
late BellBloc bellBloc;
APIServices apiServices = APIServices();
Timer? getBellTimer;
int offset = 0;
Bell bell = Bell();
List<BellItems> items = [];
bool check = true;
bool hasMore = true;
final controller = ScrollController();
@override
void initState() {
super.initState();
bellBloc = BlocProvider.of(context);
getBellNotification(offset);
controller.addListener(
() {
if (controller.position.maxScrollExtent == controller.offset) {
offset += 20;
getBellNotification(offset);
}
},
);
}
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: bellBloc.streamIsLoading,
builder: (context, isLoadingSnapshot) {
return Scaffold(
appBar: AppBar(
title: Text(appLocalization(context).bell_page_title),
centerTitle: true,
),
body: StreamBuilder<List<BellItems>>(
stream: bellBloc.streamBellItems,
initialData: items,
builder: (context, bellSnapshot) {
return check
? Center(
child: CircularProgressIndicator(
value: context.highValue,
),
)
: bellSnapshot.data?.isEmpty ?? true
? Center(
child: Text(
appLocalization(context).bell_page_no_items_body),
)
: SizedBox(
width: double.infinity,
height: double.infinity,
child: RefreshIndicator(
onRefresh: refresh,
child: ListView.builder(
controller: controller,
itemCount: (bellSnapshot.data!.length + 1),
itemBuilder: (context, index) {
if (index < bellSnapshot.data!.length) {
return GestureDetector(
onTap: () async {
List<String> read = [];
read.add(bellSnapshot.data![index].id!);
int code = await apiServices
.updateStatusOfNotification(read);
if (code == 200) {
read.clear();
} else {
read.clear();
}
refresh();
},
child: Column(
children: [
ListTile(
title: Text(
getBellEvent(
context,
bellSnapshot.data![index]
.itemDetail!.sourceName!,
bellSnapshot
.data![index].eventType!,
bellSnapshot.data![index]
.itemDetail!.targetName!,
),
overflow: TextOverflow.ellipsis,
maxLines: 3,
style:
const TextStyle(fontSize: 15),
),
trailing: Text(
timeAgo(
context,
bellSnapshot
.data![index].createdAt!,
),
),
),
const Divider(
height: 1,
),
],
),
);
} else {
return Padding(
padding: const EdgeInsets.all(8.0),
child: StreamBuilder<bool>(
stream: bellBloc.streamHasMore,
builder: (context, hasMoreSnapshot) {
return Center(
child: hasMoreSnapshot.data ?? hasMore
? const CircularProgressIndicator()
: Text(
appLocalization(context)
.bell_read_all,
),
);
},
),
);
}
},
),
),
);
},
),
);
},
);
}
Future<void> refresh() async {
check = true;
offset = 0;
items.clear();
bellBloc.sinkBellItems.add(items);
hasMore = true;
getBellNotification(offset);
}
Future<void> getBellNotification(int offset) async {
bell = await apiServices.getBellNotifications(
offset.toString(), (offset + 20).toString());
if (bell.items!.isEmpty) {
hasMore = false;
bellBloc.sinkHasMore.add(hasMore);
} else {
for (var item in bell.items!) {
items.add(item);
}
}
bellBloc.bellItems.add(items);
check = false;
}
bool checkStatus(List<BellItems> bells) {
for (var bell in bells) {
if (bell.status == 0) {
return false;
}
}
return true;
}
Future<Color> colorByTheme(int status) async {
String theme = await apiServices.checkTheme();
if (theme == AppThemes.LIGHT.name && status == 1) {
return Colors.white;
} else if (theme == AppThemes.DARK.name && status == 1) {
return Colors.black;
} else {
return const Color.fromARGB(255, 90, 175, 214);
}
}
String timeAgo(BuildContext context, DateTime dateTime) {
final duration = DateTime.now().difference(dateTime);
if (duration.inDays > 0) {
return '${duration.inDays} ${appLocalization(context).bell_days_ago}';
} else if (duration.inHours > 0) {
return '${duration.inHours} ${appLocalization(context).bell_hours_ago}';
} else if (duration.inMinutes > 0) {
return '${duration.inMinutes} ${appLocalization(context).bell_minutes_ago}';
} else {
return appLocalization(context).bell_just_now;
}
}
String getBellEvent(BuildContext context, String deviceName, String eventCode,
String targetName) {
String message = "";
if (eventCode == "001") {
message =
"${appLocalization(context).device_title} $deviceName ${appLocalization(context).disconnect_message_lowercase}";
} else if (eventCode == "002") {
message =
"${appLocalization(context).device_title} $deviceName ${appLocalization(context).gf_connected_lowercase}";
} else if (eventCode == "003") {
message =
"${appLocalization(context).device_title} $deviceName ${appLocalization(context).smoke_detecting_message_lowercase}";
} else if (eventCode == "004") {
message =
"${appLocalization(context).device_title} $deviceName ${appLocalization(context).error_message_lowercase}";
} else if (eventCode == "005") {
message =
"${appLocalization(context).device_title} $deviceName ${appLocalization(context).bell_operate_normal}";
} else if (eventCode == "006") {
message =
"${appLocalization(context).bell_battery_device} $deviceName ${appLocalization(context).low_message_lowercase}";
} else if (eventCode == "101") {
message =
"${appLocalization(context).bell_user_uppercase} ${appLocalization(context).bell_user_joined_group}";
} else if (eventCode == "102") {
message =
"${appLocalization(context).bell_user_uppercase} $deviceName ${appLocalization(context).bell_leave_group} $targetName ";
} else if (eventCode == "103") {
message =
"${appLocalization(context).device_title} $deviceName ${appLocalization(context).bell_user_added_group} $targetName";
} else if (eventCode == "104") {
message =
"${appLocalization(context).device_title} $deviceName ${appLocalization(context).bell_user_kick_group} $targetName";
} else {
message = appLocalization(context).bell_invalid_code;
}
return message;
}
}

View File

@@ -0,0 +1,97 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import '../../product/utils/response_status_utils.dart';
import '../../product/constant/enums/role_enums.dart';
import '../../product/services/api_services.dart';
import '../../product/utils/qr_utils.dart';
import '../../product/constant/icon/icon_constants.dart';
import '../../product/extention/context_extention.dart';
import '../../product/services/language_services.dart';
addNewDevice(BuildContext context, String role) async {
TextEditingController extIDController = TextEditingController(text: "");
TextEditingController deviceNameController = TextEditingController(text: "");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
dismissDirection: DismissDirection.none,
duration: context.dynamicMinutesDuration(1),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${appLocalization(context).add_device_title}: ',
style: context.titleMediumTextStyle),
Container(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: () async {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
icon: IconConstants.instance.getMaterialIcon(Icons.close)),
)
],
),
TextField(
textInputAction: TextInputAction.next,
controller: extIDController,
decoration: InputDecoration(
hintText: appLocalization(context).input_extID_device_hintText,
suffixIcon: IconButton(
onPressed: () async {
String result =
await QRScanUtils.instance.scanQR(context);
extIDController.text = result;
},
icon: IconConstants.instance
.getMaterialIcon(Icons.qr_code_scanner_outlined))),
),
role == RoleEnums.ADMIN.name
? TextField(
textInputAction: TextInputAction.done,
controller: deviceNameController,
decoration: InputDecoration(
hintText:
appLocalization(context).input_name_device_hintText),
)
: const SizedBox.shrink(),
Center(
child: TextButton(
onPressed: () async {
String extID = extIDController.text;
String deviceName = deviceNameController.text;
addDevices(context, role, extID, deviceName);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
child: Text(appLocalization(context).add_button_content)),
)
],
),
),
);
}
void addDevices(
BuildContext context, String role, String extID, String deviceName) async {
APIServices apiServices = APIServices();
Map<String, dynamic> body = {};
if (role == RoleEnums.ADMIN.name) {
body = {"ext_id": extID, "name": deviceName};
int statusCode = await apiServices.createDeviceByAdmin(body);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_create_device_success,
appLocalization(context).notification_create_device_failed);
} else {
body = {"ext_id": extID};
int statusCode = await apiServices.registerDevice(body);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_add_device_success,
appLocalization(context).notification_device_not_exist);
}
}

View File

@@ -0,0 +1,61 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import '../../product/constant/enums/role_enums.dart';
import '../../product/services/api_services.dart';
import '../../product/services/language_services.dart';
import '../../product/utils/response_status_utils.dart';
handleDeleteDevice(BuildContext context, String thingID, String role) {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text(appLocalization(dialogContext).delete_device_dialog_title),
content:
Text(appLocalization(dialogContext).delete_device_dialog_content),
actions: <Widget>[
TextButton(
child: Text(appLocalization(dialogContext).cancel_button_content),
onPressed: () {
Navigator.of(dialogContext).pop();
},
),
TextButton(
child: Text(
appLocalization(dialogContext).delete_button_content,
style: const TextStyle(color: Colors.red),
),
onPressed: () {
deleteOrUnregisterDevice(context, thingID, role);
Navigator.of(context).pop();
},
),
],
);
},
);
}
deleteOrUnregisterDevice(
BuildContext context, String thingID, String role) async {
APIServices apiServices = APIServices();
if (role == RoleEnums.USER.name) {
Map<String, dynamic> body = {
"thing_id": thingID,
};
int statusCode = await apiServices.unregisterDevice(body);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_delete_device_success,
appLocalization(context).notification_delete_device_failed);
} else {
int statusCode = await apiServices.deleteDeviceByAdmin(thingID);
showSnackBarResponseByStatusCode(
context,
statusCode,
appLocalization(context).notification_delete_device_success,
appLocalization(context).notification_delete_device_failed);
}
}

View File

@@ -0,0 +1,79 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:sfm_app/product/services/api_services.dart';
import 'package:sfm_app/product/utils/device_utils.dart';
import '../device_model.dart';
import '../../../product/base/bloc/base_bloc.dart';
class DetailDeviceBloc extends BlocBase {
APIServices apiServices = APIServices();
final deviceInfo = StreamController<Device>.broadcast();
StreamSink<Device> get sinkDeviceInfo => deviceInfo.sink;
Stream<Device> get streamDeviceInfo => deviceInfo.stream;
final deviceSensor = StreamController<Map<String, dynamic>>.broadcast();
StreamSink<Map<String, dynamic>> get sinkDeviceSensor => deviceSensor.sink;
Stream<Map<String, dynamic>> get streamDeviceSensor => deviceSensor.stream;
final deviceLocation = StreamController<String>.broadcast();
StreamSink<String> get sinkDeviceLocation => deviceLocation.sink;
Stream<String> get streamDeviceLocation => deviceLocation.stream;
@override
void dispose() {}
void getDeviceDetail(
BuildContext context,
String thingID,
Completer<GoogleMapController> controller,
) async {
String body = await apiServices.getDeviceInfomation(thingID);
if (body != "") {
final data = jsonDecode(body);
Device device = Device.fromJson(data);
sinkDeviceInfo.add(device);
if (device.areaPath != null) {
String fullLocation = await DeviceUtils.instance
.getFullDeviceLocation(context, device.areaPath!);
log("Location: $fullLocation");
sinkDeviceLocation.add(fullLocation);
}
Map<String, dynamic> sensorMap = {};
if (device.status!.sensors != null) {
sensorMap = DeviceUtils.instance
.getDeviceSensors(context, device.status!.sensors!);
} else {
sensorMap = DeviceUtils.instance.getDeviceSensors(context, []);
}
sinkDeviceSensor.add(sensorMap);
if (device.settings!.latitude! != "" &&
device.settings!.longitude! != "") {
final CameraPosition cameraPosition = CameraPosition(
target: LatLng(
double.parse(device.settings!.latitude!),
double.parse(device.settings!.longitude!),
),
zoom: 13,
);
final GoogleMapController mapController = await controller.future;
mapController
.animateCamera(CameraUpdate.newCameraPosition(cameraPosition));
}
}
}
void findLocation(BuildContext context, String areaPath) async {
String fullLocation =
await DeviceUtils.instance.getFullDeviceLocation(context, areaPath);
sinkDeviceLocation.add(fullLocation);
}
}

View File

@@ -0,0 +1,361 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:simple_ripple_animation/simple_ripple_animation.dart';
import 'dart:math' as math;
import '../device_model.dart';
import '../../../product/base/bloc/base_bloc.dart';
import '../../../product/extention/context_extention.dart';
import '../../../product/services/language_services.dart';
import '../../../product/utils/device_utils.dart';
import '../../../product/constant/icon/icon_constants.dart';
import 'device_detail_bloc.dart';
class DetailDeviceScreen extends StatefulWidget {
const DetailDeviceScreen({super.key, required this.thingID});
final String thingID;
@override
State<DetailDeviceScreen> createState() => _DetailDeviceScreenState();
}
class _DetailDeviceScreenState extends State<DetailDeviceScreen> {
List<String> imageAssets = [
IconConstants.instance.getIcon("normal_icon"),
IconConstants.instance.getIcon("offline_icon"),
IconConstants.instance.getIcon("flame_icon"),
];
String stateImgAssets(int state) {
String imgStringAsset;
if (state == 0) {
imgStringAsset = imageAssets[0];
} else if (state == 1) {
imgStringAsset = imageAssets[1];
} else {
imgStringAsset = imageAssets[2];
}
return imgStringAsset;
}
late DetailDeviceBloc detailDeviceBloc;
Completer<GoogleMapController> controller = Completer();
CameraPosition initialCamera = const CameraPosition(
target: LatLng(20.966048511844402, 105.74977710843086), zoom: 15);
@override
void initState() {
super.initState();
detailDeviceBloc = BlocProvider.of(context);
}
@override
Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
return StreamBuilder<Device>(
stream: detailDeviceBloc.streamDeviceInfo,
builder: (context, deviceSnapshot) {
if (deviceSnapshot.data?.extId == null) {
detailDeviceBloc.getDeviceDetail(context, widget.thingID, controller);
return const Center(
child: CircularProgressIndicator(),
);
} else {
return StreamBuilder<Map<String, dynamic>>(
stream: detailDeviceBloc.streamDeviceSensor,
builder: (context, sensorSnapshot) {
if (sensorSnapshot.data != null) {
return Scaffold(
appBar: AppBar(
title: Text(appLocalization(context).detail_message),
centerTitle: true,
),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
// device Name
Card(
child: Container(
width: context.dynamicWidth(1),
height: context.highValue,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
child: Center(
child: Text(
'${appLocalization(context).device_title}: ${deviceSnapshot.data!.name}',
style: const TextStyle(
fontSize: 20,
),
),
),
),
),
// Tinh trang va nhiet do
Row(
children: [
Card(
child: Container(
width: (screenWidth - 20) / 2,
height: context.highValue,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
padding:
const EdgeInsets.fromLTRB(5, 5, 0, 5),
alignment: Alignment.centerLeft,
child: Row(
children: [
SizedBox(
height: 25,
width: 25,
child: RippleAnimation(
color: DeviceUtils.instance
.getColorRiple(
deviceSnapshot.data!.state!),
delay:
const Duration(milliseconds: 800),
repeat: true,
minRadius: 40,
ripplesCount: 6,
duration: const Duration(
milliseconds: 6 * 300),
child: CircleAvatar(
minRadius: 20,
maxRadius: 20,
backgroundImage: AssetImage(
stateImgAssets(
deviceSnapshot.data!.state!,
),
),
),
),
),
SizedBox(
width: context.lowValue,
),
Text(
DeviceUtils.instance.checkStateDevice(
context,
deviceSnapshot.data!.state!,
),
style: const TextStyle(
fontSize: 15,
),
),
],
),
),
),
Card(
child: SizedBox(
width: (screenWidth - 20) / 2,
height: context.highValue,
child: Container(
alignment: Alignment.centerLeft,
padding:
const EdgeInsets.fromLTRB(5, 5, 0, 5),
child: Row(
children: [
const Icon(
Icons.thermostat,
color: Colors.blue,
size: 30,
),
const SizedBox(
width: 10,
),
Text(
"${appLocalization(context).paginated_data_table_column_deviceTemperature}: ${sensorSnapshot.data?['sensorTemp'] ?? 100}",
style: const TextStyle(
fontSize: 15,
),
),
],
),
),
),
),
],
),
Row(
children: [
Card(
child: Container(
width: (screenWidth - 20) / 2,
height: context.highValue,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
alignment: Alignment.centerLeft,
padding:
const EdgeInsets.fromLTRB(10, 5, 0, 5),
child: Row(
children: [
Transform.rotate(
angle: 90 * math.pi / 180,
child: Icon(
DeviceUtils.instance.getBatteryIcon(
int.parse(
sensorSnapshot
.data!['sensorBattery'],
),
),
color: Colors.blue,
size: 30,
),
),
SizedBox(
width: context.lowValue,
),
Text(
"${appLocalization(context).paginated_data_table_column_deviceBaterry}: ${sensorSnapshot.data!['sensorBattery']}%",
style: const TextStyle(
fontSize: 15,
),
),
],
),
),
),
Card(
child: Container(
width: (screenWidth - 20) / 2,
height: context.highValue,
alignment: Alignment.centerLeft,
padding:
const EdgeInsets.fromLTRB(10, 5, 0, 5),
child: Row(
children: [
Icon(
DeviceUtils.instance.getSignalIcon(
context,
sensorSnapshot.data!['sensorCsq'],
),
color: Colors.blue,
size: 30,
),
SizedBox(
width: context.lowValue,
),
Text(
"${appLocalization(context).paginated_data_table_column_deviceSignal}: ${sensorSnapshot.data!['sensorCsq']}",
style: const TextStyle(fontSize: 15),
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
),
],
),
),
),
],
),
Card(
child: Container(
padding: const EdgeInsets.all(10.0),
height: context.highValue,
child: Row(
children: [
const Icon(
Icons.location_on,
color: Colors.blue,
size: 30,
),
SizedBox(
width: context.lowValue,
),
Expanded(
child: StreamBuilder<String>(
stream:
detailDeviceBloc.streamDeviceLocation,
builder: (context, locationSnapshot) {
if (locationSnapshot.data != null) {
return Text(
locationSnapshot.data ?? "",
style:
const TextStyle(fontSize: 13),
maxLines: 3,
overflow: TextOverflow.ellipsis,
softWrap: true,
);
} else {
detailDeviceBloc.findLocation(context,
deviceSnapshot.data!.areaPath!);
return Text(appLocalization(context)
.undefine_message);
}
},
),
),
],
),
),
),
Card(
child: Container(
height: 300,
padding: const EdgeInsets.fromLTRB(5, 5, 5, 5),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(15),
),
),
child: deviceSnapshot.data!.settings!.latitude !=
""
? GoogleMap(
initialCameraPosition: initialCamera,
mapType: MapType.normal,
markers: {
Marker(
markerId: MarkerId(
deviceSnapshot.data!.thingId!),
position: LatLng(
double.parse(deviceSnapshot
.data!.settings!.latitude!),
double.parse(deviceSnapshot
.data!.settings!.longitude!),
),
),
},
onMapCreated: (mapcontroller) {
controller.complete(mapcontroller);
},
mapToolbarEnabled: false,
zoomControlsEnabled: false,
liteModeEnabled: true,
)
: Center(
child: Text(
appLocalization(context)
.detail_device_dont_has_location_message,
),
),
),
),
],
),
),
),
);
} else {
return Scaffold(
appBar: AppBar(),
body: const Center(
child: CircularProgressIndicator(),
),
);
}
},
);
}
},
);
}
}

View File

@@ -0,0 +1,171 @@
import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
// class Device {
// int? offset;
// List<Item>? items;
// Device({
// this.offset,
// this.items,
// });
// Device.fromJson(Map<String, dynamic> json) {
// offset = json['offset'];
// if (json['items'] != null) {
// items = [];
// json['items'].forEach((v) {
// items!.add(Item.fromJson(v));
// });
// }
// }
// }
class Device with ClusterItem {
String? extId;
String? thingId;
String? thingKey;
List<String?>? channels;
String? areaPath;
String? fvers;
String? name;
HardwareInfo? hardwareInfo;
Settings? settings;
Status? status;
DateTime? connectionTime;
int? state;
String? visibility;
DateTime? createdAt;
DateTime? updatedAt;
Device({
this.extId,
this.thingId,
this.thingKey,
this.channels,
this.areaPath,
this.fvers,
this.name,
this.hardwareInfo,
this.settings,
this.status,
this.connectionTime,
this.state,
this.visibility,
this.createdAt,
this.updatedAt,
});
Device.fromJson(Map<String, dynamic> json) {
extId = json['ext_id'];
thingId = json['thing_id'];
thingKey = json['thing_key'];
if (json['channels'] != null) {
channels = [];
json['channels'].forEach((v) {
channels!.add(v);
});
}
areaPath = json['area_path'];
fvers = json['fvers'];
name = json['name'];
hardwareInfo = json['hardware_info'] != null
? HardwareInfo.fromJson(json['hardware_info'])
: null;
settings =
json['settings'] != null ? Settings.fromJson(json['settings']) : null;
status = json['status'] != null ? Status.fromJson(json['status']) : null;
connectionTime = DateTime.parse(json['connection_time']);
state = json['state'];
visibility = json['visibility'];
createdAt = DateTime.parse(json['created_at']);
updatedAt = DateTime.parse(json['updated_at']);
}
static List<Device> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => Device.fromJson(e)).toList();
}
@override
LatLng get location {
double latitude = double.tryParse(settings?.latitude ?? '') ?? 34.639971;
double longitude = double.tryParse(settings?.longitude ?? '') ?? -39.664027;
return LatLng(latitude, longitude);
}
}
class HardwareInfo {
String? gsm;
String? imei;
String? imsi;
String? phone;
DateTime? updatedAt;
HardwareInfo({
this.gsm,
this.imei,
this.imsi,
this.phone,
this.updatedAt,
});
HardwareInfo.fromJson(Map<String, dynamic> json) {
gsm = json['gsm'];
imei = json['imei'];
imsi = json['imsi'];
phone = json['phone'];
updatedAt = DateTime.parse(json['updated_at']);
}
}
class Settings {
String? latitude;
String? longitude;
DateTime? updatedAt;
Settings({
this.latitude,
this.longitude,
this.updatedAt,
});
Settings.fromJson(Map<String, dynamic> json) {
latitude = json['latitude'];
longitude = json['longitude'];
updatedAt = DateTime.parse(json['updated_at']);
}
}
class Status {
int? readOffset;
List<Sensor>? sensors;
DateTime? updatedAt;
Status({
this.readOffset,
this.sensors,
this.updatedAt,
});
Status.fromJson(Map<String, dynamic> json) {
readOffset = json['read_offset'];
if (json['sensors'] != null) {
sensors = [];
json['sensors'].forEach((v) {
sensors!.add(Sensor.fromJson(v));
});
}
updatedAt = DateTime.parse(json['updated_at']);
}
}
class Sensor {
String? name;
int? value;
int? time;
dynamic x;
Sensor({this.name, this.value, this.time, this.x});
Sensor.fromJson(Map<String, dynamic> json) {
name = json['n'];
value = json['v'];
time = json['t'];
x = json['x'];
}
}

View File

@@ -0,0 +1,255 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intl/intl.dart';
import '../../../product/services/api_services.dart';
import '../../../product/services/language_services.dart';
import '../../../product/shared/model/ward_model.dart';
import '../../../product/utils/response_status_utils.dart';
import '../../../product/shared/model/district_model.dart';
import '../../../product/shared/model/province_model.dart';
import '../device_model.dart';
import '../../../product/base/bloc/base_bloc.dart';
class DeviceUpdateBloc extends BlocBase {
APIServices apiServices = APIServices();
final deviceInfo = StreamController<Device>.broadcast();
StreamSink<Device> get sinkDeviceInfo => deviceInfo.sink;
Stream<Device> get streamDeviceInfo => deviceInfo.stream;
// DeviceUpdateScreen
final isChanged = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsChanged => isChanged.sink;
Stream<bool> get streamIsChanged => isChanged.stream;
final provinceData = StreamController<Map<String, String>>.broadcast();
StreamSink<Map<String, String>> get sinkProvinceData => provinceData.sink;
Stream<Map<String, String>> get streamProvinceData => provinceData.stream;
final listProvinces =
StreamController<List<DropdownMenuItem<Province>>>.broadcast();
StreamSink<List<DropdownMenuItem<Province>>> get sinkListProvinces =>
listProvinces.sink;
Stream<List<DropdownMenuItem<Province>>> get streamListProvinces =>
listProvinces.stream;
final districtData = StreamController<Map<String, String>>.broadcast();
StreamSink<Map<String, String>> get sinkDistrictData => districtData.sink;
Stream<Map<String, String>> get streamDistrictData => districtData.stream;
final listDistricts =
StreamController<List<DropdownMenuItem<District>>>.broadcast();
StreamSink<List<DropdownMenuItem<District>>> get sinkListDistricts =>
listDistricts.sink;
Stream<List<DropdownMenuItem<District>>> get streamListDistricts =>
listDistricts.stream;
final wardData = StreamController<Map<String, String>>.broadcast();
StreamSink<Map<String, String>> get sinkWardData => wardData.sink;
Stream<Map<String, String>> get streamWardData => wardData.stream;
final listWards = StreamController<List<DropdownMenuItem<Ward>>>.broadcast();
StreamSink<List<DropdownMenuItem<Ward>>> get sinkListWards => listWards.sink;
Stream<List<DropdownMenuItem<Ward>>> get streamListWards => listWards.stream;
// Show Maps in DeviceUpdateScreen
final markers = StreamController<Set<Marker>>.broadcast();
StreamSink<Set<Marker>> get sinkMarkers => markers.sink;
Stream<Set<Marker>> get streamMarkers => markers.stream;
final searchLocation = StreamController<TextEditingController>.broadcast();
StreamSink<TextEditingController> get sinkSearchLocation =>
searchLocation.sink;
Stream<TextEditingController> get streamSearchLocation =>
searchLocation.stream;
@override
void dispose() {
// deviceInfo.done;
}
Future<void> getAllProvinces() async {
List<DropdownMenuItem<Province>> provincesData = [];
provincesData.clear();
sinkListProvinces.add(provincesData);
final body = await apiServices.getAllProvinces();
final data = jsonDecode(body);
List<dynamic> items = data["items"];
final provinces = Province.fromJsonDynamicList(items);
for (var province in provinces) {
provincesData.add(
DropdownMenuItem(value: province, child: Text(province.fullName!)));
}
sinkListProvinces.add(provincesData);
}
Future<void> getAllDistricts(String provinceID) async {
List<DropdownMenuItem<District>> districtsData = [];
districtsData.clear();
sinkListDistricts.add(districtsData);
final body = await apiServices.getAllDistricts(provinceID);
final data = jsonDecode(body);
List<dynamic> items = data["items"];
final districts = District.fromJsonDynamicList(items);
for (var district in districts) {
districtsData.add(
DropdownMenuItem(value: district, child: Text(district.fullName!)));
}
sinkListDistricts.add(districtsData);
}
Future<void> getAllWards(String districtID) async {
List<DropdownMenuItem<Ward>> wardsData = [];
wardsData.clear();
sinkListWards.add(wardsData);
final body = await apiServices.getAllWards(districtID);
final data = jsonDecode(body);
List<dynamic> items = data["items"];
final wards = Ward.fromJsonDynamicList(items);
for (var ward in wards) {
wardsData.add(DropdownMenuItem(value: ward, child: Text(ward.fullName!)));
}
sinkListWards.add(wardsData);
}
Future<void> getDeviceInfomation(
String thingID,
List<DropdownMenuItem<District>> districtsData,
List<DropdownMenuItem<Ward>> wardsData,
TextEditingController deviceNameController,
TextEditingController latitudeController,
TextEditingController longitudeController) async {
String body = await apiServices.getDeviceInfomation(thingID);
final data = jsonDecode(body);
Device device = Device.fromJson(data);
sinkDeviceInfo.add(device);
deviceNameController.text = device.name ?? "";
latitudeController.text = device.settings!.latitude ?? "";
longitudeController.text = device.settings!.longitude ?? "";
if (device.areaPath != "") {
List<String> areaPath = device.areaPath!.split('_');
String provinceCode = areaPath[0];
String districtCode = areaPath[1];
String wardCode = areaPath[2];
getAllDistricts(provinceCode);
getAllWards(districtCode);
final provinceResponse = await apiServices.getProvinceByID(provinceCode);
final provincesData = jsonDecode(provinceResponse);
Province province = Province.fromJson(provincesData['data']);
final districtResponse = await apiServices.getDistrictByID(districtCode);
final districtData = jsonDecode(districtResponse);
District district = District.fromJson(districtData['data']);
final wardResponse = await apiServices.getWardByID(wardCode);
final wardData = jsonDecode(wardResponse);
Ward ward = Ward.fromJson(wardData['data']);
Map<String, String> provinceData = {
"name": province.fullName!,
"code": province.code!
};
sinkProvinceData.add(provinceData);
Map<String, String> districData = {
"name": district.fullName!,
"code": district.code!,
};
sinkDistrictData.add(districData);
Map<String, String> wardMap = {
"name": ward.fullName!,
"code": ward.code!,
};
sinkWardData.add(wardMap);
}
}
Future<Province> getProvinceByName(String name) async {
final response = await apiServices.getProvincesByName(name);
final data = jsonDecode(response);
if (data != null &&
data.containsKey('items') &&
data['items'] != null &&
data['items'].isNotEmpty) {
List<dynamic> items = data['items'];
List<Province> provinces = Province.fromJsonDynamicList(items);
if (provinces.isNotEmpty) {
return provinces[0];
}
}
return Province(name: "null");
}
Future<District> getDistrictByName(String name, String provinceCode) async {
final response = await apiServices.getDistrictsByName(name);
if (response != "") {
final data = jsonDecode(response);
List<dynamic> items = data['items'];
if (items.isNotEmpty) {
List<District> districts = District.fromJsonDynamicList(items);
if (districts.isNotEmpty) {
for (var district in districts) {
if (district.provinceCode == provinceCode) {
return district;
}
}
}
}
}
return District(name: "null");
}
Future<Ward> getWardByName(String name, String districtCode) async {
final response = await apiServices.getWarsdByName(name);
final data = jsonDecode(response);
if (data != null && data['items'] != null) {
List<dynamic> items = data['items'];
if (items.isNotEmpty) {
List<Ward> wards = Ward.fromJsonDynamicList(items);
if (wards.isNotEmpty) {
for (var ward in wards) {
if (ward.districtCode == districtCode) {
return ward;
}
}
}
}
}
return Ward(name: "null");
}
Future<void> updateDevice(
BuildContext context,
String thingID,
String name,
String latitude,
String longitude,
String provinceCode,
String districtCode,
String wardCode,
) async {
DateTime dateTime = DateTime.now();
String formattedDateTime =
DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime);
Map<String, dynamic> body = {
"name": name,
"area_province": provinceCode,
"area_district": districtCode,
"area_ward": wardCode,
"latitude": latitude,
"longitude": longitude,
"note": "User updated device infomation at $formattedDateTime",
};
int statusCode = await apiServices.updateOwnerDevice(thingID, body);
showSnackBarResponseByStatusCodeNoIcon(
context,
statusCode,
appLocalization(context).notification_update_device_success,
appLocalization(context).notification_update_device_failed,
);
}
}

View File

@@ -0,0 +1,515 @@
import 'package:flutter/material.dart';
import 'package:search_choices/search_choices.dart';
import '../device_model.dart';
import 'device_update_bloc.dart';
import 'map_dialog.dart';
import '../../../product/base/bloc/base_bloc.dart';
import '../../../product/extention/context_extention.dart';
import '../../../product/services/api_services.dart';
import '../../../product/services/language_services.dart';
import '../../../product/shared/model/district_model.dart';
import '../../../product/shared/model/province_model.dart';
import '../../../product/shared/model/ward_model.dart';
class DeviceUpdateScreen extends StatefulWidget {
const DeviceUpdateScreen({super.key, required this.thingID});
final String thingID;
@override
State<DeviceUpdateScreen> createState() => _DeviceUpdateScreenState();
}
class _DeviceUpdateScreenState extends State<DeviceUpdateScreen> {
late DeviceUpdateBloc deviceUpdateBloc;
APIServices apiServices = APIServices();
Device device = Device();
bool isChanged = false;
TextEditingController deviceNameController = TextEditingController();
TextEditingController deviceLatitudeController = TextEditingController();
TextEditingController deviceLongitudeController = TextEditingController();
List<DropdownMenuItem<Province>> provincesData = [];
String? selectedProvince = "";
List<DropdownMenuItem<District>> districtsData = [];
String? selectedDistrict = "";
List<DropdownMenuItem<Ward>> wardsData = [];
String? selectedWard = "";
// static String provinceCode = "";
// static String districtCode = "";
// static String wardCode = "";
Map<String, String> provinceData = {};
Map<String, String> districtData = {};
Map<String, String> wardData = {};
@override
void initState() {
super.initState();
deviceUpdateBloc = BlocProvider.of(context);
deviceUpdateBloc.getAllProvinces();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(appLocalization(context).device_update_title),
),
body: StreamBuilder<Map<String, String>>(
stream: deviceUpdateBloc.streamProvinceData,
builder: (context, provinceNameSnapshot) {
return StreamBuilder<Map<String, String>>(
stream: deviceUpdateBloc.streamDistrictData,
builder: (context, districtNameSnapshot) {
return StreamBuilder<Map<String, String>>(
stream: deviceUpdateBloc.streamWardData,
builder: (context, wardNameSnapshot) {
return SafeArea(
child: StreamBuilder<Device>(
stream: deviceUpdateBloc.streamDeviceInfo,
initialData: device,
builder: (context, deviceInfoSnapshot) {
if (deviceInfoSnapshot.data!.thingId == null) {
deviceUpdateBloc.getDeviceInfomation(
widget.thingID,
districtsData,
wardsData,
deviceNameController,
deviceLatitudeController,
deviceLongitudeController);
return const Center(
child: CircularProgressIndicator(),
);
} else {
return StreamBuilder<bool>(
stream: deviceUpdateBloc.streamIsChanged,
initialData: isChanged,
builder: (context, isChangedSnapshot) {
return SingleChildScrollView(
child: Padding(
padding: context.paddingLow,
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"${appLocalization(context).input_name_device_device}:",
style: context
.titleMediumTextStyle),
Padding(
padding:
context.paddingLowVertical,
child: TextField(
onChanged: (value) {
isChangedListener();
},
textInputAction:
TextInputAction.next,
controller:
deviceNameController,
decoration: InputDecoration(
hintText: appLocalization(
context)
.input_name_device_hintText,
),
),
),
Text(
"${appLocalization(context).device_update_location}:",
style: context
.titleMediumTextStyle),
Padding(
padding:
context.paddingLowVertical,
child: Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
SizedBox(
width: context
.dynamicWidth(0.6),
child: Column(
children: [
SizedBox(
child: TextField(
onChanged: (value) {
isChangedListener();
},
textInputAction:
TextInputAction
.next,
controller:
deviceLatitudeController,
decoration:
InputDecoration(
label: Text(
"${appLocalization(context).update_device_dialog_location_longitude}:"),
hintText: appLocalization(
context)
.update_device_dialog_location_longitude_hintText,
),
),
),
Padding(
padding: context
.paddingLowVertical,
child: SizedBox(
child: TextField(
onChanged:
(value) {
isChangedListener();
},
controller:
deviceLongitudeController,
textInputAction:
TextInputAction
.next,
decoration:
InputDecoration(
label: Text(
"${appLocalization(context).update_device_dialog_location_latitude}:"),
hintText: appLocalization(
context)
.update_device_dialog_location_latitude_hintText,
),
),
),
),
],
),
),
Center(
child: IconButton.filled(
style: const ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(
Colors
.lightGreen)),
// iconSize: 24,
onPressed: () async {
showMapDialog(
context,
deviceUpdateBloc,
deviceLatitudeController,
deviceLongitudeController);
},
icon: const Icon(
Icons.map_outlined,
),
),
),
SizedBox(
width: context.lowValue),
],
),
),
Text(
"${appLocalization(context).device_update_province}:",
style: context
.titleMediumTextStyle),
Padding(
padding:
context.paddingLowVertical,
child: StreamBuilder<
List<
DropdownMenuItem<
Province>>>(
stream: deviceUpdateBloc
.streamListProvinces,
builder: (dialogContext,
listProvinces) {
return Container(
decoration: BoxDecoration(
borderRadius:
const BorderRadius
.all(
Radius.circular(20),
),
border: Border.all(),
),
child: SearchChoices.single(
items:
listProvinces.data ??
provincesData,
hint: provinceNameSnapshot
.data !=
null
? Text(
provinceNameSnapshot
.data![
'name'] ??
"",
)
: appLocalization(
context)
.update_device_dialog_location_province_hintText,
searchHint: appLocalization(
context)
.update_device_dialog_location_province_searchHint,
displayClearIcon: false,
onChanged: (value) {
isChangedListener();
// provinceCode =
// value.code;
selectedProvince =
value.fullName;
provinceData['name'] =
value.fullName;
provinceData['code'] =
value.code;
deviceUpdateBloc
.sinkProvinceData
.add(provinceData);
deviceUpdateBloc
.getAllDistricts(
value.code);
selectedDistrict = "";
districtData['name'] =
selectedDistrict!;
// deviceUpdateBloc.sinkDistrictName
// .add(selectedDistrict);
deviceUpdateBloc
.sinkDistrictData
.add(districtData);
selectedWard = "";
wardData['name'] =
selectedWard!;
deviceUpdateBloc
.sinkWardData
.add(wardData);
},
isExpanded: true,
),
);
},
),
),
Text(
"${appLocalization(context).device_update_district}:",
style: context
.titleMediumTextStyle),
Padding(
padding:
context.paddingLowVertical,
child: StreamBuilder<
List<
DropdownMenuItem<
District>>>(
stream: deviceUpdateBloc
.streamListDistricts,
builder: (dialogContext,
listDistricts) {
return Container(
decoration: BoxDecoration(
borderRadius:
const BorderRadius
.all(
Radius.circular(20),
),
border: Border.all(),
),
child: SearchChoices.single(
items:
listDistricts.data ??
districtsData,
hint: districtNameSnapshot
.data !=
null
? Text(
districtNameSnapshot
.data![
'name'] ??
selectedDistrict!,
)
: appLocalization(
context)
.update_device_dialog_location_district_hintText,
searchHint: appLocalization(
context)
.update_device_dialog_location_district_searchHint,
displayClearIcon: false,
onChanged: (value) {
isChangedListener();
// districtCode =
// value.code;
selectedDistrict =
value.fullName;
districtData['name'] =
value.fullName!;
districtData['code'] =
value.code;
deviceUpdateBloc
.sinkDistrictData
.add(districtData);
deviceUpdateBloc
.getAllWards(
value.code);
selectedWard = "";
wardData['name'] =
selectedWard!;
deviceUpdateBloc
.sinkWardData
.add(wardData);
},
isExpanded: true,
),
);
},
),
),
Text(
"${appLocalization(context).device_update_ward}:",
style: context
.titleMediumTextStyle),
Padding(
padding:
context.paddingLowVertical,
child: StreamBuilder<
List<DropdownMenuItem<Ward>>>(
stream: deviceUpdateBloc
.streamListWards,
builder:
(dialogContext, listWards) {
return Container(
decoration: BoxDecoration(
borderRadius:
const BorderRadius
.all(
Radius.circular(
20)),
border: Border.all()),
child: SearchChoices.single(
items: listWards.data ??
wardsData,
hint: wardNameSnapshot
.data !=
null
? Text(
wardNameSnapshot
.data![
'name'] ??
selectedWard!,
)
: appLocalization(
context)
.update_device_dialog_location_ward_hintText,
searchHint: appLocalization(
context)
.update_device_dialog_location_ward_searchHint,
displayClearIcon: false,
onChanged: (value) {
isChangedListener();
// wardCode = value.code;
selectedWard =
value.fullName;
wardData['name'] =
value.fullName!;
wardData['code'] =
value.code!;
deviceUpdateBloc
.sinkWardData
.add(wardData);
},
isExpanded: true,
),
);
},
),
),
if (isChangedSnapshot.data == true)
Center(
child: SizedBox(
width:
context.dynamicWidth(0.6),
child: TextButton(
style: ButtonStyle(
foregroundColor:
MaterialStateProperty
.all(Colors
.white),
backgroundColor:
MaterialStateProperty
.all(Colors
.blue)),
onPressed: () async {
String provinceCode =
provinceNameSnapshot
.data![
"code"] ??
"";
String districtCode =
districtNameSnapshot
.data![
"code"] ??
"";
String wardCode =
wardNameSnapshot
.data![
"code"] ??
"";
String latitude =
deviceLatitudeController
.value.text;
String longitude =
deviceLongitudeController
.value.text;
String deviceName =
deviceNameController
.value.text;
// log("ProvinceCode: $provinceCode");
// log("DistrictCode: $districtCode");
// log("WardCode: $wardCode");
// log("Latitude: $latitude");
// log("Longitude: $longitude");
// log("Device Name: $deviceName");
await deviceUpdateBloc
.updateDevice(
context,
deviceInfoSnapshot
.data!.thingId!,
deviceName,
latitude,
longitude,
provinceCode,
districtCode,
wardCode,
);
Future.delayed(
// ignore: use_build_context_synchronously
context.lowDuration,
() {
Navigator.pop(
context);
});
},
child: Text(appLocalization(
context)
.update_button_content)),
),
),
],
),
),
);
},
);
}
},
),
);
});
});
}),
);
}
void isChangedListener() {
isChanged = true;
deviceUpdateBloc.sinkIsChanged.add(isChanged);
}
}

View File

@@ -0,0 +1,47 @@
class Geocode {
List<AddressComponent>? addressComponents;
String? formattedAddress;
Geocode({
this.addressComponents,
this.formattedAddress,
});
Geocode.fromJson(Map<String, dynamic> json) {
formattedAddress = json['formatted_address'];
if (json['address_components'] != null) {
addressComponents = [];
json['address_components'].forEach((v) {
addressComponents!.add(AddressComponent.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['formatted_address'] = formattedAddress;
if (addressComponents != null) {
data['address_components'] =
addressComponents!.map((e) => e.toJson()).toList();
}
return data;
}
}
class AddressComponent {
String? longName;
String? shortName;
List<String>? types;
AddressComponent({this.longName, this.shortName, this.types});
AddressComponent.fromJson(Map<String, dynamic> json) {
longName = json['long_name'];
shortName = json['short_name'];
types = json['types'].cast<String>();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['long_name'] = longName;
data['short_name'] = shortName;
data['type'] = types;
return data;
}
}

View File

@@ -0,0 +1,298 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'device_update_bloc.dart';
import '../../../product/constant/app/app_constants.dart';
import '../../../product/extention/context_extention.dart';
import '../../../product/services/language_services.dart';
import '../../../product/shared/find_location_maps/shared_map_search_location.dart';
import '../../../product/shared/find_location_maps/model/prediction_model.dart';
import '../../../product/shared/shared_transition.dart';
import 'geocode_model.dart';
showMapDialog(
BuildContext context,
DeviceUpdateBloc deviceUpdateBloc,
TextEditingController latitudeController,
TextEditingController longitudeController) async {
const CameraPosition defaultPosition = CameraPosition(
target: LatLng(20.985424, 105.738354),
zoom: 12,
);
TextEditingController searchLocationController = TextEditingController();
TextEditingController mapDialogLatitudeController = TextEditingController();
TextEditingController mapDialogLongitudeController = TextEditingController();
Completer<GoogleMapController> ggmapController = Completer();
final streamController = StreamController<GoogleMapController>.broadcast();
showGeneralDialog(
barrierDismissible: false,
transitionDuration: context.normalDuration,
transitionBuilder: transitionsLeftToRight,
context: context,
pageBuilder: (context, animation, secondaryAnimation) {
return StreamBuilder<Set<Marker>>(
stream: deviceUpdateBloc.streamMarkers,
builder: (context, markerSnapshot) {
if (!markerSnapshot.hasData) {
if (latitudeController.value.text != "" &&
longitudeController.value.text != "") {
double latitude = double.parse(latitudeController.text);
double longitude = double.parse(longitudeController.text);
addMarker(
LatLng(latitude, longitude),
ggmapController,
deviceUpdateBloc,
mapDialogLatitudeController,
mapDialogLongitudeController,
);
}
}
return Scaffold(
appBar: AppBar(
title: Text(appLocalization(context)
.update_device_dialog_maps_dialog_title),
centerTitle: true,
actions: [
IconButton(
onPressed: () async {
String latitude = mapDialogLatitudeController.text;
String longitude = mapDialogLongitudeController.text;
log("Finish -- Latitude: $latitude, longitude: $longitude --");
getDataFromApi(latitude, longitude, deviceUpdateBloc);
latitudeController.text =
mapDialogLatitudeController.text;
longitudeController.text =
mapDialogLongitudeController.text;
bool isChange = true;
deviceUpdateBloc.sinkIsChanged.add(isChange);
Navigator.of(context).pop();
},
icon: const Icon(Icons.check))
],
),
body: Stack(
children: [
GoogleMap(
onTap: (location) async {
addMarker(
location,
ggmapController,
deviceUpdateBloc,
mapDialogLatitudeController,
mapDialogLongitudeController,
);
},
markers: markerSnapshot.data ?? {},
onMapCreated: (GoogleMapController mapController) {
ggmapController.complete(mapController);
streamController.add(mapController);
},
initialCameraPosition: defaultPosition,
),
Container(
// color: Colors.white,
height: 80,
alignment: Alignment.topCenter,
padding: const EdgeInsets.all(10),
child: StreamBuilder<TextEditingController>(
stream: deviceUpdateBloc.streamSearchLocation,
builder: (context, searchLocation) {
return NearBySearchSFM(
textInputAction: TextInputAction.done,
textEditingController:
searchLocation.data ?? searchLocationController,
googleAPIKey: ApplicationConstants.MAP_KEY,
locationLatitude: 20.985424,
locationLongitude: 105.738354,
radius: 50000,
inputDecoration: InputDecoration(
hintText: appLocalization(context)
.update_device_dialog_search_location_hint,
border: InputBorder.none),
debounceTime: 600,
isLatLngRequired: true,
getPlaceDetailWithLatLng: (Prediction prediction) {
FocusScope.of(context).unfocus();
addMarker(
LatLng(double.parse(prediction.lat!),
double.parse(prediction.lng!)),
ggmapController,
deviceUpdateBloc,
mapDialogLatitudeController,
mapDialogLongitudeController);
},
itemClick: (Prediction prediction) {
searchLocationController.text =
prediction.structuredFormatting!.mainText!;
deviceUpdateBloc.sinkSearchLocation
.add(searchLocationController);
searchLocationController.selection =
TextSelection.fromPosition(TextPosition(
offset: prediction.structuredFormatting!
.mainText!.length));
},
boxDecoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.grey,
width: 0.5,
),
borderRadius: const BorderRadius.all(
Radius.circular(20))),
);
}),
)
],
),
);
});
},
);
}
addMarker(
LatLng position,
Completer<GoogleMapController> mapController,
DeviceUpdateBloc deviceUpdateBloc,
TextEditingController mapDialogLatitudeController,
TextEditingController mapDialogLongitudeController,
) async {
log("AddMarker -- Latitude: ${position.latitude}, longitude: ${position.longitude} --");
Set<Marker> marker = {};
deviceUpdateBloc.sinkMarkers.add(marker);
Marker newMarker = Marker(
markerId: const MarkerId('value'),
position: LatLng(position.latitude, position.longitude),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
draggable: true,
onDragEnd: (position) {
mapDialogLatitudeController.text = position.latitude.toString();
mapDialogLongitudeController.text = position.longitude.toString();
},
);
marker.add(newMarker);
deviceUpdateBloc.sinkMarkers.add(marker);
mapDialogLatitudeController.text = position.latitude.toString();
mapDialogLongitudeController.text = position.longitude.toString();
updateCameraPosition(position, 14, mapController);
}
void getDataFromApi(String latitude, String longitude,
DeviceUpdateBloc deviceUpdateBloc) async {
String path =
"maps/api/geocode/json?latlng=$latitude,$longitude&language=vi&result_type=political&key=${ApplicationConstants.MAP_KEY}";
var url = Uri.parse('https://maps.googleapis.com/$path');
final response = await http.get(url);
if (response.statusCode != 200) {
log("Loi: ${response.statusCode}");
return;
}
Map<String, dynamic> data = jsonDecode(response.body);
if (!data.containsKey('results') || data['results'].isEmpty) {
log("Khong co result");
return;
}
List<dynamic> results = data['results'];
List<Geocode> geocodes =
results.map((result) => Geocode.fromJson(result)).toList();
Map<String, String> locations =
_extractLocationComponents(geocodes[0].addressComponents!);
// In ra thông tin của các location
locations.forEach((key, value) {
log("$key: $value");
});
await _processLocations(locations, deviceUpdateBloc);
}
Map<String, String> _extractLocationComponents(
List<AddressComponent> addressComponents) {
Map<String, String> locations = {};
for (var addressComponent in addressComponents) {
String longName = addressComponent.longName ?? "";
if (addressComponent.types!.contains('administrative_area_level_3') ||
addressComponent.types!.contains('sublocality_level_1')) {
locations['wardkey'] = longName;
} else if (addressComponent.types!
.contains('administrative_area_level_2') ||
addressComponent.types!.contains('sublocality_level_2') ||
addressComponent.types!.contains('locality')) {
locations['districtkey'] = longName;
} else if (addressComponent.types!
.contains('administrative_area_level_1')) {
locations['provincekey'] = longName;
}
}
return locations;
}
Future<void> _processLocations(
Map<String, String> locations, DeviceUpdateBloc deviceUpdateBloc) async {
String provinceNameFromAPI = locations['provincekey'] ?? "";
String districtNameFromAPI = locations['districtkey'] ?? "";
String wardNameFromAPI = locations['wardkey'] ?? "";
final province =
await deviceUpdateBloc.getProvinceByName(provinceNameFromAPI);
if (province.name != "null") {
log("Province: ${province.fullName}, ProvinceCode: ${province.code}");
deviceUpdateBloc.sinkProvinceData
.add({"code": province.code!, "name": province.fullName!});
deviceUpdateBloc.getAllProvinces();
final district = await deviceUpdateBloc.getDistrictByName(
districtNameFromAPI, province.code!);
log("Districtname: ${district.fullName}, districtCode: ${district.code}");
deviceUpdateBloc.getAllDistricts(province.code!);
if (district.name != "null") {
deviceUpdateBloc.sinkDistrictData
.add({"code": district.code!, "name": district.fullName!});
final ward =
await deviceUpdateBloc.getWardByName(wardNameFromAPI, district.code!);
log("Wardname: ${ward.fullName}, WardCode: ${ward.code}");
deviceUpdateBloc.getAllWards(district.code!);
if (ward.name != "null") {
log("Xac dinh duoc het thong tin tu toa do");
deviceUpdateBloc.sinkWardData
.add({"code": ward.code!, "name": ward.fullName!});
} else {
deviceUpdateBloc.sinkWardData.add({});
}
} else {
deviceUpdateBloc.sinkDistrictData.add({});
}
} else {
deviceUpdateBloc.sinkProvinceData.add({});
}
}
Future<void> updateCameraPosition(LatLng location, double zoom,
Completer<GoogleMapController> mapController) async {
final CameraPosition cameraPosition = CameraPosition(
target: LatLng(
location.latitude,
location.longitude,
),
zoom: zoom,
);
final GoogleMapController mapControllerNew = await mapController.future;
mapControllerNew.animateCamera(
CameraUpdate.newCameraPosition(
cameraPosition,
),
);
}

View File

@@ -0,0 +1,35 @@
import 'dart:async';
import 'dart:convert';
import 'device_model.dart';
import '../../product/base/bloc/base_bloc.dart';
import '../../product/services/api_services.dart';
import '../../product/utils/device_utils.dart';
class DevicesManagerBloc extends BlocBase {
APIServices apiServices = APIServices();
final userRole = StreamController<String>.broadcast();
StreamSink<String> get sinkUserRole => userRole.sink;
Stream<String> get streamUserRole => userRole.stream;
final allDevices = StreamController<List<Device>>.broadcast();
StreamSink<List<Device>> get sinkAllDevices => allDevices.sink;
Stream<List<Device>> get streamAllDevices => allDevices.stream;
@override
void dispose() {}
void getDevice() async {
String body = await apiServices.getOwnerDevices();
if (body != "") {
final data = jsonDecode(body);
List<dynamic> items = data['items'];
List<Device> originalDevices = Device.fromJsonDynamicList(items);
List<Device> devices =
DeviceUtils.instance.sortDeviceByState(originalDevices);
sinkAllDevices.add(devices);
}
}
}

View File

@@ -0,0 +1,274 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'add_new_device_widget.dart';
import 'delete_device_widget.dart';
import 'device_model.dart';
import 'devices_manager_bloc.dart';
import '../../product/base/bloc/base_bloc.dart';
import '../../product/constant/enums/app_route_enums.dart';
import '../../product/constant/enums/role_enums.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';
class DevicesManagerScreen extends StatefulWidget {
const DevicesManagerScreen({super.key});
@override
State<DevicesManagerScreen> createState() => _DevicesManagerScreenState();
}
class _DevicesManagerScreenState extends State<DevicesManagerScreen> {
late DevicesManagerBloc devicesManagerBloc;
String role = "Undefine";
APIServices apiServices = APIServices();
List<Device> devices = [];
Timer? getAllDevicesTimer;
@override
void initState() {
super.initState();
devicesManagerBloc = BlocProvider.of(context);
getUserRole();
// devicesManagerBloc.getDevice();
// getAllOwnerDevices();
// const duration = Duration(seconds: 10);
// getAllDevicesTimer =
// Timer.periodic(duration, (Timer t) => devicesManagerBloc.getDevice());
}
@override
void dispose() {
getAllDevicesTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: StreamBuilder<List<Device>>(
stream: devicesManagerBloc.streamAllDevices,
initialData: devices,
builder: (context, allDeviceSnapshot) {
if (allDeviceSnapshot.data?.isEmpty ?? devices.isEmpty) {
devicesManagerBloc.getDevice();
return const Center(child: CircularProgressIndicator());
} else {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
StreamBuilder<String>(
stream: devicesManagerBloc.streamUserRole,
initialData: role,
builder: (context, roleSnapshot) {
return PaginatedDataTable(
header: Center(
child: Text(
appLocalization(context)
.paginated_data_table_title,
style: context.titleLargeTextStyle,
),
),
columns: [
if (roleSnapshot.data == RoleEnums.ADMIN.name ||
roleSnapshot.data == RoleEnums.USER.name)
DataColumn(
label: Center(
child: Text(appLocalization(context)
.paginated_data_table_column_action))),
DataColumn(
label: Center(
child: Text(appLocalization(context)
.paginated_data_table_column_deviceName))),
DataColumn(
label: Center(
child: Text(appLocalization(context)
.paginated_data_table_column_deviceStatus))),
DataColumn(
label: Center(
child: Text(appLocalization(context)
.paginated_data_table_column_deviceBaterry))),
DataColumn(
label: Center(
child: Text(appLocalization(context)
.paginated_data_table_column_deviceSignal))),
DataColumn(
label: Center(
child: Text(appLocalization(context)
.paginated_data_table_column_deviceTemperature))),
DataColumn(
label: Center(
child: Text(appLocalization(context)
.paginated_data_table_column_deviceHump))),
DataColumn(
label: Center(
child: Text(appLocalization(context)
.paginated_data_table_column_devicePower))),
],
onPageChanged: (int pageIndex) {
// log('Chuyen page: $pageIndex');
},
rowsPerPage: 5,
actions: [
if (roleSnapshot.data == RoleEnums.USER.name ||
roleSnapshot.data == RoleEnums.ADMIN.name)
IconButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(
Colors.green),
iconColor:
MaterialStateProperty.all<Color>(
Colors.white)),
onPressed: () {
ScaffoldMessenger.of(context)
.clearSnackBars();
addNewDevice(
context, roleSnapshot.data ?? role);
},
icon: IconConstants.instance
.getMaterialIcon(Icons.add))
],
source: DeviceSource(
devices: allDeviceSnapshot.data ?? devices,
context: context,
devicesBloc: devicesManagerBloc,
role: role));
})
],
),
);
}
}),
);
}
void getUserRole() async {
role = await apiServices.getUserRole();
devicesManagerBloc.sinkUserRole.add(role);
}
}
class DeviceSource extends DataTableSource {
String role;
APIServices apiServices = APIServices();
List<Device> devices;
final DevicesManagerBloc devicesBloc;
final BuildContext context;
DeviceSource(
{required this.devices,
required this.context,
required this.devicesBloc,
required this.role});
@override
DataRow? getRow(int index) {
if (index >= devices.length) {
return null;
}
final device = devices[index];
Map<String, dynamic> sensorMap = DeviceUtils.instance
.getDeviceSensors(context, device.status?.sensors ?? []);
String deviceState =
DeviceUtils.instance.checkStateDevice(context, device.state!);
return DataRow.byIndex(
// color: getTableRowColor(device.state!),
index: index,
cells: [
if (role == RoleEnums.USER.name || role == RoleEnums.ADMIN.name)
DataCell(
Center(
child: Row(
children: [
IconButton(
// style: ButtonStyle(),
hoverColor: Colors.black,
onPressed: () {
context.pushNamed(AppRoutes.DEVICE_UPDATE.name,
pathParameters: {'thingID': device.thingId!});
},
icon: const Icon(Icons.build, color: Colors.blue)),
IconButton(
onPressed: () async {
handleDeleteDevice(context, device.thingId!, role);
},
icon: const Icon(Icons.delete, color: Colors.red)),
],
),
),
),
DataCell(
Text(device.name!,
style: TextStyle(
color: DeviceUtils.instance
.getTableRowColor(device.state!))), onTap: () {
// log(device.thingId.toString());
context.pushNamed(AppRoutes.DEVICE_DETAIL.name,
pathParameters: {'thingID': device.thingId!});
}),
DataCell(
Center(
child: Text(deviceState,
style: TextStyle(
color: DeviceUtils.instance
.getTableRowColor(device.state!)))), onTap: () {
// log(device.thingId.toString());
context.pushNamed(AppRoutes.DEVICE_DETAIL.name,
pathParameters: {'thingID': device.thingId!});
}),
DataCell(
Center(
child: Center(
child: Text(sensorMap['sensorBattery'] + "%",
style: TextStyle(
color: DeviceUtils.instance
.getTableRowColor(device.state!))))),
onTap: () => context.pushNamed(AppRoutes.DEVICE_DETAIL.name,
pathParameters: {'thingID': device.thingId!}),
),
DataCell(
Center(
child: Center(
child: Text(sensorMap['sensorCsq'],
style: TextStyle(
color: DeviceUtils.instance
.getTableRowColor(device.state!))))),
),
DataCell(
Center(
child: Text(sensorMap['sensorTemp'],
style: TextStyle(
color: DeviceUtils.instance
.getTableRowColor(device.state!)))),
),
DataCell(
Center(
child: Text(sensorMap['sensorHum'],
style: TextStyle(
color: DeviceUtils.instance
.getTableRowColor(device.state!)))),
),
DataCell(
Center(
child: Text(sensorMap['sensorVolt'],
style: TextStyle(
color: DeviceUtils.instance
.getTableRowColor(device.state!)))),
),
],
);
}
@override
int get rowCount => devices.length;
@override
bool get isRowCountApproximate => false;
@override
int get selectedRowCount => 0;
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:sfm_app/product/constant/enums/app_route_enums.dart';
import 'package:sfm_app/product/constant/image/image_constants.dart';
class NotFoundScreen extends StatelessWidget {
const NotFoundScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
Image.asset(
ImageConstants.instance.getImage('error_page'),
fit: BoxFit.cover,
),
Positioned(
bottom: MediaQuery.of(context).size.height * 0.15,
left: MediaQuery.of(context).size.width * 0.3,
right: MediaQuery.of(context).size.width * 0.3,
child: TextButton(
onPressed: () {
context.goNamed(AppRoutes.LOGIN.name);
},
child: Text(
"Go Home".toUpperCase(),
),
),
)
],
),
);
}
}

View File

@@ -0,0 +1,79 @@
import '../devices/device_model.dart';
class DeviceWithAlias {
String? extId;
String? thingId;
String? thingKey;
List<String>? channels;
String? areaPath;
String? fvers;
String? name;
HardwareInfo? hardwareInfo;
Settings? settings;
Status? status;
DateTime? connectionTime;
int? state;
String? visibility;
DateTime? createdAt;
DateTime? updatedAt;
bool? isOwner;
String? ownerId;
String? ownerName;
String? alias;
DeviceWithAlias({
this.extId,
this.thingId,
this.thingKey,
this.channels,
this.areaPath,
this.fvers,
this.name,
this.hardwareInfo,
this.settings,
this.status,
this.connectionTime,
this.state,
this.visibility,
this.createdAt,
this.updatedAt,
this.isOwner,
this.ownerId,
this.ownerName,
this.alias,
});
DeviceWithAlias.fromJson(Map<String, dynamic> json) {
extId = json['ext_id'];
thingId = json['thing_id'];
thingKey = json['thing_key'];
if (json['channels'] != null) {
channels = [];
json['channels'].forEach((v) {
channels!.add(v);
});
}
areaPath = json['area_path'];
fvers = json['fvers'];
name = json['name'];
hardwareInfo = json['hardware_info'] != null
? HardwareInfo.fromJson(json['hardware_info'])
: null;
settings =
json['settings'] != null ? Settings.fromJson(json['settings']) : null;
status = json['status'] != null ? Status.fromJson(json['status']) : null;
connectionTime = DateTime.parse(json['connection_time']);
state = json['state'];
visibility = json['visibility'];
createdAt = DateTime.parse(json['created_at']);
updatedAt = DateTime.parse(json['updated_at']);
isOwner = json['is_owner'];
ownerId = json['owner_id'];
ownerName = json['owner_name'];
alias = json['alias'];
}
static List<DeviceWithAlias> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => DeviceWithAlias.fromJson(e)).toList();
}
}

View File

@@ -0,0 +1,92 @@
import 'dart:async';
import 'device_alias_model.dart';
import '../../product/base/bloc/base_bloc.dart';
class HomeBloc extends BlocBase {
final allDevicesAlias = StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkAllDevicesAlias =>
allDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamAllDevicesAlias =>
allDevicesAlias.stream;
final onlineDevicesAlias =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkOnlineDevicesAlias =>
onlineDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamOnlineDevicesAlias =>
onlineDevicesAlias.stream;
final offlineDevicesAlias =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkOfflineDevicesAlias =>
offlineDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamOfflineDevicesAlias =>
offlineDevicesAlias.stream;
final warningDevicesAlias =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkWarningDevicesAlias =>
warningDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamWarningDevicesAlias =>
warningDevicesAlias.stream;
final notUseDevicesAlias =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkNotUseDevicesAlias =>
notUseDevicesAlias.sink;
Stream<List<DeviceWithAlias>> get streamNotUseDevicesAlias =>
notUseDevicesAlias.stream;
final allDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkAllDevicesAliasJoined =>
allDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamAllDevicesAliasJoined =>
allDevicesAliasJoined.stream;
final onlineDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkOnlineDevicesAliasJoined =>
onlineDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamOnlineDevicesAliasJoined =>
onlineDevicesAliasJoined.stream;
final offlineDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkOfflineDevicesAliasJoined =>
offlineDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamOfflineDevicesAliasJoined =>
offlineDevicesAliasJoined.stream;
final warningDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkWarningDevicesAliasJoined =>
warningDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamWarningDevicesAliasJoined =>
warningDevicesAliasJoined.stream;
final notUseDevicesAliasJoined =
StreamController<List<DeviceWithAlias>>.broadcast();
StreamSink<List<DeviceWithAlias>> get sinkNotUseDevicesAliasJoined =>
notUseDevicesAliasJoined.sink;
Stream<List<DeviceWithAlias>> get streamNotUseDevicesAliasJoined =>
notUseDevicesAliasJoined.stream;
final countNotitication = StreamController<int>.broadcast();
StreamSink<int> get sinkCountNotitication => countNotitication.sink;
Stream<int> get streamCountNotitication => countNotitication.stream;
final ownerDevicesStatus =
StreamController<Map<String, List<DeviceWithAlias>>>.broadcast();
StreamSink<Map<String, List<DeviceWithAlias>>>
get sinkOwnerDevicesStatus => ownerDevicesStatus.sink;
Stream<Map<String, List<DeviceWithAlias>>> get streamOwnerDevicesStatus =>
ownerDevicesStatus.stream;
@override
void dispose() {}
}

View File

@@ -0,0 +1,411 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'shared/alert_card.dart';
import 'shared/warning_card.dart';
import '../../product/utils/device_utils.dart';
import 'device_alias_model.dart';
import 'shared/overview_card.dart';
import '../settings/device_notification_settings/device_notification_settings_model.dart';
import '../../product/extention/context_extention.dart';
import '../../product/services/api_services.dart';
import '../../product/services/language_services.dart';
import 'home_bloc.dart';
import '../../product/base/bloc/base_bloc.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late HomeBloc homeBloc;
APIServices apiServices = APIServices();
List<DeviceWithAlias> devices = [];
List<DeviceWithAlias> allDevicesAlias = [];
List<DeviceWithAlias> onlineDevicesAlias = [];
List<DeviceWithAlias> offlineDevicesAlias = [];
List<DeviceWithAlias> warningDevicesAlias = [];
List<DeviceWithAlias> notUseDevicesAlias = [];
List<DeviceWithAlias> allDevicesAliasJoined = [];
List<DeviceWithAlias> onlineDevicesAliasJoined = [];
List<DeviceWithAlias> offlineDevicesAliasJoined = [];
List<DeviceWithAlias> warningDevicesAliasJoined = [];
List<DeviceWithAlias> notUseDevicesAliasJoined = [];
bool isFunctionCall = false;
Timer? getAllDevicesTimer;
int notificationCount = 0;
Map<String, List<DeviceWithAlias>> ownerDevicesStatus = {};
List<String> ownerDevicesState = [];
@override
void initState() {
super.initState();
homeBloc = BlocProvider.of(context);
const duration = Duration(seconds: 20);
getOwnerAndJoinedDevices();
getAllDevicesTimer =
Timer.periodic(duration, (Timer t) => getOwnerAndJoinedDevices());
}
@override
void dispose() {
getAllDevicesTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: context.paddingLow,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Row(
children: [
Text(
appLocalization(context).notification,
style: context.titleMediumTextStyle,
),
SizedBox(width: context.lowValue),
StreamBuilder<int>(
stream: homeBloc.streamCountNotitication,
builder: (context, countSnapshot) {
return Text(
"(${countSnapshot.data ?? 0})",
style: context.titleMediumTextStyle,
);
},
)
],
),
SizedBox(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: StreamBuilder<Map<String, List<DeviceWithAlias>>>(
stream: homeBloc.streamOwnerDevicesStatus,
builder: (context, snapshot) {
if (snapshot.data?['state'] != null ||
snapshot.data?['battery'] != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (snapshot.data?['state'] != null)
...snapshot.data!['state']!
.map(
(item) => FutureBuilder<Widget>(
future: warningCard(
context, apiServices, item),
builder: (context, warningCardSnapshot) {
if (warningCardSnapshot.hasData) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
maxHeight: 260,
),
child: warningCardSnapshot.data!,
);
} else {
return const SizedBox.shrink();
}
},
),
)
.toList(),
if (snapshot.data?['battery'] != null)
...snapshot.data!['battery']!
.map(
(batteryItem) => FutureBuilder<Widget>(
future: notificationCard(
context,
"lowBattery",
"Cảnh báo pin yếu",
batteryItem.name!,
batteryItem.areaPath!,
),
builder: (context, warningCardSnapshot) {
if (warningCardSnapshot.hasData) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
maxHeight: 260,
),
child: warningCardSnapshot.data!,
);
} else {
return const SizedBox.shrink();
}
},
),
)
.toList(),
]);
} else {
return Padding(
padding: context.paddingMedium,
child: Center(
child: Row(
children: [
const Icon(
Icons.check_circle_outline_rounded,
size: 40,
color: Colors.green,
),
SizedBox(width: context.lowValue),
Text(
appLocalization(context)
.notification_description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
textAlign: TextAlign.start,
),
],
),
),
);
}
},
),
),
),
StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamAllDevicesAlias,
initialData: allDevicesAlias,
builder: (context, allDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamOnlineDevicesAlias,
initialData: onlineDevicesAlias,
builder: (context, onlineDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamOfflineDevicesAlias,
initialData: offlineDevicesAlias,
builder: (context, deviceDashboardSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamWarningDevicesAlias,
initialData: warningDevicesAlias,
builder: (context, warningDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamNotUseDevicesAlias,
initialData: notUseDevicesAlias,
builder: (context, notUseSnapshot) {
return OverviewCard(
isOwner: true,
total: allDeviceSnapshot.data!.length,
active: onlineDeviceSnapshot.data!.length,
inactive:
deviceDashboardSnapshot.data!.length,
warning: warningDeviceSnapshot.data!.length,
unused: notUseSnapshot.data!.length,
);
},
);
},
);
},
);
},
);
},
),
SizedBox(height: context.lowValue),
StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamAllDevicesAliasJoined,
initialData: allDevicesAliasJoined,
builder: (context, allDeviceSnapshot) {
if (allDeviceSnapshot.data?.isEmpty ?? true) {
return const SizedBox.shrink();
}
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamOnlineDevicesAliasJoined,
initialData: onlineDevicesAliasJoined,
builder: (context, onlineDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamOfflineDevicesAliasJoined,
initialData: offlineDevicesAliasJoined,
builder: (context, deviceDashboardSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamWarningDevicesAliasJoined,
initialData: warningDevicesAliasJoined,
builder: (context, warningDeviceSnapshot) {
return StreamBuilder<List<DeviceWithAlias>>(
stream: homeBloc.streamNotUseDevicesAliasJoined,
initialData: notUseDevicesAliasJoined,
builder: (context, notUseSnapshot) {
return OverviewCard(
isOwner: false,
total: allDeviceSnapshot.data!.length,
active: onlineDeviceSnapshot.data!.length,
inactive:
deviceDashboardSnapshot.data!.length,
warning: warningDeviceSnapshot.data!.length,
unused: notUseSnapshot.data!.length,
);
},
);
},
);
},
);
},
);
},
),
],
),
),
);
}
void getOwnerAndJoinedDevices() async {
String response = await apiServices.getDashBoardDevices();
final data = jsonDecode(response);
List<dynamic> result = data["items"];
devices = DeviceWithAlias.fromJsonDynamicList(result);
getOwnerDeviceState(devices);
getDevicesStatusAlias(devices);
checkSettingdevice(devices);
}
void getOwnerDeviceState(List<DeviceWithAlias> allDevices) async {
List<DeviceWithAlias> ownerDevices = [];
for (var device in allDevices) {
if (device.isOwner!) {
ownerDevices.add(device);
}
}
if (ownerDevicesState.isEmpty ||
ownerDevicesState.length < devices.length) {
ownerDevicesState.clear();
ownerDevicesStatus.clear();
homeBloc.sinkOwnerDevicesStatus.add(ownerDevicesStatus);
int count = 0;
for (var device in ownerDevices) {
Map<String, dynamic> sensorMap = DeviceUtils.instance
.getDeviceSensors(context, device.status?.sensors ?? []);
if (device.state == 1 || device.state == 3) {
ownerDevicesStatus["state"] ??= [];
ownerDevicesStatus["state"]!.add(device);
homeBloc.sinkOwnerDevicesStatus.add(ownerDevicesStatus);
count++;
}
if (sensorMap['sensorBattery'] !=
appLocalization(context).no_data_message) {
if (double.parse(sensorMap['sensorBattery']) <= 20) {
ownerDevicesStatus['battery'] ??= [];
ownerDevicesStatus['battery']!.add(device);
homeBloc.sinkOwnerDevicesStatus.add(ownerDevicesStatus);
count++;
}
}
notificationCount = count;
homeBloc.sinkCountNotitication.add(notificationCount);
}
}
}
void getDevicesStatusAlias(List<DeviceWithAlias> devices) async {
clearAllDeviceStatusAlias();
for (DeviceWithAlias device in devices) {
if (device.isOwner == true) {
allDevicesAlias.add(device);
if (device.state! == 0 || device.state! == 1) {
onlineDevicesAlias.add(device);
homeBloc.sinkOnlineDevicesAlias.add(onlineDevicesAlias);
}
if (device.state! == -1) {
offlineDevicesAlias.add(device);
homeBloc.sinkOfflineDevicesAlias.add(offlineDevicesAlias);
}
if (device.state! == 1) {
warningDevicesAlias.add(device);
homeBloc.sinkWarningDevicesAlias.add(warningDevicesAlias);
}
if (device.state! == -2) {
notUseDevicesAlias.add(device);
homeBloc.sinkNotUseDevicesAlias.add(notUseDevicesAlias);
}
} else {
allDevicesAliasJoined.add(device);
if (device.state! == 0 || device.state! == 1) {
onlineDevicesAliasJoined.add(device);
homeBloc.sinkOnlineDevicesAliasJoined.add(onlineDevicesAliasJoined);
}
if (device.state! == -1) {
offlineDevicesAliasJoined.add(device);
homeBloc.sinkOfflineDevicesAliasJoined.add(offlineDevicesAliasJoined);
}
if (device.state! == 1) {
warningDevicesAliasJoined.add(device);
homeBloc.sinkWarningDevicesAliasJoined.add(warningDevicesAliasJoined);
}
if (device.state! == -2) {
notUseDevicesAliasJoined.add(device);
homeBloc.sinkNotUseDevicesAliasJoined.add(notUseDevicesAliasJoined);
}
}
}
checkSettingdevice(allDevicesAliasJoined);
homeBloc.sinkAllDevicesAlias.add(allDevicesAlias);
homeBloc.sinkAllDevicesAliasJoined.add(allDevicesAliasJoined);
}
void checkSettingdevice(List<DeviceWithAlias> devices) async {
if (isFunctionCall) {
} else {
String? response =
await apiServices.getAllSettingsNotificationOfDevices();
if (response != "") {
final data = jsonDecode(response);
final result = data['data'];
// log("Data ${DeviceNotificationSettings.mapFromJson(jsonDecode(data)).values.toList()}");
List<DeviceNotificationSettings> list =
DeviceNotificationSettings.mapFromJson(result).values.toList();
// log("List: $list");
Set<String> thingIdsInList =
list.map((device) => device.thingId!).toSet();
for (var device in devices) {
if (!thingIdsInList.contains(device.thingId)) {
log("Device with Thing ID ${device.thingId} is not in the notification settings list.");
await apiServices.setupDeviceNotification(
device.thingId!, device.name!);
} else {
log("All devives are in the notification settings list.");
}
}
} else {
log("apiServices: getAllSettingsNotificationofDevices error!");
}
}
isFunctionCall = true;
}
void clearAllDeviceStatusAlias() {
allDevicesAlias.clear();
homeBloc.sinkAllDevicesAlias.add(allDevicesAlias);
onlineDevicesAlias.clear();
homeBloc.sinkOnlineDevicesAlias.add(onlineDevicesAlias);
offlineDevicesAlias.clear();
homeBloc.sinkOfflineDevicesAlias.add(offlineDevicesAlias);
warningDevicesAlias.clear();
homeBloc.sinkWarningDevicesAlias.add(warningDevicesAlias);
notUseDevicesAlias.clear();
homeBloc.sinkNotUseDevicesAlias.add(notUseDevicesAlias);
allDevicesAliasJoined.clear();
homeBloc.sinkAllDevicesAliasJoined.add(allDevicesAliasJoined);
onlineDevicesAliasJoined.clear();
homeBloc.sinkOnlineDevicesAliasJoined.add(onlineDevicesAliasJoined);
offlineDevicesAliasJoined.clear();
homeBloc.sinkOfflineDevicesAliasJoined.add(offlineDevicesAliasJoined);
warningDevicesAliasJoined.clear();
homeBloc.sinkWarningDevicesAliasJoined.add(warningDevicesAliasJoined);
notUseDevicesAliasJoined.clear();
homeBloc.sinkNotUseDevicesAliasJoined.add(notUseDevicesAliasJoined);
}
}

View File

@@ -0,0 +1,130 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import '../../../product/constant/image/image_constants.dart';
import '../../../product/extention/context_extention.dart';
import '../../../product/services/language_services.dart';
import '../../../product/utils/device_utils.dart';
import '../../../product/constant/icon/icon_constants.dart';
Future<Widget> notificationCard(
BuildContext context,
String notiticationType,
String notificationTitle,
String notificationDevicename,
String notificationLocation) async {
String location = await DeviceUtils.instance
.getFullDeviceLocation(context, notificationLocation);
String path = "";
DateTime time = DateTime.now();
if (notiticationType == "lowBattery") {
path = ImageConstants.instance.getImage("low_battery");
}
return Card(
child: Padding(
padding: context.paddingLow,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
child: Text(
notificationTitle,
style: const TextStyle(
letterSpacing: 1,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 250, 84, 34),
fontSize: 18,
),
),
),
SizedBox(height: context.lowValue),
SizedBox(
child: Text(
"${appLocalization(context).device_title} $notificationDevicename",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
textAlign: TextAlign.start,
),
),
],
),
SizedBox(
height: context.dynamicWidth(0.15),
width: context.dynamicWidth(0.15),
child: Image.asset(path),
),
],
),
SizedBox(height: context.lowValue),
Row(
children: [
IconConstants.instance
.getMaterialIcon(Icons.location_on_outlined),
SizedBox(
width: context.lowValue,
),
Expanded(
child: Text(
location,
style: const TextStyle(fontSize: 15),
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
textAlign: TextAlign.start,
),
),
],
),
SizedBox(height: context.lowValue),
Row(
children: [
IconConstants.instance.getMaterialIcon(Icons.schedule),
SizedBox(
width: context.lowValue,
),
Expanded(
child: Text(
time.toString(),
style: const TextStyle(fontSize: 15),
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
textAlign: TextAlign.start,
),
),
],
),
Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.blueAccent)),
onPressed: () {},
child: Text(
appLocalization(context).detail_message,
style: const TextStyle(
color: Colors.white,
),
),
),
),
],
),
),
);
}

View File

@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:sfm_app/feature/home/shared/status_card.dart';
import 'package:sfm_app/product/extention/context_extention.dart';
import 'package:sfm_app/product/services/language_services.dart';
class OverviewCard extends StatelessWidget {
final bool isOwner;
final int total;
final int active;
final int inactive;
final int warning;
final int unused;
const OverviewCard(
{super.key,
required this.isOwner,
required this.total,
required this.active,
required this.inactive,
required this.warning,
required this.unused});
@override
Widget build(BuildContext context) {
return Card(
margin: context.paddingLow,
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: Padding(
padding: context.paddingNormal,
child: Column(
children: [
Text(
isOwner
? appLocalization(context).overview_message
: appLocalization(context).interfamily_page_name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: context.normalValue),
Column(
children: [
StatusCard(
label: appLocalization(context).total_nof_devices_message,
count: total,
color: Colors.blue,
),
StatusCard(
label: appLocalization(context).active_devices_message,
count: active,
color: Colors.green,
),
StatusCard(
label: appLocalization(context).inactive_devices_message,
count: inactive,
color: Colors.grey,
),
StatusCard(
label: appLocalization(context).warning_devices_message,
count: warning,
color: Colors.orange,
),
StatusCard(
label: appLocalization(context).unused_devices_message,
count: unused,
color: Colors.yellow,
),
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
class StatusCard extends StatelessWidget {
final String label;
final int count;
final Color color;
const StatusCard(
{super.key,
required this.label,
required this.count,
required this.color});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: color,
width: 1,
),
),
padding: const EdgeInsets.all(15),
margin: const EdgeInsets.symmetric(vertical: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontSize: 18)),
Text(
count.toString(),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,248 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:maps_launcher/maps_launcher.dart';
import '../device_alias_model.dart';
import '../../../product/constant/icon/icon_constants.dart';
import '../../../product/constant/image/image_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/shared/shared_snack_bar.dart';
Future<Widget> warningCard(
BuildContext context, APIServices apiServices, DeviceWithAlias item) async {
Color backgroundColor = Colors.blue;
Color textColor = Colors.white;
String message = "";
String fullLocation =
await DeviceUtils.instance.getFullDeviceLocation(context, item.areaPath!);
String time = "";
for (var sensor in item.status!.sensors!) {
if (sensor.name! == "11") {
DateTime dateTime =
DateTime.fromMillisecondsSinceEpoch((sensor.time!) * 1000);
time = DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime);
}
}
if (item.state! == 3) {
backgroundColor = Colors.grey;
textColor = Colors.black;
message = appLocalization(context).in_progress_message;
} else if (item.state! == 2) {
backgroundColor = const Color.fromARGB(255, 6, 138, 72);
textColor = const Color.fromARGB(255, 255, 255, 255);
message = appLocalization(context).gf_in_firefighting_message;
} else if (item.state! == 1) {
backgroundColor = const Color.fromARGB(255, 250, 63, 63);
textColor = Colors.white;
message = appLocalization(context).button_fake_fire_message;
} else {
backgroundColor = Colors.black;
textColor = Colors.white;
message = appLocalization(context).disconnect_message_uppercase;
}
return Card(
// color: Color.fromARGB(255, 208, 212, 217),
child: Padding(
padding: context.paddingLow,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
child: Text(
appLocalization(context).smoke_detecting_message,
style: const TextStyle(
letterSpacing: 1,
fontWeight: FontWeight.bold,
color: Colors.red,
fontSize: 18,
),
),
),
SizedBox(height: context.lowValue),
SizedBox(
child: Text(
"${appLocalization(context).device_title}: ${item.name}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
textAlign: TextAlign.start,
),
),
],
),
SizedBox(
height: context.dynamicWidth(0.15),
width: context.dynamicWidth(0.15),
child: Image.asset(
ImageConstants.instance.getImage("fire_warning")),
)
],
),
SizedBox(height: context.lowValue),
Row(
children: [
IconConstants.instance
.getMaterialIcon(Icons.location_on_outlined),
SizedBox(
width: context.lowValue,
),
Expanded(
child: Text(
fullLocation,
style: const TextStyle(fontSize: 15),
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
textAlign: TextAlign.start,
),
),
],
),
SizedBox(height: context.lowValue),
Row(
children: [
IconConstants.instance.getMaterialIcon(Icons.schedule),
SizedBox(
width: context.lowValue,
),
Expanded(
child: Text(
time,
style: const TextStyle(fontSize: 15),
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
textAlign: TextAlign.start,
),
),
],
),
SizedBox(
height: context.lowValue,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton.outlined(
onPressed: () async => {},
// displayListOfFireStationPhoneNumbers(testDevice),
icon: IconConstants.instance.getMaterialIcon(Icons.call),
iconSize: 25,
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.blue[300]!),
),
),
const SizedBox(width: 10),
IconButton.outlined(
onPressed: () async {
String markerLabel = "Destination";
MapsLauncher.launchCoordinates(
double.parse(item.settings!.latitude!),
double.parse(item.settings!.longitude!),
markerLabel);
},
icon: const Icon(Icons.directions),
iconSize: 25,
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.blue[300]!),
),
),
SizedBox(width: context.mediumValue),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(backgroundColor)),
onPressed: () async {
if (message ==
appLocalization(context).button_fake_fire_message) {
await showDialog(
context: context,
builder: (context) => AlertDialog(
icon: const Icon(Icons.warning),
iconColor: Colors.red,
title: Text(appLocalization(context)
.confirm_fake_fire_message),
content: Text(appLocalization(context)
.confirm_fake_fire_body),
actions: [
TextButton(
onPressed: () async {
int statusCode = await apiServices
.confirmFakeFireByUser(item.thingId!);
if (statusCode == 200) {
showNoIconTopSnackBar(
context,
appLocalization(context)
.notification_confirm_fake_fire_success,
Colors.green,
Colors.white);
} else {
showNoIconTopSnackBar(
context,
appLocalization(context)
.notification_confirm_fake_fire_failed,
Colors.red,
Colors.red);
}
Navigator.of(context).pop();
},
child: Text(
appLocalization(context)
.confirm_fake_fire_sure_message,
style: const TextStyle(color: Colors.red)),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(appLocalization(context)
.cancel_button_content),
),
],
),
);
} else {
showNoIconTopSnackBar(
context,
appLocalization(context).let_PCCC_handle_message,
Colors.orange,
Colors.white);
}
},
child: Text(
message,
style: TextStyle(color: textColor),
),
),
),
),
],
),
],
),
),
);
}

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))
],
);
});
}

View File

@@ -0,0 +1,82 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:sfm_app/feature/devices/device_model.dart';
import 'package:sfm_app/product/base/bloc/base_bloc.dart';
import 'package:sfm_app/product/services/api_services.dart';
import 'package:sfm_app/product/utils/date_time_utils.dart';
import '../../product/utils/device_utils.dart';
import 'device_logs_model.dart';
class DeviceLogsBloc extends BlocBase {
APIServices apiServices = APIServices();
final fromDate = StreamController<String>.broadcast();
StreamSink<String> get sinkFromDate => fromDate.sink;
Stream<String> get streamFromDate => fromDate.stream;
final hasMore = StreamController<bool>.broadcast();
StreamSink<bool> get sinkHasMore => hasMore.sink;
Stream<bool> get streamHasMore => hasMore.stream;
final allDevices = StreamController<List<Device>>.broadcast();
StreamSink<List<Device>> get sinkAllDevices => allDevices.sink;
Stream<List<Device>> get streamAllDevices => allDevices.stream;
final sensors = StreamController<List<SensorLogs>>.broadcast();
StreamSink<List<SensorLogs>> get sinkSensors => sensors.sink;
Stream<List<SensorLogs>> get streamSensors => sensors.stream;
@override
void dispose() {}
void getAllDevices() async {
String body = await apiServices.getOwnerDevices();
if (body != "") {
final data = jsonDecode(body);
List<dynamic> items = data['items'];
List<Device> originalDevices = Device.fromJsonDynamicList(items);
List<Device> devices =
DeviceUtils.instance.sortDeviceByState(originalDevices);
sinkAllDevices.add(devices);
}
}
void getDeviceLogByThingID(
int offset,
String thingID,
DateTime fromDate,
List<SensorLogs> sensors,
) async {
log("SensorLength: ${sensors.length}");
String fromDateString =
DateTimeUtils.instance.formatDateTimeToString(fromDate);
String now = DateTimeUtils.instance.formatDateTimeToString(DateTime.now());
// List<SensorLogs> sensors = [];
Map<String, dynamic> params = {
'thing_id': thingID,
'from': fromDateString,
'to': now,
'limit': '30',
"offset": offset.toString(),
"asc": "true"
};
final body = await apiServices.getLogsOfDevice(thingID, params);
if (body != "") {
final data = jsonDecode(body);
DeviceLog devicesListLog = DeviceLog.fromJson(data);
if (devicesListLog.sensors!.isEmpty) {
bool hasMore = false;
sinkHasMore.add(hasMore);
}
if (devicesListLog.sensors!.isNotEmpty) {
for (var sensor in devicesListLog.sensors!) {
sensors.add(sensor);
}
}
sinkSensors.add(sensors);
}
}
}

View File

@@ -0,0 +1,66 @@
class DeviceLog {
String? thingId;
DateTime? from;
DateTime? to;
int? total;
int? offset;
List<SensorLogs>? sensors;
DeviceLog({
this.thingId,
this.from,
this.to,
this.total,
this.offset,
this.sensors,
});
DeviceLog.fromJson(Map<String, dynamic> json) {
thingId = json["thing_id"];
from = DateTime.parse(json["from"]);
to = DateTime.parse(json["to"]);
total = json["total"];
offset = json["offset"];
if (json['sensors'] != null) {
sensors = [];
json['sensors'].forEach((v) {
sensors!.add(SensorLogs.fromJson(v));
});
}
}
}
class SensorLogs {
String? name;
int? value;
int? time;
LogDetail? detail;
SensorLogs({
this.name,
this.value,
this.time,
this.detail,
});
SensorLogs.fromJson(Map<String, dynamic> json) {
name = json["n"];
value = json["v"];
time = json["t"];
detail = json["x"] != null ? LogDetail.fromJson(json["x"]) : null;
}
}
class LogDetail {
String? username;
String? note;
LogDetail({
this.username,
this.note,
});
LogDetail.fromJson(Map<String, dynamic> json) {
username = json["username"];
note = json["note"];
}
}

View File

@@ -0,0 +1,295 @@
import 'dart:developer';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:sfm_app/feature/devices/device_model.dart';
import 'package:sfm_app/feature/log/device_logs_bloc.dart';
import 'package:sfm_app/product/constant/icon/icon_constants.dart';
import 'package:sfm_app/product/extention/context_extention.dart';
import 'package:sfm_app/product/services/language_services.dart';
import 'package:sfm_app/product/shared/shared_snack_bar.dart';
import 'package:sfm_app/product/utils/date_time_utils.dart';
import 'package:sfm_app/product/utils/device_utils.dart';
import '../../product/base/bloc/base_bloc.dart';
import 'device_logs_model.dart';
class DeviceLogsScreen extends StatefulWidget {
const DeviceLogsScreen({super.key});
@override
State<DeviceLogsScreen> createState() => _DeviceLogsScreenState();
}
class _DeviceLogsScreenState extends State<DeviceLogsScreen> {
TextEditingController fromDate = TextEditingController();
String fromDateApi = '';
DateTime? dateTime;
String thingID = "";
late DeviceLogsBloc deviceLogsBloc;
List<Device> allDevices = [];
List<SensorLogs> sensors = [];
int offset = 0;
final controller = ScrollController();
bool hasMore = true;
@override
void initState() {
super.initState();
deviceLogsBloc = BlocProvider.of(context);
controller.addListener(
() {
if (controller.position.maxScrollExtent == controller.offset) {
offset += 30;
deviceLogsBloc.getDeviceLogByThingID(
offset, thingID, dateTime!, sensors);
}
},
);
}
@override
void dispose() {
controller.dispose();
sensors.clear();
deviceLogsBloc.sinkSensors.add(sensors);
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Device>>(
stream: deviceLogsBloc.streamAllDevices,
builder: (context, allDevicesSnapshot) {
if (allDevicesSnapshot.data?[0].thingId == null) {
deviceLogsBloc.getAllDevices();
return const Center(
child: CircularProgressIndicator(),
);
} else {
return StreamBuilder<List<SensorLogs>>(
stream: deviceLogsBloc.streamSensors,
initialData: sensors,
builder: (context, sensorsSnapshot) {
return Padding(
padding: context.paddingLow,
child: Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
DropdownButtonFormField2<String>(
isExpanded: true,
decoration: InputDecoration(
contentPadding: context.paddingLowVertical,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
),
),
hint: Text(
appLocalization(context)
.choose_device_dropdownButton,
style: const TextStyle(
fontSize: 14,
),
),
items: allDevicesSnapshot.data?.isNotEmpty ?? false
? allDevicesSnapshot.data!
.map(
(device) => DropdownMenuItem<String>(
value: device.thingId,
child: Text(
device.name!,
style: const TextStyle(
fontSize: 14,
),
),
),
)
.toList()
: [],
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.only(right: 8),
),
onChanged: (value) {
thingID = value!;
setState(() {});
},
onSaved: (value) {
log("On Saved");
},
iconStyleData: const IconStyleData(
icon: Icon(
Icons.arrow_drop_down,
),
iconSize: 24,
),
dropdownStyleData: DropdownStyleData(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(horizontal: 16),
),
),
Padding(
padding: context.paddingLowVertical,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: context.dynamicWidth(0.6),
child: TextField(
controller: fromDate,
decoration: InputDecoration(
icon:
IconConstants.instance.getMaterialIcon(
Icons.calendar_month_outlined,
),
labelText: appLocalization(context)
.choose_date_start_datePicker,
),
readOnly: true,
onTap: () async {
DateTime? datePicker = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2023),
lastDate: DateTime(2050),
);
dateTime = datePicker;
if (datePicker != null) {
fromDateApi = DateTimeUtils.instance
.formatDateTimeToString(datePicker);
setState(
() {
fromDate.text =
DateFormat('yyyy-MM-dd')
.format(datePicker);
},
);
}
},
),
),
Center(
child: TextButton.icon(
style: const ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.green),
),
onPressed: () {
if (fromDateApi.isEmpty) {
showNoIconTopSnackBar(
context,
appLocalization(context)
.notification_enter_all_inf,
Colors.red,
Colors.white,
);
} else {
deviceLogsBloc.getDeviceLogByThingID(
offset, thingID, dateTime!, sensors);
}
log("ThingID: $thingID");
log("From Date: ${DateTimeUtils.instance.formatDateTimeToString(dateTime!)}");
},
icon: IconConstants.instance
.getMaterialIcon(Icons.search),
label: Text(
appLocalization(context)
.find_button_content,
),
),
),
],
),
),
Divider(
height: context.lowValue,
),
Expanded(
child: sensorsSnapshot.data?.isEmpty ?? false
? Center(
child: Text(
appLocalization(context).no_data_message),
)
: RefreshIndicator(
onRefresh: refresh,
child: ListView.builder(
controller: controller,
itemCount: sensorsSnapshot.data!.length + 1,
itemBuilder: (context, index) {
if (index <
sensorsSnapshot.data!.length) {
return logDetail(
sensorsSnapshot.data![index],
index,
);
} else {
return Padding(
padding: context.paddingLow,
child: StreamBuilder<bool>(
stream:
deviceLogsBloc.streamHasMore,
builder:
(context, hasMoreSnapshot) {
return Center(
child: hasMoreSnapshot.data ??
hasMore
? const CircularProgressIndicator()
: Text(
appLocalization(context)
.main_no_data),
);
},
),
);
}
},
),
),
),
],
),
),
),
);
},
);
}
},
);
}
Widget logDetail(SensorLogs sensor, int index) {
return Column(
children: [
ListTile(
subtitle:
Text(DeviceUtils.instance.getDeviceSensorsLog(context, sensor)),
// leading: const Icon(Icons.sensors_outlined),
leading: Text(index.toString()),
title: Text(
DateTimeUtils.instance
.convertCurrentMillisToDateTimeString(sensor.time ?? 0),
),
),
const Divider(
thickness: 0.5,
indent: 50,
endIndent: 100,
),
],
);
}
Future<void> refresh() async {
offset = 0;
sensors.clear();
deviceLogsBloc.sensors.add(sensors);
hasMore = true;
deviceLogsBloc.sinkHasMore.add(hasMore);
deviceLogsBloc.getDeviceLogByThingID(offset, thingID, dateTime!, sensors);
}
}

View File

@@ -0,0 +1,39 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sfm_app/product/base/bloc/base_bloc.dart';
import '../bell/bell_model.dart';
class MainBloc extends BlocBase {
final bellBloc = StreamController<Bell>.broadcast();
StreamSink<Bell> get sinkBellBloc => bellBloc.sink;
Stream<Bell> get streamBellBloc => bellBloc.stream;
final title = StreamController<String>.broadcast();
StreamSink<String> get sinkTitle => title.sink;
Stream<String> get streamTitle => title.stream;
final role = StreamController<String>.broadcast();
StreamSink<String> get sinkRole => role.sink;
Stream<String> get streamRole => role.stream;
final language = StreamController<Locale?>.broadcast();
StreamSink<Locale?> get sinkLanguage => language.sink;
Stream<Locale?> get streamLanguage => language.stream;
final themeMode = StreamController<bool>.broadcast();
StreamSink<bool> get sinkThemeMode => themeMode.sink;
Stream<bool> get streamThemeMode => themeMode.stream;
final isVNIcon = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsVNIcon => isVNIcon.sink;
Stream<bool> get streamIsVNIcon => isVNIcon.stream;
final currentPageIndex = StreamController<int>.broadcast();
StreamSink<int> get sinkCurrentPageIndex => currentPageIndex.sink;
Stream<int> get streamCurrentPageIndex => currentPageIndex.stream;
@override
void dispose() {}
}

View File

@@ -0,0 +1,393 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:developer';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:badges/badges.dart' as badges;
import 'package:sfm_app/feature/home/home_bloc.dart';
import 'package:sfm_app/product/constant/app/app_constants.dart';
import 'package:sfm_app/product/constant/enums/app_route_enums.dart';
import 'package:sfm_app/product/constant/enums/role_enums.dart';
import '../devices/devices_manager_bloc.dart';
import '../devices/devices_manager_screen.dart';
import '../home/home_screen.dart';
import '../inter_family/inter_family_bloc.dart';
import '../inter_family/inter_family_screen.dart';
import '../log/device_logs_bloc.dart';
import '../log/device_logs_screen.dart';
import 'main_bloc.dart';
import '../map/map_bloc.dart';
import '../map/map_screen.dart';
import '../../product/base/bloc/base_bloc.dart';
import '../../product/constant/enums/app_theme_enums.dart';
import '../../product/extention/context_extention.dart';
import '../../product/services/api_services.dart';
import '../../main.dart';
import '../../product/constant/icon/icon_constants.dart';
import '../../product/constant/lang/language_constants.dart';
import '../../product/services/language_services.dart';
import '../../product/theme/theme_notifier.dart';
import '../bell/bell_model.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
APIServices apiServices = APIServices();
late MainBloc mainBloc;
bool isVN = true;
bool isLight = true;
String role = 'Unknown';
String titlePage = "Page Title";
int currentPageIndex = 0;
final _badgeKey = GlobalKey();
Bell bell = Bell();
void initialCheck() async {
role = await apiServices.getUserRole();
mainBloc.sinkRole.add(role);
String language = await apiServices.checkLanguage();
String theme = await apiServices.checkTheme();
if (language == LanguageConstants.VIETNAM) {
isVN = true;
} else {
isVN = false;
}
if (theme == AppThemes.LIGHT.name) {
isLight = true;
} else {
isLight = false;
}
mainBloc.sinkIsVNIcon.add(isVN);
mainBloc.sinkThemeMode.add(isLight);
log("role: $role");
}
@override
void initState() {
super.initState();
mainBloc = BlocProvider.of(context);
initialCheck();
getBellNotification();
}
@override
Widget build(BuildContext context) {
ThemeNotifier themeNotifier = context.watch<ThemeNotifier>();
checkSelectedIndex(currentPageIndex);
List<Widget> userDestinations = [
NavigationDestination(
selectedIcon: IconConstants.instance.getMaterialIcon(Icons.home),
icon: IconConstants.instance.getMaterialIcon(Icons.home_outlined),
label: appLocalization(context).home_page_destination,
tooltip: appLocalization(context).home_page_destination,
),
NavigationDestination(
selectedIcon: IconConstants.instance.getMaterialIcon(Icons.settings),
icon: IconConstants.instance.getMaterialIcon(Icons.settings_outlined),
label: appLocalization(context).manager_page_destination,
tooltip: appLocalization(context).device_manager_page_name,
),
NavigationDestination(
selectedIcon: IconConstants.instance.getMaterialIcon(Icons.location_on),
icon:
IconConstants.instance.getMaterialIcon(Icons.location_on_outlined),
label: appLocalization(context).map_page_destination,
tooltip: appLocalization(context).map_page_destination,
),
NavigationDestination(
// selectedIcon: IconConstants.instance.getMaterialIcon(Icons.histor),
icon: IconConstants.instance.getMaterialIcon(Icons.history_rounded),
label: appLocalization(context).history_page_destination,
tooltip: appLocalization(context).history_page_destination_tooltip,
),
NavigationDestination(
selectedIcon: IconConstants.instance.getMaterialIcon(Icons.group),
icon: IconConstants.instance.getMaterialIcon(Icons.group_outlined),
label: appLocalization(context).group_page_destination,
tooltip: appLocalization(context).group_page_destination_tooltip,
),
];
List<Widget> userBody = [
BlocProvider(child: const HomeScreen(), blocBuilder: () => HomeBloc()),
BlocProvider(
child: const DevicesManagerScreen(),
blocBuilder: () => DevicesManagerBloc()),
BlocProvider(
child: const MapScreen(),
blocBuilder: () => MapBloc(),
),
BlocProvider(
child: const DeviceLogsScreen(),
blocBuilder: () => DeviceLogsBloc(),
),
BlocProvider(
child: const InterFamilyScreen(),
blocBuilder: () => InterFamilyBloc(),
),
];
List<Widget> modDestinations = [
NavigationDestination(
selectedIcon: IconConstants.instance.getMaterialIcon(Icons.home),
icon: IconConstants.instance.getMaterialIcon(Icons.home_outlined),
label: appLocalization(context).home_page_destination,
tooltip: appLocalization(context).home_page_destination,
),
NavigationDestination(
selectedIcon: IconConstants.instance.getMaterialIcon(Icons.settings),
icon: IconConstants.instance.getMaterialIcon(Icons.settings_outlined),
label: appLocalization(context).manager_page_destination,
tooltip: appLocalization(context).device_manager_page_name,
),
NavigationDestination(
selectedIcon: IconConstants.instance.getMaterialIcon(Icons.location_on),
icon:
IconConstants.instance.getMaterialIcon(Icons.location_on_outlined),
label: appLocalization(context).map_page_destination,
tooltip: appLocalization(context).map_page_destination,
),
];
List<Widget> modBody = [
BlocProvider(child: const HomeScreen(), blocBuilder: () => HomeBloc()),
BlocProvider(
child: const DevicesManagerScreen(),
blocBuilder: () => DevicesManagerBloc()),
BlocProvider(
child: const MapScreen(),
blocBuilder: () => MapBloc(),
),
];
return StreamBuilder<String>(
stream: mainBloc.streamRole,
initialData: role,
builder: (context, roleSnapshot) {
return StreamBuilder<int>(
stream: mainBloc.streamCurrentPageIndex,
initialData: currentPageIndex,
builder: (context, indexSnapshot) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
centerTitle: true,
title: StreamBuilder<String>(
stream: mainBloc.streamTitle,
initialData: titlePage,
builder: (context, titleSnapshot) {
return Text(
titleSnapshot.data ?? ApplicationConstants.APP_NAME,
);
},
),
actions: [
StreamBuilder<bool>(
stream: mainBloc.streamThemeMode,
initialData: isLight,
builder: (context, themeModeSnapshot) {
return IconButton(
onPressed: () {
themeNotifier.changeTheme();
isLight = !isLight;
mainBloc.sinkThemeMode.add(isLight);
},
icon: Icon(
themeModeSnapshot.data ?? isLight
? Icons.light_mode_outlined
: Icons.dark_mode_outlined,
),
);
},
),
StreamBuilder<bool>(
stream: mainBloc.streamIsVNIcon,
initialData: isVN,
builder: (context, isVnSnapshot) {
return IconButton(
onPressed: () async {
log("Locale: ${LanguageServices().getLocale()}");
Locale locale = await LanguageServices().setLocale(
isVN
? LanguageConstants.ENGLISH
: LanguageConstants.VIETNAM);
MyApp.setLocale(context, locale);
isVN = !isVN;
mainBloc.sinkIsVNIcon.add(isVN);
},
icon: Image.asset(
IconConstants.instance.getIcon(
isVnSnapshot.data ?? isVN
? 'vi_icon'
: 'en_icon'),
height: 24,
width: 24,
),
);
},
),
StreamBuilder<Bell>(
stream: mainBloc.streamBellBloc,
builder: (context, bellSnapshot) {
return checkStatus(bellSnapshot.data?.items ?? [])
? IconButton(
onPressed: () {
context.pushNamed(AppRoutes.BELL.name);
},
icon: const Icon(
Icons.notifications,
),
)
: GestureDetector(
child: badges.Badge(
badgeStyle: const badges.BadgeStyle(
shape: badges.BadgeShape.twitter,
),
key: _badgeKey,
badgeContent: const Icon(
CupertinoIcons.circle_filled,
color: Colors.red,
size: 5,
),
badgeAnimation:
const badges.BadgeAnimation.slide(
animationDuration:
Duration(milliseconds: 200),
colorChangeAnimationDuration:
Duration(seconds: 1),
loopAnimation: false,
curve: Curves.decelerate,
colorChangeAnimationCurve: Curves.easeInCirc,
),
showBadge: true,
// ignorePointer: false,
child: const Icon(
Icons.notifications,
size: 30,
),
),
onTap: () {
context.pushNamed(AppRoutes.BELL.name);
},
);
},
),
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),
),
),
icon: const Icon(Icons.more_vert),
itemBuilder: (context) {
return [
PopupMenuItem(
value: ApplicationConstants.SETTINGS_PATH,
onTap: () {
context.pushNamed(AppRoutes.SETTINGS.name);
},
child: Row(
children: [
const Icon(Icons.person),
const SizedBox(width: 5),
Text(appLocalization(context).profile_icon_title)
],
),
),
PopupMenuItem(
value: ApplicationConstants.LOGOUT_PATH,
onTap: () {
Future.delayed(
const Duration(milliseconds: 200),
() async {
await apiServices.logOut(context);
},
);
},
child: Row(
children: [
const Icon(Icons.logout),
const SizedBox(width: 5),
Text(
appLocalization(context).log_out,
),
],
),
),
];
},
)
],
),
bottomNavigationBar: Container(
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(50)),
padding: context.paddingLow,
child: NavigationBar(
onDestinationSelected: (index) {
currentPageIndex = index;
mainBloc.sinkCurrentPageIndex.add(currentPageIndex);
checkSelectedIndex(currentPageIndex);
},
selectedIndex: indexSnapshot.data ?? currentPageIndex,
destinations: roleSnapshot.data == RoleEnums.USER.name
? userDestinations
: modDestinations,
),
),
body: IndexedStack(
index: indexSnapshot.data ?? currentPageIndex,
children: roleSnapshot.data == RoleEnums.USER.name
? userBody
: modBody,
),
);
},
);
},
);
}
Future<void> getBellNotification() async {
bell = await apiServices.getBellNotifications("0", "20");
mainBloc.bellBloc.add(bell);
}
bool checkStatus(List<BellItems> bells) {
if (bells.isEmpty) {
return true;
}
return !bells.any((bell) => bell.status == 0);
}
void checkSelectedIndex(int current) {
if (current == 0) {
titlePage = appLocalization(context).home_page_name;
mainBloc.sinkTitle.add(titlePage);
} else if (current == 1) {
titlePage = appLocalization(context).device_manager_page_name;
mainBloc.sinkTitle.add(titlePage);
} else if (current == 2) {
titlePage = appLocalization(context).map_page_destination;
mainBloc.sinkTitle.add(titlePage);
} else if (current == 3) {
titlePage = appLocalization(context).device_log_page_name;
mainBloc.sinkTitle.add(titlePage);
} else if (current == 4) {
titlePage = appLocalization(context).interfamily_page_name;
mainBloc.sinkTitle.add(titlePage);
}
}
}

View File

@@ -0,0 +1,74 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import '../../product/services/map_services.dart';
import '../../product/shared/shared_snack_bar.dart';
import '../devices/device_model.dart';
import '../../product/base/bloc/base_bloc.dart';
import '../../product/services/api_services.dart';
class MapBloc extends BlocBase {
APIServices apiServices = APIServices();
MapServices mapServices = MapServices();
@override
void dispose() {}
final mapType = StreamController<MapType>.broadcast();
StreamSink<MapType> get sinkmapType => mapType.sink;
Stream<MapType> get streammapType => mapType.stream;
final mapController = StreamController<GoogleMapController>.broadcast();
StreamSink<GoogleMapController> get sinkmapController => mapController.sink;
Stream<GoogleMapController> get streammapController => mapController.stream;
final allDevices = StreamController<List<Device>>.broadcast();
StreamSink<List<Device>> get sinkAllDevices => allDevices.sink;
Stream<List<Device>> get streamAllDevices => allDevices.stream;
final allMarker = StreamController<Set<Marker>>.broadcast();
StreamSink<Set<Marker>> get sinkAllMarker => allMarker.sink;
Stream<Set<Marker>> get streamAllMarker => allMarker.stream;
final polylines = StreamController<List<LatLng>>.broadcast();
StreamSink<List<LatLng>> get sinkPolylines => polylines.sink;
Stream<List<LatLng>> get streamPolylines => polylines.stream;
Future<void> updateCameraPosition(
Completer<GoogleMapController> controller,
double latitude,
double longitude,
double zoom,
) async {
final CameraPosition cameraPosition =
CameraPosition(target: LatLng(latitude, longitude), zoom: zoom);
final GoogleMapController mapController = await controller.future;
mapController.animateCamera(CameraUpdate.newCameraPosition(cameraPosition));
}
Future<void> findTheWay(
BuildContext context,
Completer<GoogleMapController> controller,
LatLng origin,
LatLng destination,
) async {
List<LatLng> polylines = [];
final polylineCoordinates =
await mapServices.findTheWay(origin, destination);
if (polylineCoordinates.isNotEmpty) {
polylines = polylineCoordinates;
sinkPolylines.add(polylines);
await updateCameraPosition(
controller,
destination.latitude,
destination.longitude,
13.0,
);
} else {
showNoIconTopSnackBar(
context, "Không tìm thấy đường", Colors.orange, Colors.white);
}
}
}

View File

@@ -0,0 +1,237 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:sfm_app/feature/devices/device_model.dart';
import 'package:sfm_app/feature/map/map_bloc.dart';
import 'package:sfm_app/feature/map/widget/on_tap_marker_widget.dart';
import 'package:sfm_app/product/base/bloc/base_bloc.dart';
import 'package:sfm_app/product/constant/icon/icon_constants.dart';
import 'package:sfm_app/product/services/api_services.dart';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
late BitmapDescriptor normalIcon;
late BitmapDescriptor offlineIcon;
late BitmapDescriptor abnormalIcon;
late BitmapDescriptor flameIcon;
late BitmapDescriptor hospitalIcon;
late BitmapDescriptor fireStationIcon;
late MapBloc mapBloc;
late ClusterManager clusterManager;
MapType mapType = MapType.terrain;
APIServices apiServices = APIServices();
final streamController = StreamController<GoogleMapController>.broadcast();
List<Device> devices = [];
Completer<GoogleMapController> _controller = Completer();
List<String> imageAssets = [
IconConstants.instance.getIcon("normal_icon"),
IconConstants.instance.getIcon("offline_icon"),
IconConstants.instance.getIcon("flame_icon"),
IconConstants.instance.getIcon("hospital_marker"),
IconConstants.instance.getIcon("fire_station_marker"),
];
static const CameraPosition _myPosition = CameraPosition(
target: LatLng(20.976108, 105.791666),
zoom: 12,
);
Set<Marker> markersAll = {};
List<Marker> markers = [];
LatLng myLocation = const LatLng(213761, 123123);
@override
void initState() {
super.initState();
mapBloc = BlocProvider.of(context);
_loadIcons();
getAllMarkers();
clusterManager = _initClusterManager();
}
@override
void dispose() {
streamController.close();
_controller = Completer();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
StreamBuilder<Set<Marker>>(
stream: mapBloc.streamAllMarker,
builder: (context, markerSnapshot) {
return StreamBuilder<List<LatLng>>(
stream: mapBloc.streamPolylines,
builder: (context, polylinesSnapshot) {
return GoogleMap(
initialCameraPosition: _myPosition,
mapType: mapType,
onMapCreated: (GoogleMapController controller) {
if (!_controller.isCompleted) {
_controller.complete(controller);
}
streamController.sink.add(controller);
clusterManager.setMapId(controller.mapId);
},
markers: markerSnapshot.data ?? markersAll
..addAll(markers),
zoomControlsEnabled: true,
myLocationEnabled: true,
mapToolbarEnabled: false,
onCameraMove: (position) {
clusterManager.onCameraMove(position);
},
onCameraIdle: () {
clusterManager.updateMap();
},
polylines: {
Polyline(
polylineId: const PolylineId('router'),
points: polylinesSnapshot.data ?? [],
color: Colors.deepPurpleAccent,
width: 8,
),
},
);
},
);
},
),
],
),
);
}
Future<void> _loadIcons() async {
List<Future<BitmapDescriptor>> iconFutures = imageAssets.map((asset) {
return BitmapDescriptor.fromAssetImage(const ImageConfiguration(), asset);
}).toList();
List<BitmapDescriptor> icons = await Future.wait(iconFutures);
normalIcon = icons[0];
offlineIcon = icons[1];
flameIcon = icons[2];
hospitalIcon = icons[3];
fireStationIcon = icons[4];
}
ClusterManager _initClusterManager() {
return ClusterManager<Device>(
devices,
_updateMarkers,
markerBuilder: _getmarkerBuilder(),
);
}
Future<Marker> Function(Cluster<Device>) _getmarkerBuilder() =>
(cluster) async {
return Marker(
markerId: MarkerId(
cluster.getId(),
),
position: cluster.location,
onTap: () {
onTapMarker(
context,
_controller,
mapBloc,
myLocation,
cluster.items,
imageAssets,
markers,
hospitalIcon,
fireStationIcon);
},
icon: getMarkerIcon(cluster),
);
};
BitmapDescriptor getMarkerIcon(Cluster<Device> cluster) {
if (cluster.items.length == 1) {
Device item = cluster.items.first;
if (item.state == 0) {
return normalIcon;
} else if (item.state == 1) {
return flameIcon;
} else {
return offlineIcon;
}
}
bool hasStateOne = false;
bool hasOtherState = false;
for (var item in cluster.items) {
if (item.state == 1) {
hasStateOne = true;
break;
} else if (item.state != 0) {
hasOtherState = true;
}
}
// log("Has state = 1: $hasStateOne, Has other state: $hasOtherState");
if (hasStateOne) {
return flameIcon; // flameIcon
} else if (hasOtherState) {
return normalIcon; // normalIcon
} else {
return offlineIcon; // offlineIcon
}
}
bool checkStateMarker(Cluster<Device> cluster) {
bool hasStateOne = false;
bool hasOtherState = false;
for (var item in cluster.items) {
if (item.state == 1) {
hasStateOne = true;
break;
} else if (item.state != 0) {
hasOtherState = true;
}
}
// log("Has state = 1: $hasStateOne, Has other state: $hasOtherState");
if (hasStateOne) {
return true; // flameIcon
} else if (hasOtherState) {
return false; // normalIcon
} else {
return true; // offlineIcon
}
}
void _updateMarkers(Set<Marker> marker) {
log("Update Marker");
markersAll = marker;
mapBloc.sinkAllMarker.add(marker);
}
void getAllMarkers() async {
String response = await apiServices.getOwnerDevices();
if (response != "") {
final data = jsonDecode(response);
List<dynamic> result = data['items'];
final devicesList = Device.fromJsonDynamicList(result);
for (var device in devicesList) {
devices.add(device);
}
}
}
}

View File

@@ -0,0 +1,344 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'show_direction_widget.dart';
import 'show_nearest_place.dart';
import '../../../product/constant/icon/icon_constants.dart';
import '../../../product/extention/context_extention.dart';
import '../map_bloc.dart';
import '../../../product/services/api_services.dart';
import '../../../product/services/language_services.dart';
import '../../../product/utils/device_utils.dart';
import '../../devices/device_model.dart';
onTapMarker(
BuildContext context,
Completer<GoogleMapController> controller,
MapBloc mapBloc,
LatLng myLocation,
Iterable<Device> devices,
List<String> imageAssets,
List<Marker> otherMarkers,
BitmapDescriptor hospitalIcon,
BitmapDescriptor fireStationIcon,
) {
LatLng testLocation = const LatLng(20.985453, 105.738381);
showModalBottomSheet(
context: context,
builder: (BuildContext modalBottomSheetContext) {
if (devices.length == 1) {
List<Device> devicesList = devices.toList();
final device = devicesList[0];
String deviceState = DeviceUtils.instance
.checkStateDevice(modalBottomSheetContext, device.state ?? 3);
String imgStringAsset;
if (device.state == 0) {
imgStringAsset = imageAssets[0];
} else if (device.state == 1) {
imgStringAsset = imageAssets[2];
} else {
imgStringAsset = imageAssets[1];
}
return Padding(
padding: context.paddingLow,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"${appLocalization(context).device_title}: ${device.name}",
style: context.titleLargeTextStyle,
),
],
),
SizedBox(
height: context.lowValue,
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () async {
Navigator.pop(modalBottomSheetContext);
var destination = LatLng(
double.parse(device.settings!.latitude!),
double.parse(device.settings!.longitude!),
);
mapBloc.findTheWay(
context,
controller,
testLocation,
destination,
);
String deviceLocations = await DeviceUtils.instance
.getFullDeviceLocation(context, device.areaPath!);
String yourLocation =
appLocalization(context).map_your_location;
showDirections(
context,
controller,
otherMarkers,
mapBloc,
yourLocation,
deviceLocations,
double.parse(device.settings!.latitude!),
double.parse(device.settings!.longitude!),
);
},
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.blue),
foregroundColor: MaterialStatePropertyAll(Colors.white),
),
child: Row(
children: [
IconConstants.instance
.getMaterialIcon(Icons.turn_right),
Text(appLocalization(modalBottomSheetContext)
.map_show_direction),
],
),
),
SizedBox(
width: context.lowValue,
),
ElevatedButton.icon(
onPressed: () {
Navigator.pop(modalBottomSheetContext);
showNearPlacesSideSheet(
context,
double.parse(device.settings!.latitude!),
double.parse(device.settings!.longitude!),
"Bệnh viện gần nhất",
5000,
"hospital",
mapBloc,
controller,
otherMarkers,
hospitalIcon,
fireStationIcon,
);
},
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.pink),
foregroundColor: MaterialStatePropertyAll(Colors.white),
),
icon: IconConstants.instance
.getMaterialIcon(Icons.local_hospital),
label: Text(appLocalization(modalBottomSheetContext)
.map_nearby_hospital),
),
SizedBox(
width: context.lowValue,
),
ElevatedButton.icon(
onPressed: () {
Navigator.pop(modalBottomSheetContext);
showNearPlacesSideSheet(
context,
double.parse(device.settings!.latitude!),
double.parse(device.settings!.longitude!),
"đội pccc gần nhất",
5000,
"fire_station",
mapBloc,
controller,
otherMarkers,
hospitalIcon,
fireStationIcon,
);
},
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.red),
foregroundColor: MaterialStatePropertyAll(Colors.white),
),
icon: IconConstants.instance
.getMaterialIcon(Icons.fire_truck_outlined),
label: Text(appLocalization(modalBottomSheetContext)
.map_nearby_firestation),
),
],
),
),
SizedBox(height: context.lowValue),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${appLocalization(modalBottomSheetContext).paginated_data_table_column_deviceStatus}: '),
SizedBox(
height: 25,
width: 25,
child: CircleAvatar(
minRadius: 20,
maxRadius: 20,
backgroundImage: AssetImage(imgStringAsset),
),
),
SizedBox(width: context.lowValue),
Text(
deviceState,
style: context.bodyMediumTextStyle,
),
],
),
],
),
);
// Tiếp tục xử lý mảng devices
} else if (devices.length > 1) {
DeviceSource deviceSource = DeviceSource(
DeviceUtils.instance.sortDeviceByState(devices.toList()),
modalBottomSheetContext,
controller,
mapBloc,
);
// Devices là một phần tử
return Padding(
padding: context.paddingLow,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Text(
appLocalization(modalBottomSheetContext)
.paginated_data_table_title,
style: context.titleLargeTextStyle,
),
),
PaginatedDataTable(
columns: [
DataColumn(
label: Center(
child: Text(
appLocalization(modalBottomSheetContext)
.input_name_device_device,
),
),
),
DataColumn(
label: Center(
child: Text(
appLocalization(modalBottomSheetContext)
.paginated_data_table_column_deviceStatus,
),
),
),
DataColumn(
label: Center(
child: Text(
appLocalization(modalBottomSheetContext)
.paginated_data_table_column_deviceBaterry,
),
),
),
DataColumn(
label: Center(
child: Text(
appLocalization(modalBottomSheetContext)
.paginated_data_table_column_deviceSignal,
),
),
),
],
source: deviceSource,
rowsPerPage: 5,
),
],
),
);
} else {
return Text(appLocalization(modalBottomSheetContext).undefine_message);
}
},
);
}
class DeviceSource extends DataTableSource {
BuildContext context;
MapBloc mapBloc;
final List<Device> devices;
final Completer<GoogleMapController> controller;
DeviceSource(this.devices, this.context, this.controller, this.mapBloc);
APIServices apiServices = APIServices();
@override
DataRow? getRow(int index) {
if (index >= devices.length) {
return null;
}
final device = devices[index];
Map<String, dynamic> sensorMap = DeviceUtils.instance
.getDeviceSensors(context, device.status?.sensors ?? []);
String deviceState =
DeviceUtils.instance.checkStateDevice(context, device.state!);
return DataRow.byIndex(
color: MaterialStatePropertyAll(
DeviceUtils.instance.getTableRowColor(device.state ?? -1),
),
index: index,
cells: [
DataCell(
Text(device.name!),
onTap: () {
mapBloc.updateCameraPosition(
controller,
double.parse(device.settings!.latitude!),
double.parse(device.settings!.longitude!),
16,
);
Navigator.pop(context);
},
),
DataCell(
Text(deviceState),
onTap: () {
mapBloc.updateCameraPosition(
controller,
double.parse(device.settings!.latitude!),
double.parse(device.settings!.longitude!),
16,
);
},
),
DataCell(
Text(sensorMap['sensorBattery'] + "%"),
onTap: () {
mapBloc.updateCameraPosition(
controller,
double.parse(device.settings!.latitude!),
double.parse(device.settings!.longitude!),
16,
);
},
),
DataCell(
Text(sensorMap['sensorCsq']),
onTap: () {
mapBloc.updateCameraPosition(
controller,
double.parse(device.settings!.latitude!),
double.parse(device.settings!.longitude!),
16,
);
},
),
],
);
}
@override
bool get isRowCountApproximate => false;
@override
int get rowCount => devices.length;
@override
int get selectedRowCount => 0;
}

View File

@@ -0,0 +1,124 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:maps_launcher/maps_launcher.dart';
import 'package:sfm_app/product/constant/icon/icon_constants.dart';
import 'package:sfm_app/product/extention/context_extention.dart';
import 'package:sfm_app/product/services/language_services.dart';
import '../map_bloc.dart';
showDirections(
BuildContext context,
Completer<GoogleMapController> controller,
List<Marker> markers,
MapBloc mapBloc,
String originalName,
String destinationLocation,
double devicelat,
double devicelng,
) {
TextEditingController originController =
TextEditingController(text: originalName);
TextEditingController destinationController =
TextEditingController(text: destinationLocation);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.transparent,
// dismissDirection: DismissDirection.none,
duration: const Duration(minutes: 5),
content: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
appLocalization(context).map_show_direction,
style: context.titleLargeTextStyle,
),
Container(
alignment: Alignment.centerRight,
child: IconButton.outlined(
onPressed: () async {
await mapBloc.updateCameraPosition(
controller,
devicelat,
devicelng,
13.0,
);
List<LatLng> polylineCoordinates = [];
mapBloc.sinkPolylines.add(polylineCoordinates);
markers.clear();
if (context.mounted) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
}
},
icon: IconConstants.instance.getMaterialIcon(Icons.close),
),
)
],
),
Row(
children: [
Text(
'${appLocalization(context).map_start}: ',
),
SizedBox(width: context.lowValue),
Expanded(
child: TextField(
controller: originController,
readOnly: true,
),
),
],
),
SizedBox(
height: context.lowValue,
),
Row(
children: [
Text(
'${appLocalization(context).map_destination}: ',
),
SizedBox(width: context.lowValue),
Expanded(
child: TextField(
controller: destinationController,
readOnly: true,
),
),
],
),
SizedBox(height: context.lowValue),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () async {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
List<LatLng> polylineCoordinates = [];
mapBloc.sinkPolylines.add(polylineCoordinates);
MapsLauncher.launchCoordinates(devicelat, devicelng);
},
icon: IconConstants.instance
.getMaterialIcon(Icons.near_me_rounded),
label: Text(
appLocalization(context).map_stream,
),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.blue[300]!),
),
),
],
),
],
),
),
);
}

View File

@@ -0,0 +1,226 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import '../map_bloc.dart';
import 'show_direction_widget.dart';
import '../../../product/constant/icon/icon_constants.dart';
import '../../../product/extention/context_extention.dart';
import '../../../product/services/language_services.dart';
import '../../../product/services/map_services.dart';
import '../../../product/shared/model/near_by_search_model.dart';
showNearPlacesSideSheet(
BuildContext context,
double latitude,
double longitude,
String searchKey,
int radius,
String type,
MapBloc mapBloc,
Completer<GoogleMapController> controller,
List<Marker> markers,
BitmapDescriptor hospitalIcon,
BitmapDescriptor fireStationIcon,
) async {
double screenWidth = MediaQuery.of(context).size.width;
double screenHeight = MediaQuery.of(context).size.height;
LatLng testLocation = LatLng(latitude, longitude);
List<PlaceDetails> placeDetails = [];
placeDetails =
await findLocation(latitude, longitude, searchKey, radius, type);
await mapBloc.updateCameraPosition(controller, latitude, longitude, 12.0);
if (placeDetails.isNotEmpty) {
for (int i = 0; i < placeDetails.length; i++) {
Marker marker = Marker(
markerId: MarkerId(placeDetails[i].result!.placeId!),
position: LatLng(placeDetails[i].result!.geometry!.location!.lat!,
placeDetails[i].result!.geometry!.location!.lng!),
infoWindow: InfoWindow(title: placeDetails[i].result!.name!),
icon:
searchKey == "Bệnh viện gần nhất" ? hospitalIcon : fireStationIcon,
);
markers.add(marker);
}
}
showModalBottomSheet(
context: context,
builder: (modalBottomSheetContext) {
return Container(
padding: context.paddingLow,
width: screenWidth,
height: screenHeight / 3,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Center(
child: Text(
'${appLocalization(modalBottomSheetContext).map_result}: ',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
Container(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: () async {
Navigator.pop(modalBottomSheetContext);
markers.clear();
await mapBloc.updateCameraPosition(
controller,
latitude,
longitude,
14.0,
);
},
icon: IconConstants.instance.getMaterialIcon(Icons.close),
),
),
],
),
if (placeDetails.isNotEmpty)
Expanded(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: placeDetails.length,
itemBuilder: (BuildContext listViewContext, int index) {
final place = placeDetails[index];
return GestureDetector(
onTap: () async {
await mapBloc.updateCameraPosition(
controller,
place.result!.geometry!.location!.lat!,
place.result!.geometry!.location!.lng!,
17.0,
);
},
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
child: Container(
padding: listViewContext.paddingLow,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
),
width: screenWidth / 1.5,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
place.result!.name!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: listViewContext.lowValue),
Text(
'${appLocalization(listViewContext).change_profile_address} ${place.result!.formattedAddress} '),
SizedBox(height: listViewContext.lowValue),
if (place.result?.openingHours?.openNow == null)
Text(
appLocalization(listViewContext)
.map_always_opened,
style: const TextStyle(
color: Colors.green,
),
)
else
place.result?.openingHours?.openNow ?? false
? Text(
appLocalization(listViewContext)
.map_openning,
style: const TextStyle(
color: Colors.green,
),
)
: Text(
appLocalization(listViewContext)
.map_closed,
style: const TextStyle(
color: Colors.red,
),
),
SizedBox(
height: listViewContext.lowValue,
),
Center(
child: ElevatedButton.icon(
style: const ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue),
foregroundColor:
MaterialStatePropertyAll(Colors.white),
),
onPressed: () async {
Navigator.pop(modalBottomSheetContext);
var destination = LatLng(
place.result!.geometry!.location!.lat!,
place.result!.geometry!.location!.lng!);
mapBloc.findTheWay(context, controller,
testLocation, destination);
// findTheWay(myLocation, destination);
await mapBloc.updateCameraPosition(
controller,
place.result!.geometry!.location!.lat!,
place.result!.geometry!.location!.lng!,
13.0,
);
showDirections(
context,
controller,
markers,
mapBloc,
appLocalization(context)
.map_your_location,
place.result!.name!,
latitude,
longitude,
);
},
icon: IconConstants.instance
.getMaterialIcon(Icons.turn_right),
label: Text(appLocalization(listViewContext)
.map_show_direction),
),
),
],
),
),
),
);
},
),
)
else
Center(
child: Text(
appLocalization(modalBottomSheetContext).map_no_results,
),
),
],
),
);
},
);
}
Future<List<PlaceDetails>> findLocation(double latitude, double longitude,
String searchKey, int radius, String type) async {
MapServices mapServices = MapServices();
final nearByPlaces = await mapServices.getNearbyPlaces(
latitude, longitude, searchKey, radius, type);
return nearByPlaces.isNotEmpty ? nearByPlaces : [];
}

View File

@@ -0,0 +1,42 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'device_notification_settings_model.dart';
import '../../../product/base/bloc/base_bloc.dart';
class DeviceNotificationSettingsBloc extends BlocBase {
final listNotifications =
StreamController<List<DeviceNotificationSettings>>.broadcast();
StreamSink<List<DeviceNotificationSettings>> get sinkListNotifications =>
listNotifications.sink;
Stream<List<DeviceNotificationSettings>> get streamListNotifications =>
listNotifications.stream;
final deviceThingID = StreamController<String>.broadcast();
StreamSink<String> get sinkDeviceThingID => deviceThingID.sink;
Stream<String> get streamDeviceThingID => deviceThingID.stream;
final deviceNotificationSettingMap =
StreamController<Map<String, int>>.broadcast();
StreamSink<Map<String, int>> get sinkDeviceNotificationSettingMap =>
deviceNotificationSettingMap.sink;
Stream<Map<String, int>> get streamDeviceNotificationSettingMap =>
deviceNotificationSettingMap.stream;
final isDataChange = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsDataChange => isDataChange.sink;
Stream<bool> get streamIsDataChange => isDataChange.stream;
final iconChange = StreamController<IconData>.broadcast();
StreamSink<IconData> get sinkIconChange => iconChange.sink;
Stream<IconData> get streamIconChange => iconChange.stream;
final messageChange = StreamController<String>.broadcast();
StreamSink<String> get sinkMessageChange => messageChange.sink;
Stream<String> get streaMmessageChange => messageChange.stream;
@override
void dispose() {}
}

View File

@@ -0,0 +1,56 @@
class DeviceNotificationSettings {
String? id;
String? userId;
String? thingId;
DeviceAliasSettings? settings;
DateTime? updatedAt;
DeviceNotificationSettings({
this.id,
this.userId,
this.thingId,
this.settings,
this.updatedAt,
});
DeviceNotificationSettings.fromJson(Map<String, dynamic> json) {
id = json["id"];
userId = json["user_id"];
thingId = json["thing_id"];
settings = json["settings"] != null
? DeviceAliasSettings.fromJson(json["settings"])
: null;
updatedAt = DateTime.parse(json["updated_at"]);
}
static List<DeviceNotificationSettings> mapToList(
Map<String, DeviceNotificationSettings> map) {
return map.values.toList();
}
static Map<String, DeviceNotificationSettings> mapFromJson(
Map<String, dynamic> json) {
final Map<String, DeviceNotificationSettings> devices = {};
json.forEach((key, value) {
devices[key] = DeviceNotificationSettings.fromJson(value);
});
return devices;
}
}
class DeviceAliasSettings {
String? alias;
Map<String, int>? notifiSettings;
DeviceAliasSettings({
this.alias,
this.notifiSettings,
});
DeviceAliasSettings.fromJson(Map<String, dynamic> json) {
alias = json['alias'];
if (json['notifi_settings'] != null) {
notifiSettings = Map<String, int>.from(json['notifi_settings']);
}
}
}

View File

@@ -0,0 +1,313 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:convert';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import '../../../product/shared/shared_snack_bar.dart';
import 'device_notification_settings_bloc.dart';
import 'device_notification_settings_model.dart';
import '../../../product/base/bloc/base_bloc.dart';
import '../../../product/extention/context_extention.dart';
import '../../../product/services/api_services.dart';
import '../../../product/services/language_services.dart';
class DeviceNotificationSettingsScreen extends StatefulWidget {
const DeviceNotificationSettingsScreen({super.key});
@override
State<DeviceNotificationSettingsScreen> createState() =>
_DeviceNotificationSettingsScreenState();
}
class _DeviceNotificationSettingsScreenState
extends State<DeviceNotificationSettingsScreen> {
late DeviceNotificationSettingsBloc deviceNotificationSettingsBloc;
String thingID = "";
List<DeviceNotificationSettings> deviceNotifications = [];
APIServices apiServices = APIServices();
@override
void initState() {
super.initState();
deviceNotificationSettingsBloc = BlocProvider.of(context);
getNotificationSetting();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<List<DeviceNotificationSettings>>(
stream: deviceNotificationSettingsBloc.streamListNotifications,
initialData: deviceNotifications,
builder: (context, notificationSnapshot) {
return StreamBuilder<String>(
stream: deviceNotificationSettingsBloc.streamDeviceThingID,
initialData: thingID,
builder: (context, deviceThingIdSnapshot) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(
appLocalization(context).profile_setting,
style: Theme.of(context).textTheme.titleLarge,
),
),
body: deviceNotifications.isEmpty
? Center(
child: CircularProgressIndicator(
value: context.mediumValue,
),
)
: Padding(
padding: context.paddingLow,
child: Column(
children: [
DropdownButtonFormField2<
DeviceNotificationSettings>(
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),
),
iconStyleData: const IconStyleData(
icon: Icon(
Icons.arrow_drop_down,
),
iconSize: 24,
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(horizontal: 16),
),
items: deviceNotifications
.map((e) => DropdownMenuItem<
DeviceNotificationSettings>(
value: e,
child: Text(e.settings!.alias!),
))
.toList(),
onChanged: (value) {
thingID = value!.thingId ?? "";
deviceNotificationSettingsBloc
.sinkDeviceThingID
.add(thingID);
},
),
if (deviceThingIdSnapshot.data! != "")
listNotificationSetting(
deviceThingIdSnapshot.data ?? thingID,
deviceNotifications)
],
),
),
);
});
});
}
void getNotificationSetting() async {
String? response = await apiServices.getAllSettingsNotificationOfDevices();
final data = jsonDecode(response);
final result = data['data'];
// log("Data ${DeviceNotificationSettings.mapFromJson(jsonDecode(data)).values.toList()}");
deviceNotifications =
DeviceNotificationSettings.mapFromJson(result).values.toList();
deviceNotificationSettingsBloc.sinkListNotifications
.add(deviceNotifications);
}
Widget listNotificationSetting(
String thingID, List<DeviceNotificationSettings> devices) {
DeviceNotificationSettings chooseDevice = DeviceNotificationSettings();
for (var device in devices) {
if (device.thingId == thingID) {
chooseDevice = device;
break;
}
}
IconData selectIcon = Icons.unpublished_outlined;
String selectMessage = appLocalization(context)
.change_profile_device_notification_deselect_all;
bool isChange = false;
bool isDataChange = false;
// Lấy notifiSettings
final notifiSettings = chooseDevice.settings?.notifiSettings ?? {};
deviceNotificationSettingsBloc.sinkDeviceNotificationSettingMap
.add(notifiSettings);
return SingleChildScrollView(
child: StreamBuilder<Map<String, int>>(
stream:
deviceNotificationSettingsBloc.streamDeviceNotificationSettingMap,
initialData: notifiSettings,
builder: (context, deviceNotificationSettingSnapshot) {
return StreamBuilder<bool>(
stream: deviceNotificationSettingsBloc.streamIsDataChange,
initialData: isDataChange,
builder: (context, isDataChangeSnapshot) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton.icon(
onPressed: () {
changeIconAndMessage(
isChange,
selectIcon,
selectMessage,
notifiSettings,
isDataChange);
isChange = !isChange;
},
icon: StreamBuilder<IconData>(
stream: deviceNotificationSettingsBloc
.streamIconChange,
initialData: selectIcon,
builder: (context, iconSnapshot) {
return Icon(
iconSnapshot.data ?? selectIcon);
}),
label: StreamBuilder<String>(
stream: deviceNotificationSettingsBloc
.streaMmessageChange,
builder: (context, messageSnapshot) {
return Text(
messageSnapshot.data ?? selectMessage);
})),
if (isDataChangeSnapshot.data ?? isDataChange)
IconButton(
onPressed: () {
updateDeviceNotification(
thingID, notifiSettings, isDataChange);
},
icon: const Icon(Icons.done))
],
),
ListView.builder(
shrinkWrap: true,
itemCount:
deviceNotificationSettingSnapshot.data?.length ??
notifiSettings.length,
itemBuilder: (context, index) {
final key = deviceNotificationSettingSnapshot
.data!.keys
.elementAt(index);
final value =
deviceNotificationSettingSnapshot.data![key];
return CheckboxListTile(
value: value == 1,
onChanged: (newValue) {
notifiSettings[key] = newValue == true ? 1 : 0;
deviceNotificationSettingsBloc
.sinkDeviceNotificationSettingMap
.add(notifiSettings);
isDataChange = true;
deviceNotificationSettingsBloc.sinkIsDataChange
.add(isDataChange);
},
title: Text(getNotificationEvent(key)),
);
},
),
if (isDataChangeSnapshot.data ?? isDataChange)
Center(
child: TextButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(
Colors.green),
foregroundColor:
MaterialStateProperty.all<Color>(
Colors.white)),
onPressed: () async {
updateDeviceNotification(
thingID, notifiSettings, isDataChange);
},
child: Text(appLocalization(context)
.update_button_content)),
)
],
);
});
}),
);
}
void changeIconAndMessage(bool isChange, IconData icon, String message,
Map<String, int> notifiSettings, bool isDataChange) {
if (isChange == false) {
icon = Icons.published_with_changes_outlined;
message = appLocalization(context)
.change_profile_device_notification_select_all;
notifiSettings.updateAll((key, value) => 0);
} else {
icon = Icons.unpublished_outlined;
message = appLocalization(context)
.change_profile_device_notification_deselect_all;
notifiSettings.updateAll((key, value) => 1);
}
isDataChange = true;
deviceNotificationSettingsBloc.sinkIsDataChange.add(isDataChange);
deviceNotificationSettingsBloc.sinkDeviceNotificationSettingMap
.add(notifiSettings);
deviceNotificationSettingsBloc.sinkIconChange.add(icon);
deviceNotificationSettingsBloc.sinkMessageChange.add(message);
}
String getNotificationEvent(String eventCode) {
String event = "";
if (eventCode == "001") {
event = "Thiết bị mất kết nối";
} else if (eventCode == "002") {
event = "Hoạt động của thiết bị";
} else if (eventCode == "003") {
event = "Cảnh báo của thiết bị";
} else if (eventCode == "004") {
event = "Thiết bị lỗi";
} else if (eventCode == "005") {
event = "Thiết bị bình thường";
} else if (eventCode == "006") {
event = "Thiết bị yếu pin";
} else if (eventCode == "101") {
event = "Người dùng tham gia nhóm";
} else if (eventCode == "102") {
event = "Người dùng rời nhóm";
} else if (eventCode == "103") {
event = "Thiết bị tham gia nhóm";
} else if (eventCode == "104") {
event = "Thiết bị bị xóa khỏi nhóm";
} else {
event = "Chưa xác định";
}
return event;
}
void updateDeviceNotification(String thingID, Map<String, int> notifiSettings,
bool isDataChange) async {
int statusCode = await apiServices.updateDeviceNotificationSettings(
thingID, notifiSettings);
if (statusCode == 200) {
showNoIconTopSnackBar(
context,
appLocalization(context).notification_update_device_settings_success,
Colors.green,
Colors.white);
} else {
showNoIconTopSnackBar(
context,
appLocalization(context).notification_update_device_settings_failed,
Colors.red,
Colors.white);
}
isDataChange = false;
deviceNotificationSettingsBloc.sinkIsDataChange.add(isDataChange);
}
}

View File

@@ -0,0 +1,37 @@
class User {
String? id;
String? username;
String? role;
String? name;
String? email;
String? phone;
String? address;
String? notes;
String? latitude;
String? longitude;
User(
{this.address,
this.email,
this.id,
this.name,
this.notes,
this.phone,
this.role,
this.username,
this.latitude,
this.longitude});
User.fromJson(Map<String, dynamic> json) {
id = json['id'];
username = json['username'];
role = json['role'];
name = json['name'];
email = json['email'];
phone = json['phone'];
address = json['address'];
notes = json['notes'];
latitude = json['latitude'];
longitude = json['longitude'];
}
}

View File

@@ -0,0 +1,440 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import '../../../product/shared/shared_snack_bar.dart';
import '../../../product/constant/icon/icon_constants.dart';
import '../../../product/services/api_services.dart';
import '../settings_bloc.dart';
import '../../../product/shared/shared_input_decoration.dart';
import '../../../product/extention/context_extention.dart';
import '../../../product/services/language_services.dart';
import 'profile_model.dart';
changeUserInfomation(
BuildContext context, User user, SettingsBloc settingsBloc) {
final formKey = GlobalKey<FormState>();
String username = user.name ?? "";
String email = user.email ?? "";
String tel = user.phone ?? "";
String address = user.address ?? "";
bool isChange = false;
APIServices apiServices = APIServices();
showModalBottomSheet(
isScrollControlled: true,
useSafeArea: true,
context: context,
builder: (modalBottomSheetContext) {
return StreamBuilder<bool>(
stream: settingsBloc.streamIsChangeProfileInfomation,
initialData: isChange,
builder: (context, isChangeSnapshot) {
return Scaffold(
appBar: AppBar(
title: Text(appLocalization(context).change_profile_title),
centerTitle: true,
actions: [
isChangeSnapshot.data ?? isChange
? IconButton(
onPressed: () async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
String latitude = user.latitude ?? "";
String longitude = user.longitude ?? "";
Map<String, dynamic> body = {
"name": username,
"email": email,
"phone": tel,
"address": address,
"latitude": latitude,
"longitude": longitude
};
int statusCode =
await apiServices.updateUserProfile(body);
if (statusCode == 200) {
showNoIconTopSnackBar(
modalBottomSheetContext,
appLocalization(context)
.notification_update_profile_success,
Colors.green,
Colors.white);
} else {
showNoIconTopSnackBar(
modalBottomSheetContext,
appLocalization(context)
.notification_update_profile_failed,
Colors.redAccent,
Colors.white);
}
Navigator.pop(modalBottomSheetContext);
}
},
icon:
IconConstants.instance.getMaterialIcon(Icons.check),
)
: const SizedBox.shrink()
],
),
body: SingleChildScrollView(
child: Form(
key: formKey,
child: Padding(
padding: context.paddingLow,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appLocalization(context).change_profile_username,
style: context.titleMediumTextStyle,
),
Padding(
padding: context.paddingLowVertical,
child: TextFormField(
initialValue: username,
textInputAction: TextInputAction.next,
validator: (usernameValue) {
if (usernameValue == "null" ||
usernameValue!.isEmpty) {
return appLocalization(modalBottomSheetContext)
.login_account_not_empty;
}
return null;
},
onChanged: (value) {
isChange = true;
settingsBloc.sinkIsChangeProfileInfomation
.add(isChange);
},
onSaved: (newUsername) {
username = newUsername!;
},
decoration: borderRadiusTopLeftAndBottomRight(
modalBottomSheetContext,
appLocalization(modalBottomSheetContext)
.change_profile_username_hint),
),
),
Text(
appLocalization(context).change_profile_email,
style: context.titleMediumTextStyle,
),
Padding(
padding: context.paddingLowVertical,
child: TextFormField(
initialValue: email,
textInputAction: TextInputAction.next,
validator: (emailValue) {
if (emailValue == "null" || emailValue!.isEmpty) {
return appLocalization(modalBottomSheetContext)
.change_profile_email_not_empty;
}
return null;
},
onChanged: (value) {
isChange = true;
settingsBloc.sinkIsChangeProfileInfomation
.add(isChange);
},
onSaved: (newEmail) {
email = newEmail!;
},
decoration: borderRadiusTopLeftAndBottomRight(
modalBottomSheetContext,
appLocalization(modalBottomSheetContext)
.change_profile_email_hint),
),
),
Text(
appLocalization(context).change_profile_tel,
style: context.titleMediumTextStyle,
),
Padding(
padding: context.paddingLowVertical,
child: TextFormField(
initialValue: tel,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.phone,
validator: (telValue) {
if (telValue == "null" || telValue!.isEmpty) {
return appLocalization(modalBottomSheetContext)
.change_profile_tel_not_empty;
}
return null;
},
onChanged: (value) {
isChange = true;
settingsBloc.sinkIsChangeProfileInfomation
.add(isChange);
},
onSaved: (newTel) {
tel = newTel!;
},
decoration: borderRadiusTopLeftAndBottomRight(
modalBottomSheetContext,
appLocalization(modalBottomSheetContext)
.change_profile_tel_hint),
),
),
Text(
appLocalization(context).change_profile_address,
style: context.titleMediumTextStyle,
),
Padding(
padding: context.paddingLowVertical,
child: TextFormField(
initialValue: address,
textInputAction: TextInputAction.done,
onSaved: (newAddress) {
address = newAddress!;
},
onChanged: (value) {
isChange = true;
settingsBloc.sinkIsChangeProfileInfomation
.add(isChange);
},
decoration: borderRadiusTopLeftAndBottomRight(
modalBottomSheetContext,
appLocalization(modalBottomSheetContext)
.change_profile_address_hint),
),
),
SizedBox(height: context.mediumValue),
isChangeSnapshot.data ?? isChange
? Center(
child: TextButton(
onPressed: () async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
String latitude = user.latitude ?? "";
String longitude = user.longitude ?? "";
Map<String, dynamic> body = {
"name": username,
"email": email,
"phone": tel,
"address": address,
"latitude": latitude,
"longitude": longitude
};
int statusCode = await apiServices
.updateUserProfile(body);
if (statusCode == 200) {
showNoIconTopSnackBar(
modalBottomSheetContext,
appLocalization(context)
.notification_update_profile_success,
Colors.green,
Colors.white);
} else {
showNoIconTopSnackBar(
modalBottomSheetContext,
appLocalization(context)
.notification_update_profile_failed,
Colors.redAccent,
Colors.white);
}
Navigator.pop(modalBottomSheetContext);
}
},
style: const ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue),
foregroundColor:
MaterialStatePropertyAll(Colors.white),
),
child: Text(appLocalization(context)
.update_button_content),
),
)
: const SizedBox.shrink(),
],
),
),
),
),
);
},
);
},
);
}
changeUserPassword(BuildContext context, SettingsBloc settingsBloc) {
final formKey = GlobalKey<FormState>();
String oldPass = "";
String newPass = "";
APIServices apiServices = APIServices();
bool isChange = false;
showModalBottomSheet(
isScrollControlled: true,
useSafeArea: true,
context: context,
builder: (modalBottomSheetContext) {
return StreamBuilder<bool>(
stream: settingsBloc.streamIsChangeProfileInfomation,
initialData: isChange,
builder: (context, isChangeSnapshot) {
return Scaffold(
appBar: AppBar(
title: Text(appLocalization(context).change_profile_title),
centerTitle: true,
actions: [
isChangeSnapshot.data ?? isChange
? IconButton(
onPressed: () async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
Map<String, dynamic> body = {
"password_old": oldPass,
"password_new": newPass,
};
int statusCode =
await apiServices.updateUserPassword(body);
if (statusCode == 200) {
showNoIconTopSnackBar(
modalBottomSheetContext,
appLocalization(context)
.notification_update_password_success,
Colors.green,
Colors.white);
} else {
showNoIconTopSnackBar(
modalBottomSheetContext,
appLocalization(context)
.notification_update_password_failed,
Colors.redAccent,
Colors.white);
}
Navigator.pop(modalBottomSheetContext);
}
},
icon:
IconConstants.instance.getMaterialIcon(Icons.check),
)
: const SizedBox.shrink()
],
),
body: SingleChildScrollView(
child: Form(
key: formKey,
child: Padding(
padding: context.paddingLow,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appLocalization(context).change_profile_old_pass,
style: context.titleMediumTextStyle,
),
Padding(
padding: context.paddingLowVertical,
child: TextFormField(
initialValue: oldPass,
textInputAction: TextInputAction.next,
validator: (oldPassValue) {
if (oldPassValue == "null" ||
oldPassValue!.isEmpty) {
return appLocalization(modalBottomSheetContext)
.change_profile_old_pass_not_empty;
}
return null;
},
onChanged: (value) {
isChange = true;
settingsBloc.sinkIsChangeProfileInfomation
.add(isChange);
},
onSaved: (newOldPass) {
oldPass = newOldPass!;
},
decoration: borderRadiusTopLeftAndBottomRight(
modalBottomSheetContext,
appLocalization(modalBottomSheetContext)
.change_profile_old_pass_hint),
),
),
Text(
appLocalization(context).change_profile_new_pass,
style: context.titleMediumTextStyle,
),
Padding(
padding: context.paddingLowVertical,
child: TextFormField(
initialValue: newPass,
textInputAction: TextInputAction.done,
validator: (newPassValue) {
if (newPassValue == "null" ||
newPassValue!.isEmpty) {
return appLocalization(modalBottomSheetContext)
.change_profile_new_pass_not_empty;
}
return null;
},
onChanged: (value) {
isChange = true;
settingsBloc.sinkIsChangeProfileInfomation
.add(isChange);
},
onSaved: (newnewPass) {
newPass = newnewPass!;
},
decoration: borderRadiusTopLeftAndBottomRight(
modalBottomSheetContext,
appLocalization(modalBottomSheetContext)
.change_profile_new_pass_hint),
),
),
SizedBox(height: context.mediumValue),
isChangeSnapshot.data ?? isChange
? Center(
child: TextButton(
onPressed: () async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
Map<String, dynamic> body = {
"password_old": oldPass,
"password_new": newPass,
};
int statusCode = await apiServices
.updateUserPassword(body);
if (statusCode == 200) {
showNoIconTopSnackBar(
modalBottomSheetContext,
appLocalization(context)
.notification_update_password_success,
Colors.green,
Colors.white);
} else {
showNoIconTopSnackBar(
modalBottomSheetContext,
appLocalization(context)
.notification_update_password_failed,
Colors.redAccent,
Colors.white);
}
Navigator.pop(modalBottomSheetContext);
}
},
style: const ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue),
foregroundColor:
MaterialStatePropertyAll(Colors.white),
),
child: Text(appLocalization(context)
.update_button_content),
),
)
: const SizedBox.shrink()
],
),
),
),
),
);
},
);
},
);
}

View File

@@ -0,0 +1,25 @@
import 'dart:async';
import 'profile/profile_model.dart';
import '../../product/base/bloc/base_bloc.dart';
class SettingsBloc extends BlocBase {
// Settings Screen
final userProfile = StreamController<User>.broadcast();
StreamSink<User> get sinkUserProfile => userProfile.sink;
Stream<User> get streamUserProfile => userProfile.stream;
// Profile Screen
final isChangeProfileInfomation = StreamController<bool>.broadcast();
StreamSink<bool> get sinkIsChangeProfileInfomation =>
isChangeProfileInfomation.sink;
Stream<bool> get streamIsChangeProfileInfomation =>
isChangeProfileInfomation.stream;
@override
void dispose() {
}
}

View File

@@ -0,0 +1,153 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../product/constant/app/app_constants.dart';
import 'profile/profile_screen.dart';
import '../../product/constant/icon/icon_constants.dart';
import '../../product/extention/context_extention.dart';
import '../../product/services/api_services.dart';
import 'profile/profile_model.dart';
import 'settings_bloc.dart';
import '../../product/base/bloc/base_bloc.dart';
import '../../product/services/language_services.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
late SettingsBloc settingsBloc;
User user = User();
APIServices apiServices = APIServices();
@override
void initState() {
super.initState();
settingsBloc = BlocProvider.of(context);
getUserProfile();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(appLocalization(context).profile_page_title),
centerTitle: true,
),
body: StreamBuilder<User>(
stream: settingsBloc.streamUserProfile,
initialData: user,
builder: (context, userSnapshot) {
return userSnapshot.data?.id == "" || user.id == ""
? Center(
child: CircularProgressIndicator(
value: context.highValue,
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircleAvatar(
radius: 70,
child: CircleAvatar(
radius: 60,
child: CircleAvatar(
radius: 50,
child: Text(
getAvatarContent(
userSnapshot.data?.username ?? ""),
style: const TextStyle(
fontSize: 35, fontWeight: FontWeight.bold),
),
),
),
),
SizedBox(height: context.lowValue),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
userSnapshot.data?.name ?? "User Name",
style: const TextStyle(
fontWeight: FontWeight.w900, fontSize: 26),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text(userSnapshot.data?.email ?? "Email")],
),
SizedBox(height: context.mediumValue),
cardContent(
Icons.account_circle_rounded,
appLocalization(context).profile_change_info,
),
SizedBox(height: context.lowValue),
cardContent(
Icons.lock_outline,
appLocalization(context).profile_change_pass,
),
SizedBox(height: context.lowValue),
cardContent(
Icons.settings_outlined,
appLocalization(context).profile_setting,
),
SizedBox(height: context.lowValue),
cardContent(
Icons.logout_outlined,
appLocalization(context).log_out,
),
],
);
}),
);
}
cardContent(IconData icon, String content) {
return GestureDetector(
onTap: () async {
if (icon == Icons.account_circle_rounded) {
changeUserInfomation(context, user, settingsBloc);
} else if (icon == Icons.lock_outline) {
changeUserPassword(context, settingsBloc);
} else if (icon == Icons.settings_outlined) {
context.push(ApplicationConstants.DEVICE_NOTIFICATIONS_SETTINGS);
} else {
await apiServices.logOut(context);
}
},
child: Card(
margin: const EdgeInsets.only(left: 35, right: 35, bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
child: ListTile(
leading: IconConstants.instance.getMaterialIcon(icon),
title: Text(
content,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
trailing: const Icon(
Icons.arrow_forward_ios_outlined,
),
),
),
);
}
void getUserProfile() async {
String data = await apiServices.getUserDetail();
user = User.fromJson(jsonDecode(data));
settingsBloc.sinkUserProfile.add(user);
}
String getAvatarContent(String username) {
String name = "";
if (username.isNotEmpty) {
name = username[0].toUpperCase();
}
return name;
}
}