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

86
lib/firebase_options.dart Normal file
View File

@@ -0,0 +1,86 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyCD8s_CD1dzXuY2EdgzulRJqZV7TY-1chY',
appId: '1:910110439150:web:b3dc22fa9ecb953b5b65ff',
messagingSenderId: '910110439150',
projectId: 'sfm-notification',
authDomain: 'sfm-notification.firebaseapp.com',
storageBucket: 'sfm-notification.firebasestorage.app',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyADdZ66_q4HALlb0-yEGgeyGnsbwmlvDsA',
appId: '1:910110439150:android:c3dbc3b4a85d7cb75b65ff',
messagingSenderId: '910110439150',
projectId: 'sfm-notification',
storageBucket: 'sfm-notification.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyCpcnAulIA5fxrSM0uHSf0uWSzzlM208Fw',
appId: '1:910110439150:ios:1d111f0cdd0240a35b65ff',
messagingSenderId: '910110439150',
projectId: 'sfm-notification',
storageBucket: 'sfm-notification.firebasestorage.app',
iosBundleId: 'com.example.sfmApp',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyCpcnAulIA5fxrSM0uHSf0uWSzzlM208Fw',
appId: '1:910110439150:ios:1d111f0cdd0240a35b65ff',
messagingSenderId: '910110439150',
projectId: 'sfm-notification',
storageBucket: 'sfm-notification.firebasestorage.app',
iosBundleId: 'com.example.sfmApp',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyCD8s_CD1dzXuY2EdgzulRJqZV7TY-1chY',
appId: '1:910110439150:web:7c413e6bc22555575b65ff',
messagingSenderId: '910110439150',
projectId: 'sfm-notification',
authDomain: 'sfm-notification.firebaseapp.com',
storageBucket: 'sfm-notification.firebasestorage.app',
);
}

80
lib/main.dart Normal file
View File

@@ -0,0 +1,80 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sfm_app/product/services/language_services.dart';
import 'feature/main/main_bloc.dart';
import 'product/base/bloc/base_bloc.dart';
import 'product/constant/navigation/navigation_router.dart';
import 'product/theme/provider/app_provider.dart';
import 'product/theme/theme_notifier.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MultiProvider(
providers: [...ApplicationProvider.instance.dependItems],
child: BlocProvider(
child: const MyApp(),
blocBuilder: () => MainBloc(),
),
),
);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
static void setLocale(BuildContext context, Locale newLocale) {
_MyAppState? state = context.findAncestorStateOfType<_MyAppState>();
state?.setLocale(newLocale);
}
}
class _MyAppState extends State<MyApp> {
Locale? _locale;
late MainBloc mainBloc;
LanguageServices languageServices = LanguageServices();
// late ThemeNotifier themeNotifier;
setLocale(Locale locale) {
_locale = locale;
mainBloc.sinkLanguage.add(_locale);
}
@override
void initState() {
super.initState();
mainBloc = BlocProvider.of(context);
// themeNotifier = Provider.of<ThemeNotifier>(context, listen: false);
// ThemeNotifier().loadThemeFromPreferences();
// log("ThemeKey1: ${LocaleManager.instance.getStringValue(PreferencesKeys.THEME)}");
// log("Date: ${DateTime.parse("2024-11-16T07:17:36.785Z")}");
}
@override
void didChangeDependencies() {
languageServices.getLocale().then((locale) => {setLocale(locale)});
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
final router = goRouter();
return StreamBuilder<Locale?>(
stream: mainBloc.streamLanguage,
initialData: _locale,
builder: (context, languageSnapshot) {
return MaterialApp.router(
theme: context.watch<ThemeNotifier>().currentTheme,
routerConfig: router,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: languageSnapshot.data,
);
},
);
}
}

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
typedef BlocBuilder<T> = T Function();
typedef BlocDisposer<T> = Function(T);
abstract class BlocBase {
void dispose();
}
class _BlocProviderInherited<T> extends InheritedWidget {
const _BlocProviderInherited({
Key? key,
required Widget child,
required this.bloc,
}) : super(key: key, child: child);
final T bloc;
@override
bool updateShouldNotify(_BlocProviderInherited oldWidget) => false;
}
class BlocProvider<T extends BlocBase> extends StatefulWidget {
const BlocProvider({
Key? key,
required this.child,
required this.blocBuilder,
this.blocDispose,
}) : super(key: key);
final Widget child;
final BlocBuilder<T> blocBuilder;
final BlocDisposer<T>? blocDispose;
@override
BlocProviderState<T> createState() => BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
_BlocProviderInherited<T> provider = context
.getElementForInheritedWidgetOfExactType<_BlocProviderInherited<T>>()
?.widget as _BlocProviderInherited<T>;
return provider.bloc;
}
}
class BlocProviderState<T extends BlocBase> extends State<BlocProvider<T>> {
late T bloc;
@override
void initState() {
super.initState();
bloc = widget.blocBuilder();
}
@override
void dispose() {
if (widget.blocDispose != null) {
widget.blocDispose!(bloc);
} else {
bloc.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return _BlocProviderInherited<T>(
bloc: bloc,
child: widget.child,
);
}
}

View File

@@ -0,0 +1,142 @@
import 'dart:async';
import 'package:flutter/material.dart';
abstract class BasePageScreen extends StatefulWidget {
const BasePageScreen({Key? key}) : super(key: key);
}
abstract class BasePageScreenState<T extends BasePageScreen> extends State<T> {
bool _isBack = true;
bool _isHome = true;
bool _isShowTitle = true;
final _loadingController = StreamController<bool>();
String appBarTitle();
String appBarSubTitle();
void onClickBackButton();
void onClickHome();
void isBackButton(bool isBack) {
_isBack = isBack;
}
void isHomeButton(bool isHome) {
_isHome = isHome;
}
void isShowTitle(bool isShowTitle) {
_isShowTitle = isShowTitle;
}
void showLoading() {
_loadingController.add(true);
}
void hideLoading() {
_loadingController.add(false);
}
}
mixin BaseScreen<T extends BasePageScreen> on BasePageScreenState<T> {
Widget body();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// flexibleSpace: Container(
// decoration: const BoxDecoration(
// image: DecorationImage(
// image: AssetImage('assets/images/bg_header.jpeg'),
// fit: BoxFit.cover)),
// ),
title: Column(
children: [
Visibility(
visible: _isShowTitle,
child: Text(
appBarTitle(),
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
Text(
appBarSubTitle(),
maxLines: _isShowTitle ? 1 : 2,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
)
],
),
leading: _isBack
? IconButton(
icon: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
),
onPressed: () {
// onClickBackButton();
Navigator.of(context).pushNamedAndRemoveUntil(
"/main", (Route<dynamic> route) => false);
},
)
: Container(),
actions: [
_isHome
? IconButton(
icon: const Icon(
Icons.home,
color: Colors.white,
),
onPressed: () {
onClickHome();
},
)
: Container()
],
),
body: Stack(
children: [
Container(
height: double.infinity,
color: Colors.white,
child: body(),
),
StreamBuilder(
stream: _loadingController.stream,
builder: (_, snapshot) {
return snapshot.data == true
? Positioned.fill(
child: _buildLoader(),
)
: const SizedBox();
},
),
],
));
}
Widget _buildLoader() {
return Container(
color: Colors.black54,
child: const Center(
child: CircularProgressIndicator(),
),
);
}
@override
void dispose() {
_loadingController.close();
super.dispose();
}
}

View File

@@ -0,0 +1,61 @@
import 'package:app_settings/app_settings.dart';
import 'package:flutter/material.dart';
import '../../../extention/context_extention.dart';
import '../../../services/language_services.dart';
class RequestPermissionDialog {
showRequestPermissionDialog(BuildContext context, IconData icon,
String dialogContent, AppSettingsType type) {
showDialog(
useRootNavigator: false,
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Center(
child: Icon(icon),
),
content: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: context.paddingNormalVertical,
child: Text(
dialogContent,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
),
),
Divider(height: context.lowValue),
GestureDetector(
onTap: () {
AppSettings.openAppSettings(type: type);
},
child: Padding(
padding:
context.paddingNormalVertical, // Cách giữa các phần tử
child: Text(appLocalization(context).allow_message,
style: const TextStyle(fontWeight: FontWeight.bold)),
),
),
Divider(height: context.lowValue),
GestureDetector(
onTap: () {
Navigator.of(dialogContext).pop(); // Đóng dialog
},
child: Padding(
padding:
context.paddingNormalVertical, // Cách giữa các phần tử
child: Text(appLocalization(context).decline_message,
style: const TextStyle(fontWeight: FontWeight.bold)),
),
),
],
),
);
},
);
}
}

42
lib/product/cache/local_manager.dart vendored Normal file
View File

@@ -0,0 +1,42 @@
import 'package:shared_preferences/shared_preferences.dart';
import '../constant/enums/local_keys_enums.dart';
class LocaleManager {
LocaleManager._init() {
SharedPreferences.getInstance().then((value) {
_preferences = value;
});
}
static final LocaleManager _instance = LocaleManager._init();
SharedPreferences? _preferences;
static LocaleManager get instance => _instance;
static Future prefrencesInit() async {
instance._preferences ??= await SharedPreferences.getInstance();
}
void setString(PreferencesKeys key, String value) async {
await _preferences?.setString(key.toString(), value);
}
void setInt(PreferencesKeys key, int value) async {
await _preferences?.setInt(key.toString(), value);
}
Future<void> setStringValue(PreferencesKeys key, String value) async {
await _preferences!.setString(key.toString(), value);
}
String getStringValue(PreferencesKeys key) =>
_preferences?.getString(key.toString()) ?? '';
int getIntValue(PreferencesKeys key) =>
_preferences?.getInt(key.toString()) ?? 0;
Future<void> deleteStringValue(PreferencesKeys key) async {
await _preferences!.remove(key.toString());
}
}

View File

@@ -0,0 +1,24 @@
// ignore_for_file: constant_identifier_names
class APIPathConstants {
static const LOGIN_PATH = "/api/login";
static const LOGOUT_PATH = "/api/logout";
static const BELL_NOTIFICATIONS_PATH = "/api/notifications/bell-list";
static const BELL_UPDATE_READ_NOTIFICATIONS_PATH =
"/api/notifications/bell-read";
static const DEVICE_NOTIFICATION_SETTINGS = "/api/users/my-device-settings";
static const DASHBOARD_DEVICES = "/api/dashboard/devices";
static const USER_PATH = "/api/users";
static const USER_PROFILE_PATH = "/api/users/profile";
static const PROVINCES_PATH = "/api/vn/provinces";
static const DISTRICTS_PATH = "/api/vn/districts";
static const WARDS_PATH = "/api/vn/wards";
static const DEVICE_PATH = "/api/devices";
static const DEVICE_REGISTER_PATH = "/api/devices/register";
static const DEVICE_UNREGISTER_PATH = "/api/devices/unregister";
static const ALL_GROUPS_PATH = "/api/groups/user";
static const GROUPS_PATH = "/api/groups";
static const JOIN_GROUP_PATH = "/api/groups/join";
static const APPROVE_GROUP_PATH = "/api/groups/approve";
static const DEVICE_LOGS_PATH = "/api/device-logs";
}

View File

@@ -0,0 +1,21 @@
// ignore_for_file: constant_identifier_names
class ApplicationConstants {
static const APP_NAME = "Smatec SFM";
static const DOMAIN = "sfm.smatec.com.vn";
static const MAP_KEY = "AIzaSyDI8b-PUgKUgj5rHdtgEHCwWjUXYJrqYhE";
static const LOGIN_PATH = "/login";
static const LOGOUT_PATH = "/logout";
static const HOME_PATH = "/";
static const SETTINGS_PATH = "/settings";
static const BELL_PATH = "/bell";
static const DEVICES_MANAGER_PATH = "/devices-manager";
static const DEVICES_UPDATE_PATH = "/device-update";
static const DEVICES_DETAIL_PATH = "/device";
static const MAP_PATH = "/map";
static const DEVICE_LOGS_PATH = "/device-logs";
static const GROUP_PATH = "/groups";
static const DEVICE_NOTIFICATIONS_SETTINGS = "/device-notifications-settings";
static const OWNER_GROUP = "owner";
static const PARTICIPANT_GROUP = "participant";
}

View File

@@ -0,0 +1,16 @@
// ignore_for_file: constant_identifier_names
enum AppRoutes {
LOGIN,
HOME,
SETTINGS,
DEVICE_NOTIFICATION_SETTINGS,
BELL,
DEVICES,
DEVICE_UPDATE,
DEVICE_DETAIL,
MAP,
HISTORY,
GROUPS,
GROUP_DETAIL,
}

View File

@@ -0,0 +1,3 @@
// ignore_for_file: constant_identifier_names
enum AppThemes { LIGHT, DARK, SYSTEM }

View File

@@ -0,0 +1,10 @@
// ignore_for_file: constant_identifier_names
enum PreferencesKeys{
TOKEN,
UID,
EXP,
ROLE,
LANGUAGE_CODE,
THEME
}

View File

@@ -0,0 +1,7 @@
// ignore_for_file: constant_identifier_names
enum RoleEnums{
USER,
ADMIN,
MOD
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
class IconConstants {
IconConstants._init();
static IconConstants? _instance;
static IconConstants get instance => _instance ??= IconConstants._init();
String get logo => getIcon("");
String getIcon(String name) => "assets/icons/$name.png";
Icon getMaterialIcon(IconData icon) => Icon(icon);
}

View File

@@ -0,0 +1,9 @@
class ImageConstants {
ImageConstants._init();
static ImageConstants? _instance;
static ImageConstants get instance => _instance ??= ImageConstants._init();
String get logo => getImage("");
String getImage(String name) => "assets/images/$name.png";
}

View File

@@ -0,0 +1,6 @@
// ignore_for_file: constant_identifier_names
class LanguageConstants {
static const ENGLISH = "en";
static const VIETNAM = "vi";
}

View File

@@ -0,0 +1,156 @@
import 'package:go_router/go_router.dart';
import 'package:sfm_app/feature/devices/device_detail/device_detail_bloc.dart';
import 'package:sfm_app/feature/devices/device_detail/device_detail_screen.dart';
import 'package:sfm_app/feature/settings/device_notification_settings/device_notification_settings_bloc.dart';
import 'package:sfm_app/feature/settings/device_notification_settings/device_notification_settings_screen.dart';
import '../app/app_constants.dart';
import '../../../feature/auth/login/bloc/login_bloc.dart';
import '../../../feature/auth/login/screen/login_screen.dart';
import '../../../feature/bell/bell_bloc.dart';
import '../../../feature/bell/bell_screen.dart';
import '../../../feature/devices/device_update/device_update_bloc.dart';
import '../../../feature/devices/device_update/device_update_screen.dart';
import '../../../feature/devices/devices_manager_bloc.dart';
import '../../../feature/devices/devices_manager_screen.dart';
import '../../../feature/error/not_found_screen.dart';
import '../../../feature/inter_family/group_detail/group_detail_bloc.dart';
import '../../../feature/inter_family/group_detail/group_detail_screen.dart';
import '../../../feature/inter_family/inter_family_bloc.dart';
import '../../../feature/inter_family/inter_family_screen.dart';
import '../../../feature/log/device_logs_bloc.dart';
import '../../../feature/log/device_logs_screen.dart';
import '../../../feature/main/main_bloc.dart';
import '../../../feature/main/main_screen.dart';
import '../../../feature/map/map_bloc.dart';
import '../../../feature/map/map_screen.dart';
import '../../../feature/settings/settings_bloc.dart';
import '../../../feature/settings/settings_screen.dart';
import '../../../product/base/bloc/base_bloc.dart';
import '../enums/app_route_enums.dart';
import '../../../product/shared/shared_transition.dart';
GoRouter goRouter() {
return GoRouter(
debugLogDiagnostics: true,
errorBuilder: (context, state) => const NotFoundScreen(),
initialLocation: ApplicationConstants.LOGIN_PATH,
routes: <RouteBase>[
GoRoute(
path: ApplicationConstants.LOGIN_PATH,
name: AppRoutes.LOGIN.name,
builder: (context, state) => BlocProvider(
child: const LoginScreen(),
blocBuilder: () => LoginBloc(),
),
),
GoRoute(
path: ApplicationConstants.HOME_PATH,
name: AppRoutes.HOME.name,
builder: (context, state) => BlocProvider(
child: const MainScreen(),
blocBuilder: () => MainBloc(),
),
),
GoRoute(
path: ApplicationConstants.SETTINGS_PATH,
name: AppRoutes.SETTINGS.name,
pageBuilder: (context, state) => CustomTransitionPage(
child: BlocProvider(
child: const SettingsScreen(),
blocBuilder: () => SettingsBloc(),
),
transitionsBuilder: transitionsBottomToTop,
),
),
GoRoute(
path: ApplicationConstants.BELL_PATH,
name: AppRoutes.BELL.name,
pageBuilder: (context, state) => CustomTransitionPage(
child: BlocProvider(
child: const BellScreen(),
blocBuilder: () => BellBloc(),
),
transitionsBuilder: transitionsCustom1),
),
GoRoute(
path: ApplicationConstants.DEVICES_MANAGER_PATH,
name: AppRoutes.DEVICES.name,
builder: (context, state) => BlocProvider(
child: const DevicesManagerScreen(),
blocBuilder: () => DevicesManagerBloc(),
),
),
GoRoute(
path: '${ApplicationConstants.DEVICES_UPDATE_PATH}/:thingID',
name: AppRoutes.DEVICE_UPDATE.name,
pageBuilder: (context, state) => CustomTransitionPage(
child: BlocProvider(
child: DeviceUpdateScreen(
thingID: state.pathParameters['thingID']!,
),
blocBuilder: () => DeviceUpdateBloc(),
),
transitionsBuilder: transitionsBottomToTop),
),
GoRoute(
path: '${ApplicationConstants.DEVICES_DETAIL_PATH}/:thingID',
name: AppRoutes.DEVICE_DETAIL.name,
pageBuilder: (context, state) => CustomTransitionPage(
child: BlocProvider(
child: DetailDeviceScreen(
thingID: state.pathParameters['thingID']!,
),
blocBuilder: () => DetailDeviceBloc(),
),
transitionsBuilder: transitionsRightToLeft),
),
GoRoute(
path: ApplicationConstants.MAP_PATH,
name: AppRoutes.MAP.name,
builder: (context, state) => BlocProvider(
child: const MapScreen(),
blocBuilder: () => MapBloc(),
),
),
GoRoute(
path: ApplicationConstants.DEVICE_LOGS_PATH,
name: AppRoutes.HISTORY.name,
builder: (context, state) => BlocProvider(
child: const DeviceLogsScreen(),
blocBuilder: () => DeviceLogsBloc(),
),
),
GoRoute(
path: ApplicationConstants.GROUP_PATH,
name: AppRoutes.GROUPS.name,
builder: (context, state) => BlocProvider(
child: const InterFamilyScreen(),
blocBuilder: () => InterFamilyBloc(),
),
),
GoRoute(
path: '${ApplicationConstants.GROUP_PATH}/:groupId',
name: AppRoutes.GROUP_DETAIL.name,
pageBuilder: (context, state) {
final groupId = state.pathParameters['groupId']!;
final role = state.extra! as String;
return CustomTransitionPage(
child: BlocProvider(
child: DetailGroupScreen(group: groupId, role: role),
blocBuilder: () => DetailGroupBloc(),
),
transitionsBuilder: transitionsRightToLeft);
}),
GoRoute(
path: ApplicationConstants.DEVICE_NOTIFICATIONS_SETTINGS,
name: AppRoutes.DEVICE_NOTIFICATION_SETTINGS.name,
pageBuilder: (context, state) => CustomTransitionPage(
child: BlocProvider(
child: const DeviceNotificationSettingsScreen(),
blocBuilder: () => DeviceNotificationSettingsBloc(),
),
transitionsBuilder: transitionsRightToLeft),
),
],
);
}

View File

@@ -0,0 +1,7 @@
// ignore_for_file: constant_identifier_names
class StatusCodeConstants {
static const CREATED = 201;
static const OK = 200;
static const BAD_REQUEST = 400;
}

View File

@@ -0,0 +1,103 @@
import 'dart:math';
import 'package:flutter/material.dart';
import '../theme/app_theme_light.dart';
// MEDIA
extension ContextExtension on BuildContext {
MediaQueryData get mediaQuery => MediaQuery.of(this);
}
// VALUES
extension MediaQueryExtension on BuildContext {
double get height => mediaQuery.size.height;
double get width => mediaQuery.size.width;
double get lowValue => height * 0.01;
double get normalValue => height * 0.02;
double get mediumValue => height * 0.04;
double get highValue => height * 0.1;
double dynamicWidth(double val) => width * val;
double dynamicHeight(double val) => height * val;
}
// THEME
extension ThemeExtension on BuildContext {
ThemeData get theme => Theme.of(this);
TextTheme get textTheme => theme.textTheme;
ColorScheme get colors => AppThemeLight.instance.theme.colorScheme;
}
// PADDING ALLL
extension PaddingExtensionAll on BuildContext {
EdgeInsets get paddingLow => EdgeInsets.all(lowValue);
EdgeInsets get paddingNormal => EdgeInsets.all(normalValue);
EdgeInsets get paddingMedium => EdgeInsets.all(mediumValue);
EdgeInsets get paddingHigh => EdgeInsets.all(highValue);
EdgeInsets dynamicPadding(double val) => EdgeInsets.all(val);
// double dynamicPadding(double val) => height * val;
}
// PADDING SYMETRIC
extension PaddingExtensionSymetric on BuildContext {
// VERTICAL PADDİNG
EdgeInsets get paddingLowVertical => EdgeInsets.symmetric(vertical: lowValue);
EdgeInsets get paddingNormalVertical =>
EdgeInsets.symmetric(vertical: normalValue);
EdgeInsets get paddingMediumVertical =>
EdgeInsets.symmetric(vertical: mediumValue);
EdgeInsets get paddingHighVertical =>
EdgeInsets.symmetric(vertical: highValue);
// HORIZONTAL PADDİNG
EdgeInsets get paddingLowHorizontal =>
EdgeInsets.symmetric(horizontal: lowValue);
EdgeInsets get paddingNormalHorizontal =>
EdgeInsets.symmetric(horizontal: normalValue);
EdgeInsets get paddingMediumHorizontal =>
EdgeInsets.symmetric(horizontal: mediumValue);
EdgeInsets get paddingHighHorizontal =>
EdgeInsets.symmetric(horizontal: highValue);
}
// RANDOM COLOR
extension PageExtension on BuildContext {
Color get randomColor => Colors.primaries[Random().nextInt(17)];
}
// DURATION
extension DurationExtension on BuildContext {
Duration get lowDuration => const Duration(milliseconds: 150);
Duration get normalDuration => const Duration(milliseconds: 500);
Duration dynamicSecondDuration(int seconds) => Duration(seconds: seconds);
Duration dynamicMinutesDuration(int minutes) => Duration(minutes: minutes);
}
// RADIUS
extension RadiusExtension on BuildContext {
Radius get lowRadius => Radius.circular(width * 0.02);
Radius get normalRadius => Radius.circular(width * 0.05);
Radius get highRadius => Radius.circular(width * 0.1);
}
extension TextStyleExtention on BuildContext {
TextStyle get labelSmallTextStyle => Theme.of(this).textTheme.labelSmall!;
TextStyle get labelMediumTextStyle => Theme.of(this).textTheme.labelMedium!;
TextStyle get labelLargeTextStyle => Theme.of(this).textTheme.labelLarge!;
TextStyle get bodySmallTextStyle => Theme.of(this).textTheme.bodySmall!;
TextStyle get bodyMediumTextStyle => Theme.of(this).textTheme.bodyMedium!;
TextStyle get bodyLargeTextStyle => Theme.of(this).textTheme.bodyLarge!;
TextStyle get titleSmallTextStyle => Theme.of(this).textTheme.titleSmall!;
TextStyle get titleMediumTextStyle => Theme.of(this).textTheme.titleMedium!;
TextStyle get titleLargeTextStyle => Theme.of(this).textTheme.titleLarge!;
TextStyle get headlineSmallTextStyle =>
Theme.of(this).textTheme.headlineSmall!;
TextStyle get headlineMediumTextStyle =>
Theme.of(this).textTheme.headlineMedium!;
TextStyle get headlineLargeTextStyle =>
Theme.of(this).textTheme.headlineLarge!;
}

View File

@@ -0,0 +1,242 @@
{
"description_NOTUSE": "This is english language in HomePage",
"home_page_name": "Home Page",
"vietnam_language": "Vietnamese",
"english_language": "English",
"notification": "Notifications:",
"profile_icon_title": "Settings",
"log_out": "Log out",
"log_out_content": "Are you sure you want to log out?",
"notification_description": "All devices are operating normally",
"button_fake_fire_message": "False fire alarm",
"in_progress_message": "In progress",
"smoke_detecting_message": "Smoke detecting!",
"smoke_detecting_message_lowercase": "smoke detecting!",
"disconnect_message_uppercase": "Disconnected",
"disconnect_message_lowercase": "disconnected",
"location_message": "Address: ",
"confirm_fake_fire_message": "Are you sure the fire is a false alarm?",
"confirm_fake_fire_body": "Please check carefully to ensure that this is just a normal incident. The fire department will confirm that this is a false alarm!",
"confirm_fake_fire_sure_message": "I''m sure",
"let_PCCC_handle_message": "Let the Fire Prevention and Fighting Team handle it!",
"overview_message": "Overview",
"total_nof_devices_message": "Total number of devices",
"active_devices_message": "Active",
"inactive_devices_message": "Inactive",
"warning_devices_message": "Warning",
"unused_devices_message": "Unused",
"description_NOTUSE1": "This is english language in DeviceManagerPage",
"device_manager_page_name": "Devices Manager",
"add_device_title": "Add new device",
"input_extID_device_input": "Devcice ID",
"input_extID_device_hintText": "Enter the device ID",
"input_name_device_device": "Device Name",
"input_name_device_hintText": "Enter the device Name",
"paginated_data_table_title": "List of devices",
"paginated_data_table_column_action": "Action",
"paginated_data_table_column_deviceName": "Device name",
"paginated_data_table_column_deviceStatus": "Status",
"paginated_data_table_column_deviceBaterry": "Battery",
"paginated_data_table_column_deviceSignal": "Signal",
"paginated_data_table_column_deviceTemperature": "Temperature",
"paginated_data_table_column_deviceHump": "Humidity",
"paginated_data_table_column_devicePower": "Power",
"delete_device_dialog_title": "Remove device",
"delete_device_dialog_content": "Are you sure you want to delete this device?",
"update_device_dialog_title": "Update device",
"update_device_dialog_location_title": "Device location",
"update_device_dialog_location_longitude": "Longitude",
"update_device_dialog_location_latitude": "Latitude",
"update_device_dialog_location_longitude_hintText": "Enter longitude",
"update_device_dialog_location_latitude_hintText": "Enter latitude",
"update_device_dialog_location_province_hintText": "Select Province/City",
"update_device_dialog_location_province_searchHint": "Find Province/City",
"update_device_dialog_location_district_hintText": "Select District",
"update_device_dialog_location_district_searchHint": "Find district",
"update_device_dialog_location_ward_hintText": "Select Ward/Commune",
"update_device_dialog_location_ward_searchHint": "Find Ward/Commune",
"update_device_dialog_maps_dialog_title": "Update location",
"update_device_dialog_search_location_hint": "Search Location",
"description_NOTUSE8": "This is english language in MapPositionPage",
"map_your_location": "Your Location",
"map_show_direction": "Give directions",
"map_nearby_hospital": "Nearby hospital",
"map_nearest_hospital": "Nearest hospital",
"map_nearby_firestation": "Nearby fire station",
"map_nearest_firestation": "Nearest fire station",
"map_result": "Result",
"map_always_opened": "Always open",
"map_openning": "Openning",
"map_closed": "Closed",
"map_no_results": "No results found",
"map_start": "Start",
"map_destination": "Destination",
"map_stream": "Stream",
"description_NOTUSE2": "This is english language in DeviceLogPage",
"device_log_page_name": "Devices Log",
"choose_device_dropdownButton": "Select device",
"choose_date_start_datePicker": "Start from",
"choose_date_end_datePicker": "End",
"main_no_data": "No data yet.",
"description_NOTUSE3": "This is english language in InterFamily",
"interfamily_page_name": "InterFamily",
"my_group_title": "My group",
"invite_group": "Joined group",
"add_new_group": "Add new group",
"join_group": "Join group",
"group_name_title": "Group Name",
"group_id_title": "Group ID",
"add_new_user_title": "Add user",
"share_group_title": "Share group",
"change_group_infomation_title": "Change Infomation",
"change_group_infomation_content": "Change group infomation",
"delete_group_title": "Delete group",
"delete_group_content": "Are you sure you want to delete this group?",
"leave_group_content": "Are you sure you want to leave this group?",
"dont_have_group": "No group yet",
"dont_join_group": "You haven''t joined any groups yet.",
"description_group": "Description",
"add_new_device_title": "Add new device",
"approve_user": "Approve members",
"devices_title": "Devices",
"device_title": "Device",
"member_title": "Members",
"leave_group_title": "Leave group",
"dont_have_device": "No device yet",
"description_NOTUSE4": "This is english language in ProfilePage",
"profile_page_title": "Settings Page",
"profile_change_info": "Change information",
"profile_change_pass": "Change password",
"profile_setting": "Notification Setting",
"change_profile_title": "Personal information",
"change_profile_username": "Username: ",
"change_profile_username_hint": "Enter username ",
"change_profile_email": "Email: ",
"change_profile_email_hint": "Enter email ",
"change_profile_email_not_empty": "Email cannot be empty",
"change_profile_tel": "Phone number: ",
"change_profile_tel_hint": "Enter phone number",
"change_profile_tel_not_empty": "Phone number cannot be empty",
"change_profile_address": "Address: ",
"change_profile_address_hint": "Enter address",
"change_profile_old_pass": "Password: ",
"change_profile_old_pass_hint": "Enter password",
"change_profile_old_pass_not_empty": "Old password cannot be empty",
"change_profile_new_pass": "New password: ",
"change_profile_new_pass_hint": "Enter new password",
"change_profile_new_pass_not_empty": "New password cannot be empty",
"change_profile_device_notification_select_all": "Select all",
"change_profile_device_notification_deselect_all": "Deselect all",
"description_NOTUSE5": "This is english language in BellPage",
"bell_page_title": "Notifications",
"bell_page_no_items_body": "No notifications yet",
"bell_user_uppercase": "User",
"bell_battery_device": "Device Battery",
"bell_user_joined_group": "joined group",
"bell_leave_group": "left group",
"bell_user_added_group": "added to the group",
"bell_user_kick_group": "removed from the group",
"bell_operate_normal": "operating normally",
"bell_invalid_code": "Invalid event code",
"bell_days_ago": "days ago",
"bell_hours_ago": "hours ago",
"bell_minutes_ago": "minutes ago",
"bell_just_now": "just now",
"bell_read_all": "You have read all the notifications",
"description_NOTUSE6": "This is english language in GlobalFunction",
"gf_newly_create_message": "Newly created",
"gf_disconnect_message": "Disconnected",
"gf_smoke_detected_message": "Smoke detected",
"gf_no_signal_message": "No Signal",
"gf_weak_signal_message": "Weak Signal",
"gf_moderate_signal_message": "Moderate signal",
"gf_good_signal_message": "Good signal",
"gf_volt_detect_message": "Voltage detected",
"gf_temp_detect_message": "Temperature detected",
"gf_hum_detect_message": "Humidity detected",
"gf_battery_detect_message": "Battery detected",
"gf_offline_message": "Offline",
"gf_in_firefighting_message": "In firefighting",
"gf_device_error_message": "Device error",
"gf_not_move_message": "Not moved",
"gf_moving_message": "Moved",
"gf_remove_from_base_message": "Removed from the base",
"gf_connected_lowercase": "connected",
"description_NOTUSE7": "This is english language in LoginPage",
"login_account_not_empty": "Account cannot be empty",
"login_account_hint": "Account",
"login_password_not_empty": "Password cannot be empty",
"login_password_hint": "Password",
"login_success_message": "Login successful",
"login_incorrect_usernameOrPass": "Incorrect account or password",
"login_button_content": "Login",
"description_NOTUSE9": "This is english language in DeviceUpdatePage",
"device_update_title": "Update Device",
"device_update_location": "Device Location",
"device_update_province": "Province/City",
"device_update_district": "District",
"device_update_ward": "Ward/Commune",
"description_NOTUSE10": "This is english language in DetailDevicePage",
"detail_device_dont_has_location_message": "No location information available yet",
"no_data_message": "No data yet",
"normal_message": "Normal",
"warning_status_message": "Warning",
"undefine_message": "Undefined",
"low_message_uppercase": "Low",
"moderate_message_uppercase": "Moderate",
"good_message_uppercase": "Good",
"low_message_lowercase": "low",
"moderate_message_lowercase": "moderate",
"good_message_lowercase": "good",
"error_message_uppercase": "Error",
"error_message_lowercase": "error",
"warning_message": "Warning: ",
"loading_message": "Loading...",
"detail_message": "Detail",
"decline_message": "DECLINE",
"allow_message": "ALLOW",
"add_button_content": "Add",
"update_button_content": "Update",
"change_button_content": "Change",
"confirm_button_content": "Confirm",
"delete_button_content": "Delete",
"cancel_button_content": "Cancel",
"find_button_content": "Find",
"home_page_destination": "Home",
"manager_page_destination": "Manager",
"map_page_destination": "Map",
"history_page_destination": "History",
"history_page_destination_tooltip": "Device history",
"group_page_destination": "Group",
"group_page_destination_tooltip": "Exchange device notifications",
"notification_enter_all_inf": "Please enter all the required information",
"notification_update_device_success": "Device update successfully",
"notification_update_device_failed": "Device update failed",
"notification_update_device_error": "Device update Error",
"notification_cannot_find_address_from_location": "Can''t find the location",
"notification_add_device_success": "Device added successfully",
"notification_add_device_failed": "Failed to add device",
"notification_create_device_success": "Device created successfully",
"notification_create_device_failed": "Failed to create device",
"notification_delete_device_success": "Device deleted successfully",
"notification_delete_device_failed": "Failed to delete device",
"notification_device_not_exist": "The device does not exist",
"notification_add_group_success": "Group created successfully",
"notification_add_group_failed": "Failed to create group",
"notification_update_group_success": "Group updated successfully",
"notification_update_group_failed": "Failed to updated group",
"notification_delete_group_success": "Group deleted successfully",
"notification_delete_group_failed": "Failed to delete group",
"notification_leave_group_success": "Leave group successfully",
"notification_leave_group_failed": "Failed to leave group",
"notification_join_request_group_success": "Group join request successful!",
"notification_join_request_group_failed": "Group join request failed!",
"notification_update_profile_success": "Update profile successfully",
"notification_update_profile_failed": "Failed to update profile",
"notification_update_password_success": "Change password successfully",
"notification_update_password_failed": "The old password does not match",
"notification_update_device_settings_success": "Device notification updated successfully",
"notification_update_device_settings_failed": "Failed to update device notification",
"notification_confirm_fake_fire_success": "Information has been updated to the Fire Station",
"notification_confirm_fake_fire_failed": "Failed to update confirm fake fire"
}

View File

@@ -0,0 +1,242 @@
{
"description_NOTUSE": "This is VietNam language in HomePage",
"home_page_name": "Trang chủ",
"vietnam_language": "Tiếng Việt",
"english_language": "Tiếng Anh",
"notification": "Thông báo:",
"profile_icon_title": "Cài đặt",
"log_out": "Đăng xuất",
"log_out_content": "Bạn chắc chắn muốn đăng xuất?",
"notification_description": "Tất cả thiết bị hoạt động bình thường",
"button_fake_fire_message": "Cháy giả?",
"in_progress_message": "Đang xử lý",
"smoke_detecting_message": "Phát hiện khói!",
"smoke_detecting_message_lowercase": "Phát hiện khói!",
"disconnect_message_uppercase": "Mất kết nối",
"disconnect_message_lowercase": "mất kết nối",
"location_message": "Địa chỉ: ",
"confirm_fake_fire_message": "Bạn chắc chắn đám cháy là cháy giả?",
"confirm_fake_fire_body": "Bạn hãy kiểm tra thật kỹ để chắc chắn rằng đây chỉ là sự cố bình thường. Đội PCCC sẽ xác nhận đây là đám cháy giả!",
"confirm_fake_fire_sure_message": "Tôi chắc chắn",
"let_PCCC_handle_message": "Hãy để Đội PCCC xử lý!",
"overview_message": "Tổng quan",
"total_nof_devices_message": "Tổng số",
"active_devices_message": "Đang hoạt động",
"inactive_devices_message": "Đang tắt",
"warning_devices_message": "Cảnh báo",
"unused_devices_message": "Không sử dụng",
"description_NOTUSE1": "This is vietnamese language in DeviceManagerPage",
"device_manager_page_name": "Quản lý thiết bị",
"add_device_title": "Thêm thiết bị",
"input_extID_device_input": "Mã thiết bị",
"input_extID_device_hintText": "Nhập mã thiết bị",
"input_name_device_device": "Tên thiết bị",
"input_name_device_hintText": "Nhập tên thiết bị",
"paginated_data_table_title": "Danh sách thiết bị",
"paginated_data_table_column_action": "Thao tác",
"paginated_data_table_column_deviceName": "Tên thiết bị",
"paginated_data_table_column_deviceStatus": "Tình trạng",
"paginated_data_table_column_deviceBaterry": "Mức pin",
"paginated_data_table_column_deviceSignal": "Mức sóng",
"paginated_data_table_column_deviceTemperature": "Nhiệt độ",
"paginated_data_table_column_deviceHump": "Độ ẩm",
"paginated_data_table_column_devicePower": "Nguồn",
"delete_device_dialog_title": "Xóa thiết bị",
"delete_device_dialog_content": "Bạn có chắc chắn muốn xóa thiết bị này?",
"update_device_dialog_title": "Sửa thiết bị",
"update_device_dialog_location_title": "Ví trí thiết bị",
"update_device_dialog_location_longitude": "Kinh độ",
"update_device_dialog_location_latitude": "Vĩ độ",
"update_device_dialog_location_longitude_hintText": "Nhập kinh độ",
"update_device_dialog_location_latitude_hintText": "Nhập vĩ độ",
"update_device_dialog_location_province_hintText": "Chọn Tỉnh/Thành phố",
"update_device_dialog_location_province_searchHint": "Tìm Tỉnh/Thành phố",
"update_device_dialog_location_district_hintText": "Chọn Quận/Huyện",
"update_device_dialog_location_district_searchHint": "Tìm Quận/Huyện",
"update_device_dialog_location_ward_hintText": "Chọn Phường/Xã",
"update_device_dialog_location_ward_searchHint": "Tìm Phường/Xã",
"update_device_dialog_maps_dialog_title": "Cập nhật vị trí",
"update_device_dialog_search_location_hint": "Tìm kiếm địa chỉ",
"description_NOTUSE8": "This is vietnamese language in MapPositionPage",
"map_your_location": "Vị trí của bạn",
"map_show_direction": "Chỉ đường",
"map_nearby_hospital": "Bệnh viện gần đó",
"map_nearest_hospital": "Bệnh viện gần nhất",
"map_nearby_firestation": "Trạm cứu hỏa gần đó",
"map_nearest_firestation": "Trạm cứu hỏa gần nhất",
"map_result": "Kết quả",
"map_always_opened": "Luôn mở cửa",
"map_openning": "Đang mở cửa",
"map_closed": "Đóng cửa",
"map_no_results": "Không tìm thấy kết quả",
"map_start": "Xuất phát",
"map_destination": "Đích đến",
"map_stream": "Trực tiếp",
"description_NOTUSE2": "This is vietnamese language in DeviceLogPage",
"device_log_page_name": "Lịch sử thiết bị",
"choose_device_dropdownButton": "Chọn thiết bị",
"choose_date_start_datePicker": "Bắt đầu từ",
"choose_date_end_datePicker": "Kết thúc",
"main_no_data": "Chưa có dữ liệu.",
"description_NOTUSE3": "This is vietnamese language in InterFamily",
"interfamily_page_name": "Liên gia",
"my_group_title": "Group của tôi",
"invite_group": "Group tham gia",
"add_new_group": "Thêm nhóm mới",
"join_group": "Tham gia nhóm",
"group_name_title": "Tên nhóm",
"group_id_title": "ID nhóm",
"add_new_user_title": "Thêm người dùng",
"share_group_title": "Chia sẻ nhóm",
"change_group_infomation_title": "Đổi thông tin",
"change_group_infomation_content": "Chỉnh sửa thông tin nhóm",
"delete_group_title": "Xóa nhóm",
"delete_group_content": "Bạn chắc chắn muốn xóa nhóm này?",
"leave_group_content": "Bạn chắc chắn muốn rời nhóm?",
"dont_have_group": "Chưa có nhóm",
"dont_join_group": "Bạn chưa tham gia nhóm nào",
"description_group": "Mô tả",
"add_new_device_title": "Thêm thiết bị mới",
"approve_user": "Duyệt thành viên",
"devices_title": "Thiết bị",
"device_title": "Thiết bị",
"member_title": "Thành viên",
"leave_group_title": "Rời nhóm",
"dont_have_device": "Chưa có thiết bị",
"description_NOTUSE4": "This is vietnamese language in ProfilePage",
"profile_page_title": "Cài đặt",
"profile_change_info": "Đổi thông tin cá nhân",
"profile_change_pass": "Đổi mật khẩu",
"profile_setting": "Cài đặt thông báo",
"change_profile_title": "Thông tin người dùng",
"change_profile_username": "Tên người dùng: ",
"change_profile_username_hint": "Nhập tên ",
"change_profile_email": "Email: ",
"change_profile_email_hint": "Nhập email ",
"change_profile_email_not_empty": "Email không được để trống",
"change_profile_tel": "Số điện thoại: ",
"change_profile_tel_hint": "Nhập số điện thoại",
"change_profile_tel_not_empty": "Số điện thoại không được để trống",
"change_profile_address": "Địa chỉ: ",
"change_profile_address_hint": "Nhập địa chỉ",
"change_profile_old_pass": "Mật khẩu cũ: ",
"change_profile_old_pass_hint": "Nhập mật khẩu cũ",
"change_profile_old_pass_not_empty": "Mật khẩu không được để trống",
"change_profile_new_pass": "Mật khẩu mới: ",
"change_profile_new_pass_hint": "Nhập mật khẩu mới",
"change_profile_new_pass_not_empty": "Mật khẩu không được để trống",
"change_profile_device_notification_select_all": "Chọn tất cả",
"change_profile_device_notification_deselect_all": "Bỏ chọn tất cả",
"description_NOTUSE5": "This is vietnamese language in BellPage",
"bell_page_title": "Thông báo",
"bell_page_no_items_body": "Chưa có thông báo",
"bell_user_uppercase": "Người dùng",
"bell_battery_device": "Pin thiết bị",
"bell_user_joined_group": "đã tham gia nhóm",
"bell_leave_group": "đã rời nhóm",
"bell_user_added_group": "đã được thêm vào nhóm",
"bell_user_kick_group": "đã bị xóa khỏi nhóm",
"bell_operate_normal": "hoạt động bình thường",
"bell_invalid_code": "Mã sự kiện không hợp lệ",
"bell_days_ago": "ngày trước",
"bell_hours_ago": "giờ trước",
"bell_minutes_ago": "phút trước",
"bell_just_now": "Vừa xong",
"bell_read_all": "Bạn đã xem hết thông báo",
"description_NOTUSE6": "This is vietnamese language in GlobalFunction",
"gf_newly_create_message": "Mới tạo",
"gf_disconnect_message": "Mất kết nối",
"gf_smoke_detected_message": "Đang hoạt động",
"gf_no_signal_message": "Không có sóng",
"gf_weak_signal_message": "Mức sóng yếu",
"gf_moderate_signal_message": "Mức sóng khá",
"gf_good_signal_message": "Mức sóng tốt",
"gf_volt_detect_message": "Có điện thế",
"gf_temp_detect_message": "Có nhiệt độ",
"gf_hum_detect_message": "Có độ ẩm",
"gf_battery_detect_message": "Có mức pin",
"gf_offline_message": "Không hoạt động",
"gf_in_firefighting_message": "Đang chữa cháy",
"gf_device_error_message": "Thiết bị lỗi",
"gf_not_move_message": "Chưa di chuyển",
"gf_moving_message": "Đã di chuyển",
"gf_remove_from_base_message": "Bị tháo khỏi đế",
"gf_connected_lowercase": "đã kết nối",
"description_NOTUSE7": "This is vietnamese language in LoginPage",
"login_account_not_empty": "Tài khoản không được để trống",
"login_account_hint": "Tài khoản",
"login_password_not_empty": "Mật khẩu không được để trống",
"login_password_hint": "Mật khẩu",
"login_success_message": "Đăng nhập thành công",
"login_incorrect_usernameOrPass": "Tài khoản hoặc mật khẩu không đúng",
"login_button_content": "Đăng nhập",
"description_NOTUSE9": "This is vietnamese language in DeviceUpdatePage",
"device_update_title": "Chỉnh sửa chi tiết thiết bị",
"device_update_location": "Vị trí thiết bị",
"device_update_province": "Tỉnh/Thành phố",
"device_update_district": "Quận/Huyện",
"device_update_ward": "Phường/Xã",
"description_NOTUSE10": "This is vietnamese language in DetailDevicePage",
"detail_device_dont_has_location_message": "Chưa có thông tin về vị trí",
"no_data_message": "Chưa có",
"normal_message": "Bình thường",
"warning_status_message": "Cảnh báo",
"undefine_message": "Không xác định",
"low_message_uppercase": "Yếu",
"moderate_message_uppercase": "Khá",
"good_message_uppercase": "Tốt",
"low_message_lowercase": "yếu",
"moderate_message_lowercase": "khá",
"good_message_lowercase": "tốt",
"error_message_uppercase": "Lỗi",
"error_message_lowercase": "lỗi",
"warning_message": "Cảnh báo:",
"loading_message": "Đang tải...",
"detail_message": "Chi tiết",
"decline_message": "TỪ CHỐI",
"allow_message": "CHO PHÉP",
"add_button_content": "Thêm",
"update_button_content": "Cập nhật",
"change_button_content": "Chỉnh sửa",
"confirm_button_content": "Xác nhận",
"delete_button_content": "Xóa",
"cancel_button_content": "Hủy",
"find_button_content": "Tìm",
"home_page_destination": "Trang chủ",
"manager_page_destination": "Quản lý",
"map_page_destination": "Bản đồ",
"history_page_destination": "Lịch sử",
"history_page_destination_tooltip": "Lịch sử thiết bị",
"group_page_destination": "Nhóm",
"group_page_destination_tooltip": "Trao đổi thông báo thiết bị",
"notification_enter_all_inf": "Vui lòng điền đầy đủ thông tin",
"notification_update_device_success": "Cập nhật thiết bị thành công",
"notification_update_device_failed": "Cập nhật thiết bị thất bại",
"notification_update_device_error": "Cập nhật lỗi",
"notification_cannot_find_address_from_location": "Không tìm được vị trí",
"notification_add_device_success": "Thêm thiết bị thành công",
"notification_add_device_failed": "Thêm thiết bị thất bại",
"notification_create_device_success": "Tạo thiết bị thành công",
"notification_create_device_failed": "Tạo thiết bị thất bại",
"notification_delete_device_success": "Xóa thiết bị thành công",
"notification_delete_device_failed": "Xóa thiết bị thất bại",
"notification_device_not_exist": "Thiết bị không tồn tại",
"notification_add_group_success": "Tạo nhóm thành công",
"notification_add_group_failed": "Tạo nhóm thất bại",
"notification_update_group_success": "Sửa nhóm thành công",
"notification_update_group_failed": "Sửa nhóm thất bại",
"notification_delete_group_success": "Xóa nhóm thành công",
"notification_delete_group_failed": "Xóa nhóm thất bại",
"notification_leave_group_success": "Rời nhóm thành công",
"notification_leave_group_failed": "Rời nhóm thất bại",
"notification_join_request_group_success": "Yêu cầu tham gia nhóm thành công!",
"notification_join_request_group_failed": "Yêu cầu tham gia nhóm thất bại!",
"notification_update_profile_success": "Sửa thông tin thành công",
"notification_update_profile_failed": "Sửa thông tin thất bại",
"notification_update_password_success": "Đổi mật khẩu thành công",
"notification_update_password_failed": "Mật khẩu cũ không khớp",
"notification_update_device_settings_success": "Cập nhật thông báo cho thiết bị thành công",
"notification_update_device_settings_failed": "Cập nhật thông báo cho thiết bị thất bại",
"notification_confirm_fake_fire_success": "Đã cập nhật thông tin đến đội PCCC",
"notification_confirm_fake_fire_failed": "Cập nhật cháy giả thất bại"
}

View File

@@ -0,0 +1,18 @@
import 'package:sfm_app/product/constant/icon/icon_constants.dart';
import 'package:sfm_app/product/constant/lang/language_constants.dart';
class Language {
final int id;
final String imgageIcon;
final String name;
final String languageCode;
Language(this.id, this.imgageIcon, this.name, this.languageCode);
static List<Language> languages() {
return <Language>[
Language(1, IconConstants.instance.getIcon("vi_icon"), "vn", LanguageConstants.VIETNAM),
Language(2, IconConstants.instance.getIcon("en_icon"), "en", LanguageConstants.VIETNAM),
];
}
}

View File

@@ -0,0 +1,114 @@
import 'dart:convert';
import 'dart:developer';
import 'package:sfm_app/product/constant/status_code/status_code_constants.dart';
import '../cache/local_manager.dart';
import '../constant/app/app_constants.dart';
import '../constant/enums/local_keys_enums.dart';
import 'package:http/http.dart' as http;
class NetworkManager {
NetworkManager._init();
static NetworkManager? _instance;
static NetworkManager? get instance => _instance ??= NetworkManager._init();
Future<Map<String, String>> getHeaders() async {
String? token =
LocaleManager.instance.getStringValue(PreferencesKeys.TOKEN);
Map<String, String> headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
"Access-Control-Allow-Credentials": "false",
"Access-Control-Allow-Headers":
"Origin,Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,locale",
'Access-Control-Allow-Origin': "*",
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Authorization': token,
};
return headers;
}
/// Retrieves data from the server using a GET request.
///
/// [path] is the endpoint for the request. Returns the response body as a
/// [String] if the request is successful (status code 200), or an empty
/// string if the request fails
Future<String> getDataFromServer(String path) async {
final url = Uri.https(ApplicationConstants.DOMAIN, path);
log("GET url: $url");
final headers = await getHeaders();
final response = await http.get(url, headers: headers);
if (response.statusCode == StatusCodeConstants.OK ||
response.statusCode == StatusCodeConstants.CREATED) {
return response.body;
} else {
return "";
}
}
/// Sends a GET request to the server with the specified parameters.
///
/// This function constructs a URL from the provided [path] and [params],
/// then sends an HTTP GET request to the server. If the response has a
/// status code of 200, the function returns the response body.
/// Otherwise, it returns an empty string.
///
/// [path] is the endpoint on the server.
/// [params] is a map containing query parameters for the request.
///
/// Returns a [Future<String>] containing the server response body.
Future<String> getDataFromServerWithParams(
String path, Map<String, dynamic> params) async {
final url = Uri.https(ApplicationConstants.DOMAIN, path, params);
log("GET Params url: $url");
final headers = await getHeaders();
final response = await http.get(url, headers: headers);
if (response.statusCode == StatusCodeConstants.CREATED ||
response.statusCode == StatusCodeConstants.OK) {
return response.body;
} else {
return "";
}
}
/// Creates new data on the server using a POST request.
///
/// [path] is the endpoint for the request, and [body] contains the data
/// to be sent. Returns the HTTP status code of the response.
Future<int> createDataInServer(String path, Map<String, dynamic> body) async {
final url = Uri.https(ApplicationConstants.DOMAIN, path);
log("POST url: $url");
final headers = await getHeaders();
final response =
await http.post(url, headers: headers, body: jsonEncode(body));
return response.statusCode;
}
/// Updates existing data on the server using a PUT request.
///
/// [path] is the endpoint for the request, and [body] contains the data
/// to be updated. Returns the HTTP status code of the response.
Future<int> updateDataInServer(String path, Map<String, dynamic> body) async {
final url = Uri.https(ApplicationConstants.DOMAIN, path);
log("PUT url: $url");
final headers = await getHeaders();
final response =
await http.put(url, headers: headers, body: jsonEncode(body));
return response.statusCode;
}
/// Deletes data from the server using a DELETE request.
///
/// [path] is the endpoint for the request. Returns the HTTP status code
/// of the response, indicating the result of the deletion operation.
/// A status code of 200 indicates success, while other codes indicate
/// failure or an error.
Future<int> deleteDataInServer(String path) async {
final url = Uri.https(ApplicationConstants.DOMAIN, path);
log("DELETE url: $url");
final headers = await getHeaders();
final response = await http.delete(url, headers: headers);
return response.statusCode;
}
}

View File

@@ -0,0 +1,31 @@
import 'dart:developer';
import 'package:app_settings/app_settings.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import '../base/widget/dialog/request_permission_dialog.dart';
class NotificationPermission {
FirebaseMessaging messaging = FirebaseMessaging.instance;
void requestNotificationsPermission(BuildContext context) async {
NotificationSettings settings = await messaging.requestPermission(
alert: true,
announcement: true,
badge: true,
carPlay: true,
criticalAlert: true,
provisional: true,
sound: true);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
log("NotificationsPermission: User granted permission");
} else if (settings.authorizationStatus ==
AuthorizationStatus.provisional) {
log("NotificationsPermission: User granted provisional permission");
} else {
log("NotificationsPermission: User denied permission");
// ignore: use_build_context_synchronously
RequestPermissionDialog().showRequestPermissionDialog(context,
Icons.location_on_outlined, "ABCDE", AppSettingsType.notification);
}
}
}

View File

@@ -0,0 +1,386 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import '../constant/app/api_path_constant.dart';
import '../shared/shared_snack_bar.dart';
import '../constant/enums/app_route_enums.dart';
import 'language_services.dart';
import '../../feature/bell/bell_model.dart';
import '../cache/local_manager.dart';
import '../constant/app/app_constants.dart';
import '../constant/enums/local_keys_enums.dart';
import '../network/network_manager.dart';
class APIServices {
Map<String, String> headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
"Access-Control-Allow-Credentials": "false",
"Access-Control-Allow-Headers":
"Origin,Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,locale",
'Access-Control-Allow-Origin': "*",
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
};
Future<Map<String, String>> getHeaders() async {
String? token =
LocaleManager.instance.getStringValue(PreferencesKeys.TOKEN);
Map<String, String> headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
"Access-Control-Allow-Credentials": "false",
"Access-Control-Allow-Headers":
"Origin,Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,locale",
'Access-Control-Allow-Origin': "*",
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Authorization': token,
};
return headers;
}
Future<String> login(String path, Map<String, dynamic> loginRequest) async {
final url = Uri.https(ApplicationConstants.DOMAIN, path);
final headers = await getHeaders();
final response =
await http.post(url, headers: headers, body: jsonEncode(loginRequest));
return response.body;
}
Future<void> logOut(BuildContext context) async {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(appLocalization(context).log_out_content,
textAlign: TextAlign.center),
actions: [
TextButton(
onPressed: () async {
var url = Uri.http(ApplicationConstants.DOMAIN,
APIPathConstants.LOGOUT_PATH);
final headers = await NetworkManager.instance!.getHeaders();
final response = await http.post(url, headers: headers);
if (response.statusCode == 200) {
LocaleManager.instance
.deleteStringValue(PreferencesKeys.UID);
LocaleManager.instance
.deleteStringValue(PreferencesKeys.TOKEN);
LocaleManager.instance
.deleteStringValue(PreferencesKeys.EXP);
LocaleManager.instance
.deleteStringValue(PreferencesKeys.ROLE);
context.goNamed(AppRoutes.LOGIN.name);
} else {
showErrorTopSnackBarCustom(
context, "Error: ${response.statusCode}");
}
},
child: Text(appLocalization(context).log_out,
style: const TextStyle(color: Colors.red)),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(appLocalization(context).cancel_button_content),
),
],
));
}
Future<String> getUID() async {
String uid = LocaleManager.instance.getStringValue(PreferencesKeys.UID);
return uid;
}
Future<String> getUserRole() async {
String role = LocaleManager.instance.getStringValue(PreferencesKeys.ROLE);
return role;
}
Future<String> checkTheme() async {
String theme = LocaleManager.instance.getStringValue(PreferencesKeys.THEME);
return theme;
}
Future<String> checkLanguage() async {
String language =
LocaleManager.instance.getStringValue(PreferencesKeys.LANGUAGE_CODE);
return language;
}
Future<Bell> getBellNotifications(String offset, String pagesize) async {
Bell bell = Bell();
final params = {"offset": offset, "page_size": pagesize};
final data = await NetworkManager.instance!.getDataFromServerWithParams(
APIPathConstants.BELL_NOTIFICATIONS_PATH, params);
if (data != "") {
bell = Bell.fromJson(jsonDecode(data));
return bell;
} else {
return bell;
}
}
Future<int> updateStatusOfNotification(List<String> notificationID) async {
Map<String, dynamic> body = {
"event_ids": notificationID,
};
int statusCode = await NetworkManager.instance!.updateDataInServer(
APIPathConstants.BELL_UPDATE_READ_NOTIFICATIONS_PATH, body);
return statusCode;
}
Future<String> getUserDetail() async {
String uid = await getUID();
String? response = await NetworkManager.instance!
.getDataFromServer('${APIPathConstants.USER_PATH}/$uid');
return response;
}
Future<int> updateUserProfile(Map<String, dynamic> body) async {
String uid = await getUID();
int statusCode = await NetworkManager.instance!
.updateDataInServer("${APIPathConstants.USER_PROFILE_PATH}/$uid", body);
return statusCode;
}
Future<int> updateUserPassword(Map<String, dynamic> body) async {
String uid = await getUID();
int statusCode = await NetworkManager.instance!.updateDataInServer(
"${APIPathConstants.USER_PATH}/$uid/password", body);
return statusCode;
}
Future<String> getAllSettingsNotificationOfDevices() async {
String? data = await NetworkManager.instance!
.getDataFromServer(APIPathConstants.DEVICE_NOTIFICATION_SETTINGS);
return data;
}
Future<int> updateDeviceNotificationSettings(
String thingID, Map<String, int> data) async {
Map<String, dynamic> body = {"thing_id": thingID, "notifi_settings": data};
int statusCode = await NetworkManager.instance!.updateDataInServer(
APIPathConstants.DEVICE_NOTIFICATION_SETTINGS, body);
return statusCode;
}
Future<String> getDashBoardDevices() async {
String? data = await NetworkManager.instance!
.getDataFromServer(APIPathConstants.DASHBOARD_DEVICES);
return data;
}
Future<int> setupDeviceNotification(String thingID, String deviceName) async {
Map<String, dynamic> body = {
"thing_id": thingID,
"alias": deviceName,
"notifi_settings": {
"001": 1,
"002": 1,
"003": 1,
"004": 1,
"005": 1,
"006": 1,
"101": 1,
"102": 1,
"103": 1,
"104": 1,
}
};
int statusCode = await NetworkManager.instance!.updateDataInServer(
APIPathConstants.DEVICE_NOTIFICATION_SETTINGS, body);
return statusCode;
}
Future<String> getAllProvinces() async {
String? data = await NetworkManager.instance!
.getDataFromServer(APIPathConstants.PROVINCES_PATH);
return data;
}
Future<String> getProvincesByName(String name) async {
final params = {'name': name};
String? data = await NetworkManager.instance!
.getDataFromServerWithParams(APIPathConstants.PROVINCES_PATH, params);
return data;
}
Future<String> getProvinceByID(String provinceID) async {
String data = await NetworkManager.instance!
.getDataFromServer("${APIPathConstants.PROVINCES_PATH}/$provinceID");
return data;
}
Future<String> getAllDistricts(String provinceID) async {
final params = {"parent": provinceID};
String? data = await NetworkManager.instance!
.getDataFromServerWithParams(APIPathConstants.DISTRICTS_PATH, params);
return data;
}
Future<String> getDistrictsByName(String districtName) async {
final params = {"name": districtName};
String? data = await NetworkManager.instance!
.getDataFromServerWithParams(APIPathConstants.DISTRICTS_PATH, params);
return data;
}
Future<String> getDistrictByID(String districtID) async {
String? data = await NetworkManager.instance!
.getDataFromServer("${APIPathConstants.DISTRICTS_PATH}/$districtID");
return data;
}
Future<String> getAllWards(String districtID) async {
final params = {'parent': districtID};
String? data = await NetworkManager.instance!
.getDataFromServerWithParams(APIPathConstants.WARDS_PATH, params);
return data;
}
Future<String> getWarsdByName(String wardName) async {
final params = {"name": wardName};
String? data = await NetworkManager.instance!
.getDataFromServerWithParams(APIPathConstants.WARDS_PATH, params);
return data;
}
Future<String> getWardByID(String wardID) async {
String? data = await NetworkManager.instance!
.getDataFromServer("${APIPathConstants.WARDS_PATH}/$wardID");
return data;
}
Future<int> confirmFakeFireByUser(String thingID) async {
Map<String, dynamic> body = {
"state": 3,
"note": "Người dùng xác nhận cháy giả!"
};
int statusCode = await NetworkManager.instance!
.updateDataInServer("${APIPathConstants.DEVICE_PATH}/$thingID", body);
return statusCode;
}
Future<String> getOwnerDevices() async {
String? data = await NetworkManager.instance!
.getDataFromServer(APIPathConstants.DEVICE_PATH);
return data;
}
Future<int> createDeviceByAdmin(Map<String, dynamic> body) async {
int? statusCode = await NetworkManager.instance!
.createDataInServer(APIPathConstants.DEVICE_PATH, body);
return statusCode;
}
Future<int> registerDevice(Map<String, dynamic> body) async {
int? statusCode = await NetworkManager.instance!
.createDataInServer(APIPathConstants.DEVICE_REGISTER_PATH, body);
return statusCode;
}
Future<int> deleteDeviceByAdmin(String thingID) async {
int statusCode = await NetworkManager.instance!
.deleteDataInServer("${APIPathConstants.DEVICE_PATH}/$thingID");
return statusCode;
}
Future<int> unregisterDevice(Map<String, dynamic> body) async {
int statusCode = await NetworkManager.instance!
.createDataInServer(APIPathConstants.DEVICE_UNREGISTER_PATH, body);
return statusCode;
}
Future<String> getDeviceInfomation(String thingID) async {
String? response = await NetworkManager.instance!
.getDataFromServer("${APIPathConstants.DEVICE_PATH}/$thingID");
return response;
}
Future<String> getAllGroups() async {
String? body = await NetworkManager.instance!
.getDataFromServer(APIPathConstants.ALL_GROUPS_PATH);
return body;
}
Future<int> createGroup(Map<String, dynamic> body) async {
int? statusCode = await NetworkManager.instance!
.createDataInServer(APIPathConstants.GROUPS_PATH, body);
return statusCode;
}
Future<int> updateGroup(Map<String, dynamic> body, String groupID) async {
int? statusCode = await NetworkManager.instance!
.updateDataInServer("${APIPathConstants.GROUPS_PATH}/$groupID", body);
return statusCode;
}
Future<int> joinGroup(String groupID, Map<String, dynamic> body) async {
int? statusCode = await NetworkManager.instance!
.createDataInServer(APIPathConstants.JOIN_GROUP_PATH, body);
return statusCode;
}
Future<int> deleteGroup(String groupID) async {
int? statusCode = await NetworkManager.instance!
.deleteDataInServer("${APIPathConstants.GROUPS_PATH}/$groupID");
return statusCode;
}
Future<String> getGroupDetail(String groupID) async {
String? body = await NetworkManager.instance!
.getDataFromServer("${APIPathConstants.GROUPS_PATH}/$groupID");
return body;
}
Future<int> approveGroup(Map<String, dynamic> body) async {
int statusCode = await NetworkManager.instance!
.createDataInServer(APIPathConstants.APPROVE_GROUP_PATH, body);
return statusCode;
}
Future<int> deleteUserInGroup(String groupID, String userID) async {
int? statusCode = await NetworkManager.instance!.deleteDataInServer(
"${APIPathConstants.GROUPS_PATH}/$groupID/users/$userID");
return statusCode;
}
Future<int> deleteDeviceInGroup(String groupID, String thingID) async {
int? statusCode = await NetworkManager.instance!.deleteDataInServer(
"${APIPathConstants.GROUPS_PATH}/$groupID/devices/$thingID");
return statusCode;
}
Future<int> updateDeviceAlias(Map<String, dynamic> body) async {
int? statusCode = await NetworkManager.instance!.updateDataInServer(
APIPathConstants.DEVICE_NOTIFICATION_SETTINGS, body);
return statusCode;
}
Future<int> addDeviceToGroup(
String groupID, Map<String, dynamic> body) async {
int? statusCode = await NetworkManager.instance!.createDataInServer(
"${APIPathConstants.GROUPS_PATH}/$groupID/things", body);
return statusCode;
}
Future<int> updateOwnerDevice(
String thingID, Map<String, dynamic> body) async {
int? statusCode = await NetworkManager.instance!
.updateDataInServer("${APIPathConstants.DEVICE_PATH}/$thingID", body);
return statusCode;
}
Future<String> getLogsOfDevice(
String thingID, Map<String, dynamic> params) async {
String? body = await NetworkManager.instance!
.getDataFromServerWithParams(APIPathConstants.DEVICE_LOGS_PATH, params);
return body;
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import '../cache/local_manager.dart';
import '../constant/enums/local_keys_enums.dart';
import '../constant/lang/language_constants.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LanguageServices {
Future<Locale> setLocale(String languageCode) async {
await LocaleManager.prefrencesInit();
LocaleManager.instance
.setStringValue(PreferencesKeys.LANGUAGE_CODE, languageCode);
return _locale(languageCode);
}
Future<Locale> getLocale() async {
await LocaleManager.prefrencesInit();
String languageCode =
LocaleManager.instance.getStringValue(PreferencesKeys.LANGUAGE_CODE);
return _locale(languageCode);
}
Locale _locale(String languageCode) {
switch (languageCode) {
case LanguageConstants.ENGLISH:
return const Locale(LanguageConstants.ENGLISH, '');
case LanguageConstants.VIETNAM:
return const Locale(LanguageConstants.VIETNAM, '');
default:
return const Locale(LanguageConstants.VIETNAM, '');
}
}
}
AppLocalizations appLocalization(BuildContext context) {
return AppLocalizations.of(context)!;
}

View File

@@ -0,0 +1,63 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
import '../constant/app/app_constants.dart';
import '../shared/find_location_maps/model/prediction_model.dart';
import '../shared/model/near_by_search_model.dart';
class MapServices {
Future<List<PlaceDetails>> getNearbyPlaces(double latitude, double longitude,
String searchKey, int radius, String type) async {
List<PlaceDetails> result = [];
var url = Uri.parse(
'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$searchKey&language=vi&location=$latitude%2C$longitude&radius=$radius&strictbounds=true&type=$type&key=${ApplicationConstants.MAP_KEY}');
log("URL LIST: $url");
var response = await http.post(url);
final placesAutocompleteResponse =
PlacesAutocompleteResponse.fromJson(jsonDecode(response.body));
if (placesAutocompleteResponse.predictions != null) {
for (int i = 0; i < placesAutocompleteResponse.predictions!.length; i++) {
var url =
"https://maps.googleapis.com/maps/api/place/details/json?placeid=${placesAutocompleteResponse.predictions![i].placeId}&language=vi&key=${ApplicationConstants.MAP_KEY}";
log(url.toString());
Response response = await Dio().get(
url,
);
PlaceDetails placeDetails = PlaceDetails.fromJson(response.data);
result.add(placeDetails);
// displayLocation(placesAutocompleteResponse.predictions![i], i);
}
return result;
} else {
log("null");
return [];
}
}
Future<List<LatLng>> findTheWay(LatLng origin, LatLng destination) async {
List<LatLng> polylineCoordinates = [];
PolylinePoints polylinePoints = PolylinePoints();
PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
ApplicationConstants.MAP_KEY,
PointLatLng(origin.latitude, origin.longitude),
PointLatLng(destination.latitude, destination.longitude),
travelMode: TravelMode.driving,
optimizeWaypoints: true);
if (result.points.isNotEmpty) {
for (var point in result.points) {
polylineCoordinates.add(LatLng(point.latitude, point.longitude));
}
return polylineCoordinates;
} else {
log("Lỗi khi tìm đường");
return [];
}
}
}

View File

@@ -0,0 +1,103 @@
import 'dart:developer' as dev;
import 'dart:math' as math;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class NotificationServices {
FirebaseMessaging messaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
void firebaseInit(BuildContext context) async {
FirebaseMessaging.onMessage.listen((message) {
initNotifications(context, message);
showNotification(message);
dev.log(
"Title: ${message.notification!.title}, Body: ${message.notification!.body} from ${message.sentTime}");
});
}
Future<String> getDeviceToken() async {
String? token = await messaging.getToken();
return token!;
}
void isTokenRefresh() async {
messaging.onTokenRefresh.listen((newToken) {
dev.log("Refresh Firebase Messaging Token: $newToken");
});
}
void initNotifications(BuildContext context, RemoteMessage message) async {
var androidInitializationSettings =
const AndroidInitializationSettings('app_icon');
var iosInitializationSettings = const DarwinInitializationSettings();
var initializationSettings = InitializationSettings(
android: androidInitializationSettings, iOS: iosInitializationSettings);
await _flutterLocalNotificationsPlugin.initialize(initializationSettings,
onDidReceiveNotificationResponse: (payload) {
handleMessage(context, message);
});
}
Future<void> showNotification(RemoteMessage message) async {
AndroidNotificationChannel androidNotificationChannel =
AndroidNotificationChannel(
math.Random.secure().nextInt(1000000).toString(),
'high Important Notification',
importance: Importance.max);
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
androidNotificationChannel.id.toString(),
androidNotificationChannel.name.toString(),
sound: RawResourceAndroidNotificationSound(message.data['sound']),
channelDescription: "Channel description",
importance: androidNotificationChannel.importance,
priority: Priority.high,
ticker: 'ticker',
);
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentBanner: true,
presentSound: true,
);
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails);
Future.delayed(Duration.zero, () {
_flutterLocalNotificationsPlugin.show(0, message.notification!.title!,
message.notification!.body, notificationDetails);
});
}
void handleMessage(BuildContext context, RemoteMessage message) async {
if (message.data['type'] == "msj") {
// Navigator.push(context,
// MaterialPageRoute(builder: (context) => const MessageScreen()));
} else if (message.data['type'] == "warn") {
// Navigator.push(
// context, MaterialPageRoute(builder: (context) => const MapScreen()));
} else {
dev.log("Not found data");
}
}
Future<void> setupInteractMessage(BuildContext context) async {
// When app terminate
RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
// showNotification(initialMessage);
// ignore: use_build_context_synchronously
handleMessage(context, initialMessage);
}
// When app is inBackGround
FirebaseMessaging.onMessageOpenedApp.listen((message) {
handleMessage(context, message);
});
}
}

View File

@@ -0,0 +1,137 @@
import 'package:dio/dio.dart';
import 'error_response_model.dart';
class DioErrorHandler {
ErrorResponse errorResponse = ErrorResponse();
String errorDescription = "";
ErrorResponse handleDioError(DioException dioError) {
switch (dioError.type) {
case DioExceptionType.cancel:
errorResponse.message = "Request to API server was cancelled";
break;
case DioExceptionType.connectionTimeout:
errorResponse.message = "Connection timeout with API server";
break;
case DioExceptionType.unknown:
if ((dioError.message!.contains("RedirectException"))) {
errorResponse.message = dioError.message;
} else {
errorResponse.message = "Please check the internet connection";
}
break;
case DioExceptionType.receiveTimeout:
errorResponse.message = "Receive timeout in connection with API server";
break;
case DioExceptionType.badResponse:
try {
if (dioError.response?.data['message'] != null) {
errorResponse.message = dioError.response?.data['message'];
} else {
if ((dioError.response?.statusMessage ?? "").isNotEmpty) {
errorResponse.message = dioError.response?.statusMessage;
} else {
return _handleError(
dioError.response!.statusCode, dioError.response!.data);
}
}
} catch (e) {
if ((dioError.response?.statusMessage ?? "").isNotEmpty) {
errorResponse.message = dioError.response?.statusMessage;
} else {
return _handleError(
dioError.response!.statusCode, dioError.response!.data);
}
}
break;
case DioExceptionType.sendTimeout:
errorResponse.message = "Send timeout in connection with API server";
break;
default:
errorResponse.message = "Something went wrong";
break;
}
return errorResponse;
}
ErrorResponse _handleError(int? statusCode, dynamic error) {
switch (statusCode) {
case 400:
return getMas(error);
// case 401:
// return checkTokenExpire(error);
case 404:
return getMas(error);
case 403:
return getMas(error);
case 500:
errorResponse.message = 'Internal server error';
return errorResponse;
default:
return getUnKnownMes(error);
}
}
// checkTokenExpire(error) {
// // print("my error ${error}");
// if (error['msg'].toString().toLowerCase() ==
// "Token has expired".toLowerCase()) {
// UIData.tokenExpire(error['msg']);
// return;
// }
// errorResponse.message = error['msg'].toString();
// return errorResponse;
// }
getMas(dynamic error) {
print("myError ${error.runtimeType}");
if (error.runtimeType != String) {
errorResponse.message =
error['message'].toString(); //?? S.of(Get.context).something_wrong;
} else {
if (error['msg'] != null) {
errorResponse.message = error['msg'].toString();
} else {
errorResponse.message = "Something Wrong";
} //S.of(Get.context).something_wrong;
}
return errorResponse;
}
getUnKnownMes(dynamic error) {
if (error['msg'] != null) {
errorResponse.message = error['msg'].toString();
} else if (error['message'] != null) {
errorResponse.message = error['message'].toString();
} else {
errorResponse.message = "Something went wrong";
}
return errorResponse;
}
}
class ErrorHandler {
static final ErrorHandler _inst = ErrorHandler.internal();
ErrorHandler.internal();
factory ErrorHandler() {
return _inst;
}
ErrorResponse errorResponse = ErrorResponse();
ErrorResponse handleError(var error) {
if (error.runtimeType.toString().toLowerCase() ==
"_TypeError".toLowerCase()) {
// return error.toString();
errorResponse.message = "The Provided API key is invalid";
return errorResponse;
} else if (error is DioError) {
return DioErrorHandler().handleDioError(error);
}
errorResponse.message = "The Provided API key is invalid";
return errorResponse;
}
}

View File

@@ -0,0 +1,19 @@
class ErrorResponse {
String? message;
int? status;
ErrorResponse({this.message, this.status});
ErrorResponse.fromJson(Map<String, dynamic> json) {
message = json['message'];
status = json['status'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['message'] = message;
data['status'] = status;
return data;
}
}

View File

@@ -0,0 +1,234 @@
class PlaceDetails {
Result? result;
String? status;
PlaceDetails({this.result, this.status});
PlaceDetails.fromJson(Map<String, dynamic> json) {
result =
json['result'] != null ? Result.fromJson(json['result']) : null;
status = json['status'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (result != null) {
data['result'] = result!.toJson();
}
data['status'] = status;
return data;
}
}
class Result {
List<AddressComponents>? addressComponents;
String? adrAddress;
String? formattedAddress;
Geometry? geometry;
String? icon;
String? name;
List<Photos>? photos;
String? placeId;
String? reference;
String? scope;
List<String>? types;
String? url;
int? utcOffset;
String? vicinity;
String? website;
Result(
{this.addressComponents,
this.adrAddress,
this.formattedAddress,
this.geometry,
this.icon,
this.name,
this.photos,
this.placeId,
this.reference,
this.scope,
this.types,
this.url,
this.utcOffset,
this.vicinity,
this.website});
Result.fromJson(Map<String, dynamic> json) {
if (json['address_components'] != null) {
addressComponents = [];
json['address_components'].forEach((v) {
addressComponents!.add(AddressComponents.fromJson(v));
});
}
adrAddress = json['adr_address'];
formattedAddress = json['formatted_address'];
geometry = json['geometry'] != null
? Geometry.fromJson(json['geometry'])
: null;
icon = json['icon'];
name = json['name'];
if (json['photos'] != null) {
photos = [];
json['photos'].forEach((v) {
photos!.add(Photos.fromJson(v));
});
}
placeId = json['place_id'];
reference = json['reference'];
scope = json['scope'];
types = json['types'].cast<String>();
url = json['url'];
utcOffset = json['utc_offset'];
vicinity = json['vicinity'];
website = json['website'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (addressComponents != null) {
data['address_components'] =
addressComponents!.map((v) => v.toJson()).toList();
}
data['adr_address'] = adrAddress;
data['formatted_address'] = formattedAddress;
if (geometry != null) {
data['geometry'] = geometry!.toJson();
}
data['icon'] = icon;
data['name'] = name;
if (photos != null) {
data['photos'] = photos!.map((v) => v.toJson()).toList();
}
data['place_id'] = placeId;
data['reference'] = reference;
data['scope'] = scope;
data['types'] = types;
data['url'] = url;
data['utc_offset'] = utcOffset;
data['vicinity'] = vicinity;
data['website'] = website;
return data;
}
}
class AddressComponents {
String? longName;
String? shortName;
List<String>? types;
AddressComponents({this.longName, this.shortName, this.types});
AddressComponents.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['types'] = types;
return data;
}
}
class Geometry {
Location? location;
Viewport? viewport;
Geometry({this.location, this.viewport});
Geometry.fromJson(Map<String, dynamic> json) {
location = json['location'] != null
? Location.fromJson(json['location'])
: null;
viewport = json['viewport'] != null
? Viewport.fromJson(json['viewport'])
: null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (location != null) {
data['location'] = location!.toJson();
}
if (viewport != null) {
data['viewport'] = viewport!.toJson();
}
return data;
}
}
class Location {
double? lat;
double? lng;
Location({this.lat, this.lng});
Location.fromJson(Map<String, dynamic> json) {
lat = json['lat'];
lng = json['lng'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['lat'] = lat;
data['lng'] = lng;
return data;
}
}
class Viewport {
Location? northeast;
Location? southwest;
Viewport({this.northeast, this.southwest});
Viewport.fromJson(Map<String, dynamic> json) {
northeast = json['northeast'] != null
? Location.fromJson(json['northeast'])
: null;
southwest = json['southwest'] != null
? Location.fromJson(json['southwest'])
: null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (northeast != null) {
data['northeast'] = northeast!.toJson();
}
if (southwest != null) {
data['southwest'] = southwest!.toJson();
}
return data;
}
}
class Photos {
int? height;
List<String>? htmlAttributions;
String? photoReference;
int? width;
Photos({this.height, this.htmlAttributions, this.photoReference, this.width});
Photos.fromJson(Map<String, dynamic> json) {
height = json['height'];
htmlAttributions = json['html_attributions'].cast<String>();
photoReference = json['photo_reference'];
width = json['width'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['height'] = height;
data['html_attributions'] = htmlAttributions;
data['photo_reference'] = photoReference;
data['width'] = width;
return data;
}
}

View File

@@ -0,0 +1,157 @@
class PlacesAutocompleteResponse {
List<Prediction>? predictions;
String? status;
PlacesAutocompleteResponse({this.predictions, this.status});
PlacesAutocompleteResponse.fromJson(Map<String, dynamic> json) {
if (json['predictions'] != null) {
predictions = [];
json['predictions'].forEach((v) {
predictions!.add(Prediction.fromJson(v));
});
}
status = json['status'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (predictions != null) {
data['predictions'] = predictions!.map((v) => v.toJson()).toList();
}
data['status'] = status;
return data;
}
}
class Prediction {
String? description;
String? id;
List<MatchedSubstrings>? matchedSubstrings;
String? placeId;
String? reference;
StructuredFormatting? structuredFormatting;
List<Terms>? terms;
List<String>? types;
String? lat;
String? lng;
Prediction(
{this.description,
this.id,
this.matchedSubstrings,
this.placeId,
this.reference,
this.structuredFormatting,
this.terms,
this.types,
this.lat,
this.lng});
Prediction.fromJson(Map<String, dynamic> json) {
description = json['description'];
id = json['id'];
if (json['matched_substrings'] != null) {
matchedSubstrings = [];
json['matched_substrings'].forEach((v) {
matchedSubstrings!.add(MatchedSubstrings.fromJson(v));
});
}
placeId = json['place_id'];
reference = json['reference'];
structuredFormatting = json['structured_formatting'] != null
? StructuredFormatting.fromJson(json['structured_formatting'])
: null;
if (json['terms'] != null) {
terms = [];
json['terms'].forEach((v) {
terms!.add(Terms.fromJson(v));
});
}
types = json['types'].cast<String>();
lat = json['lat'];
lng = json['lng'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['description'] = description;
data['id'] = id;
if (matchedSubstrings != null) {
data['matched_substrings'] =
matchedSubstrings!.map((v) => v.toJson()).toList();
}
data['place_id'] = placeId;
data['reference'] = reference;
if (structuredFormatting != null) {
data['structured_formatting'] = structuredFormatting!.toJson();
}
if (terms != null) {
data['terms'] = terms!.map((v) => v.toJson()).toList();
}
data['types'] = types;
data['lat'] = lat;
data['lng'] = lng;
return data;
}
}
class MatchedSubstrings {
int? length;
int? offset;
MatchedSubstrings({this.length, this.offset});
MatchedSubstrings.fromJson(Map<String, dynamic> json) {
length = json['length'];
offset = json['offset'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['length'] = length;
data['offset'] = offset;
return data;
}
}
class StructuredFormatting {
String? mainText;
String? secondaryText;
StructuredFormatting({this.mainText, this.secondaryText});
StructuredFormatting.fromJson(Map<String, dynamic> json) {
mainText = json['main_text'];
secondaryText = json['secondary_text'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['main_text'] = mainText;
data['secondary_text'] = secondaryText;
return data;
}
}
class Terms {
int? offset;
String? value;
Terms({this.offset, this.value});
Terms.fromJson(Map<String, dynamic> json) {
offset = json['offset'];
value = json['value'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['offset'] = offset;
data['value'] = value;
return data;
}
}

View File

@@ -0,0 +1,313 @@
// ignore_for_file: unnecessary_this, use_build_context_synchronously, prefer_typing_uninitialized_variables, library_private_types_in_public_api
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:rxdart/rxdart.dart';
import 'error/dio_handle_error.dart';
import 'model/place_detail_model.dart';
import 'model/prediction_model.dart';
// ignore: must_be_immutable
class NearBySearchSFM extends StatefulWidget {
InputDecoration inputDecoration;
ItemClick? itemClick;
GetPlaceDetailswWithLatLng? getPlaceDetailWithLatLng;
bool isLatLngRequired = true;
double locationLatitude;
double locationLongitude;
int radius;
TextStyle textStyle;
String googleAPIKey;
int debounceTime = 600;
List<String>? countries = [];
TextEditingController textEditingController = TextEditingController();
ListItemBuilder? itemBuilder;
TextInputAction? textInputAction;
Widget? seperatedBuilder;
void clearData;
BoxDecoration? boxDecoration;
bool isCrossBtnShown;
bool showError;
NearBySearchSFM(
{super.key,
required this.textEditingController,
required this.googleAPIKey,
required this.locationLatitude,
required this.locationLongitude,
required this.radius,
this.debounceTime = 600,
this.inputDecoration = const InputDecoration(),
this.itemClick,
this.isLatLngRequired = true,
this.textStyle = const TextStyle(),
this.countries,
this.getPlaceDetailWithLatLng,
this.itemBuilder,
this.boxDecoration,
this.isCrossBtnShown = true,
this.seperatedBuilder,
this.showError = true,
this.textInputAction});
@override
_NearBySearchSFMState createState() =>
_NearBySearchSFMState();
}
class _NearBySearchSFMState
extends State<NearBySearchSFM> {
final subject = PublishSubject<String>();
OverlayEntry? _overlayEntry;
List<Prediction> alPredictions = [];
TextEditingController controller = TextEditingController();
final LayerLink _layerLink = LayerLink();
bool isSearched = false;
bool isCrossBtn = true;
late var _dio;
CancelToken? _cancelToken = CancelToken();
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
alignment: Alignment.centerLeft,
decoration: widget.boxDecoration ??
BoxDecoration(
shape: BoxShape.rectangle,
border: Border.all(color: Colors.grey, width: 0.6),
borderRadius: const BorderRadius.all(Radius.circular(10))),
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormField(
decoration: widget.inputDecoration,
style: widget.textStyle,
controller: widget.textEditingController,
onChanged: (string) {
subject.add(string);
if (widget.isCrossBtnShown) {
isCrossBtn = string.isNotEmpty ? true : false;
setState(() {});
}
},
),
),
(!widget.isCrossBtnShown)
? const SizedBox()
: isCrossBtn && _showCrossIconWidget()
? IconButton(onPressed: clearData, icon: Icon(Icons.close))
: const SizedBox()
],
),
),
);
}
getLocation(
String text, double latitude, double longitude, int radius) async {
// String url =
// "https://maps.googleapis.com/maps/api/place/nearbysearch/json?keyword=$text&location=$latitude%2C$longitude&radius=$radius&type=hospital|pharmacy|doctor&key=${widget.googleAPIKey}";
String url =
"https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$text&location=$latitude%2C$longitude&radius=$radius&strictbounds=true&key=${widget.googleAPIKey}";
log(url);
if (widget.countries != null) {
for (int i = 0; i < widget.countries!.length; i++) {
String country = widget.countries![i];
if (i == 0) {
url = url + "&components=country:$country";
} else {
url = url + "|" + "country:" + country;
}
}
}
if (_cancelToken?.isCancelled == false) {
_cancelToken?.cancel();
_cancelToken = CancelToken();
}
try {
Response response = await _dio.get(url);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
Map map = response.data;
if (map.containsKey("error_message")) {
throw response.data;
}
PlacesAutocompleteResponse subscriptionResponse =
PlacesAutocompleteResponse.fromJson(response.data);
if (text.length == 0) {
alPredictions.clear();
this._overlayEntry!.remove();
return;
}
isSearched = false;
alPredictions.clear();
if (subscriptionResponse.predictions!.isNotEmpty &&
(widget.textEditingController.text.toString().trim()).isNotEmpty) {
alPredictions.addAll(subscriptionResponse.predictions!);
}
this._overlayEntry = null;
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry!);
} catch (e) {
var errorHandler = ErrorHandler.internal().handleError(e);
_showSnackBar("${errorHandler.message}");
}
}
@override
void initState() {
super.initState();
_dio = Dio();
subject.stream
.distinct()
.debounceTime(Duration(milliseconds: widget.debounceTime))
.listen(textChanged);
}
textChanged(String text) async {
getLocation(text, widget.locationLatitude, widget.locationLongitude,
widget.radius);
}
OverlayEntry? _createOverlayEntry() {
if (context.findRenderObject() != null) {
RenderBox renderBox = context.findRenderObject() as RenderBox;
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) => Positioned(
left: offset.dx,
top: size.height + offset.dy,
width: size.width,
child: CompositedTransformFollower(
showWhenUnlinked: false,
link: this._layerLink,
offset: Offset(0.0, size.height + 5.0),
child: Material(
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: alPredictions.length,
separatorBuilder: (context, pos) =>
widget.seperatedBuilder ?? SizedBox(),
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
var selectedData = alPredictions[index];
if (index < alPredictions.length) {
widget.itemClick!(selectedData);
if (widget.isLatLngRequired) {
getPlaceDetailsFromPlaceId(selectedData);
}
removeOverlay();
}
},
child: widget.itemBuilder != null
? widget.itemBuilder!(
context, index, alPredictions[index])
: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.all(5),
child: Text(alPredictions[index].description!)),
);
},
)),
),
));
}
return null;
}
removeOverlay() {
alPredictions.clear();
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry!);
this._overlayEntry!.markNeedsBuild();
}
Future<Response?> getPlaceDetailsFromPlaceId(Prediction prediction) async {
//String key = GlobalConfiguration().getString('google_maps_key');
var url =
"https://maps.googleapis.com/maps/api/place/details/json?placeid=${prediction.placeId}&key=${widget.googleAPIKey}";
Response response = await Dio().get(
url,
);
PlaceDetails placeDetails = PlaceDetails.fromJson(response.data);
prediction.lat = placeDetails.result!.geometry!.location!.lat.toString();
prediction.lng = placeDetails.result!.geometry!.location!.lng.toString();
widget.getPlaceDetailWithLatLng!(prediction);
return null;
}
void clearData() {
widget.textEditingController.clear();
if (_cancelToken?.isCancelled == false) {
_cancelToken?.cancel();
}
setState(() {
alPredictions.clear();
isCrossBtn = false;
});
if (this._overlayEntry != null) {
try {
this._overlayEntry?.remove();
} catch (e) {}
}
}
_showCrossIconWidget() {
return (widget.textEditingController.text.isNotEmpty);
}
_showSnackBar(String errorData) {
if (widget.showError) {
final snackBar = SnackBar(
content: Text("$errorData"),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
}
PlacesAutocompleteResponse parseResponse(Map responseBody) {
return PlacesAutocompleteResponse.fromJson(
responseBody as Map<String, dynamic>);
}
PlaceDetails parsePlaceDetailMap(Map responseBody) {
return PlaceDetails.fromJson(responseBody as Map<String, dynamic>);
}
typedef ItemClick = void Function(Prediction postalCodeResponse);
typedef GetPlaceDetailswWithLatLng = void Function(
Prediction postalCodeResponse);
typedef ListItemBuilder = Widget Function(
BuildContext context, int index, Prediction prediction);

View File

@@ -0,0 +1,43 @@
class District {
String? code;
String? name;
String? nameEn;
String? fullName;
String? fullNameEn;
String? codeName;
String? provinceCode;
String? type;
District({
this.code,
this.name,
this.nameEn,
this.fullName,
this.fullNameEn,
this.codeName,
this.provinceCode,
this.type,
});
District.fromJson(Map<String, dynamic> json) {
code = json['code'];
name = json['name'];
nameEn = json['name_en'];
fullName = json['full_name'];
fullNameEn = json['full_name_en'];
codeName = json['code_name'];
provinceCode = json['parent'];
type = json['type'];
}
static List<District> fromJsonList(List list) {
return list.map((e) => District.fromJson(e)).toList();
}
static List<District> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => District.fromJson(e)).toList();
}
// @override
// String toString() => fullName!;
}

View File

@@ -0,0 +1,172 @@
class NearbySearch {
List<Result>? results;
String? status;
NearbySearch({
this.results,
this.status,
});
NearbySearch.fromJson(Map<String, dynamic> json) {
if (json['results'] != null) {
results = [];
json['results'].forEach((v) {
results!.add(Result.fromJson(v));
});
}
status = json['status'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (results != null) {
data['result'] = results!.map((v) => v.toJson()).toList();
}
data['status'] = status;
return data;
}
}
class PlaceDetails {
Result? result;
String? status;
PlaceDetails({this.result, this.status});
PlaceDetails.fromJson(Map<String, dynamic> json) {
result = json['result'] != null ? Result.fromJson(json['result']) : null;
status = json['status'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (result != null) {
data['result'] = result!.toJson();
}
data['status'] = status;
return data;
}
static List<PlaceDetails> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => PlaceDetails.fromJson(e)).toList();
}
}
class Result {
Geometry? geometry;
String? icon;
String? name;
String? placeId;
String? vicinity;
String? adress;
String? formattedAddress;
String? formattedPhoneNumber;
String? phoneNumber;
OpeningHours? openingHours;
Result({
this.geometry,
this.icon,
this.name,
this.placeId,
this.vicinity,
this.formattedPhoneNumber,
this.phoneNumber,
this.openingHours,
this.adress,
this.formattedAddress,
});
Result.fromJson(Map<String, dynamic> json) {
geometry =
json['geometry'] != null ? Geometry.fromJson(json['geometry']) : null;
icon = json['icon'];
name = json['name'];
placeId = json['place_id'];
vicinity = json['vicinity'];
adress = json['adr_address'];
formattedPhoneNumber = json['formatted_phone_number'] ?? "";
phoneNumber = json['international_phone_number'] ?? "";
formattedAddress = json['formatted_address'];
openingHours = json['opening_hours'] != null
? OpeningHours.fromJson(json['opening_hours'])
: null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (geometry != null) {
data['geometry'] = geometry!.toJson();
}
data['icon'] = icon;
data['name'] = name;
data['vicinity'] = vicinity;
data['adr_address'] = adress;
data['formatted_address'] = formattedAddress;
data['international_phone_number'] = phoneNumber;
data['formatted_phone_number'] = formattedPhoneNumber;
if (openingHours != null) {
data['opening_hours'] = openingHours!.toJson();
}
return data;
}
}
class Geometry {
Location? location;
Geometry({
this.location,
});
Geometry.fromJson(Map<String, dynamic> json) {
location =
json['location'] != null ? Location.fromJson(json['location']) : null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (location != null) {
data['location'] = location!.toJson();
}
return data;
}
}
class Location {
double? lat;
double? lng;
Location({
this.lat,
this.lng,
});
Location.fromJson(Map<String, dynamic> json) {
lat = json['lat'];
lng = json['lng'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['lat'] = lat;
data['lng'] = lng;
return data;
}
}
class OpeningHours {
bool? openNow;
OpeningHours({
this.openNow,
});
OpeningHours.fromJson(Map<String, dynamic> json) {
openNow = json['open_now'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['open_now'] = openNow;
return data;
}
}

View File

@@ -0,0 +1,42 @@
class Province {
String? code;
String? name;
String? nameEn;
String? fullName;
String? fullNameEn;
String? codeName;
String? parent;
String? type;
Province(
{this.code,
this.name,
this.nameEn,
this.fullName,
this.fullNameEn,
this.codeName,
this.parent,
this.type});
Province.fromJson(Map<String, dynamic> json) {
code = json['code'];
name = json['name'];
nameEn = json['name_en'];
fullName = json['full_name'];
fullNameEn = json['full_name_en'];
codeName = json['code_name'];
parent = json['parent'];
type = json['type'];
}
static List<Province> fromJsonList(List list) {
return list.map((e) => Province.fromJson(e)).toList();
}
static List<Province> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => Province.fromJson(e)).toList();
}
@override
String toString() => fullName!;
}

View File

@@ -0,0 +1,43 @@
class Ward {
String? code;
String? name;
String? nameEn;
String? fullName;
String? fullNameEn;
String? codeName;
String? districtCode;
String? type;
Ward({
this.code,
this.name,
this.nameEn,
this.fullName,
this.fullNameEn,
this.codeName,
this.districtCode,
this.type,
});
Ward.fromJson(Map<String, dynamic> json) {
code = json['code'];
name = json['name'];
nameEn = json['name_en'];
fullName = json['full_name'];
fullNameEn = json['full_name_en'];
codeName = json['code_name'];
districtCode = json['parent'];
type = json['type'];
}
static List<Ward> fromJsonList(List list) {
return list.map((e) => Ward.fromJson(e)).toList();
}
static List<Ward> fromJsonDynamicList(List<dynamic> list) {
return list.map((e) => Ward.fromJson(e)).toList();
}
@override
String toString() => fullName!;
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:sfm_app/product/constant/image/image_constants.dart';
class SharedBackground extends StatelessWidget {
final Widget child;
const SharedBackground({super.key, required this.child});
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Directionality(
textDirection: TextDirection.ltr,
child: Scaffold(
resizeToAvoidBottomInset: false,
body: SizedBox(
height: size.height,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(
top: 0,
left: 0,
child: Image.asset(
ImageConstants.instance.getImage("background_top"),
width: size.width * 0.3,
),
),
Positioned(
bottom: 0,
right: 0,
child: Image.asset(
ImageConstants.instance.getImage("background_bottom"),
width: size.width * 0.4,
),
),
SafeArea(child: child)
],
),
),
),
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import '../extention/context_extention.dart';
InputDecoration borderRadiusTopLeftAndBottomRight(
BuildContext context, String hintText) =>
InputDecoration(
hintText: hintText,
border: OutlineInputBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12), bottomRight: Radius.circular(12)),
));
InputDecoration borderRadiusAll(BuildContext context, String hintText) =>
InputDecoration(
hintText: hintText,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(context.normalRadius),
));

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:sfm_app/product/extention/context_extention.dart';
import 'package:top_snackbar_flutter/custom_snack_bar.dart';
import 'package:top_snackbar_flutter/top_snack_bar.dart';
void showNoIconTopSnackBar(BuildContext context, String message,
Color backgroundColor, Color textColor) {
if (!context.mounted) return;
showTopSnackBar(
Overlay.of(context),
Card(
color: backgroundColor,
child: Container(
margin: const EdgeInsets.only(left: 20),
height: 50,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
message,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: textColor,
),
),
),
),
),
displayDuration: context.lowDuration);
}
void showSuccessTopSnackBarCustom(BuildContext context, String message) {
if (!context.mounted) return;
showTopSnackBar(
Overlay.of(context),
SizedBox(
height: 60,
child: CustomSnackBar.success(
message: message,
icon: const Icon(Icons.sentiment_very_satisfied_outlined,
color: Color(0x15000000), size: 80))),
displayDuration: context.lowDuration);
}
void showErrorTopSnackBarCustom(BuildContext context, String message) {
if (!context.mounted) return;
showTopSnackBar(
Overlay.of(context),
SizedBox(
height: 60,
child: CustomSnackBar.error(
message: message,
icon: const Icon(Icons.sentiment_dissatisfied_outlined,
color: Color(0x15000000), size: 80))),
displayDuration: context.lowDuration);
}

View File

@@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
/// Hiệu ứng di chuyển từ phải qua trái
Widget transitionsRightToLeft(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
}
/// Hiệu ứng di chuyển từ trái sang phải
Widget transitionsLeftToRight(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
const begin = Offset(-1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
}
/// Hiệu ứng di chuyển từ dưới lên trên
Widget transitionsBottomToTop(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
}
/// Hiệu ứng di chuyển từ trên xuống dưới
Widget transitionsTopToBottom(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
const begin = Offset(0.0, -1.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
}
/// Hiệu ứng mở dần vào và ra
Widget transitionsFaded(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
const begin = 0.0;
const end = 1.0;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var fadeAnimation = animation.drive(tween);
return FadeTransition(
opacity: fadeAnimation,
child: child,
);
}
/// Hiệu ứng di chuyển từ kích thước nhỏ đến đầy đủ
Widget transitionsScale(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
const begin = 0.0; // Bắt đầu từ kích thước 0
const end = 1.0; // Kết thúc ở kích thước 1
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var scaleAnimation = animation.drive(tween);
return ScaleTransition(
scale: scaleAnimation,
child: child,
);
}
Widget transitionsTotation(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
const begin = 0.0; // Bắt đầu từ góc 0 độ
const end = 1.0; // Kết thúc ở góc 1 vòng (360 độ)
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var rotationAnimation = animation.drive(tween);
return RotationTransition(
turns: rotationAnimation,
child: child,
);
}
/// Hiệu ứng kết hợp (Di chuyển và mờ dần)
Widget transitionsCustom1(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
const beginOffset = Offset(1.0, 0.0); // Di chuyển từ phải vào
const endOffset = Offset.zero;
const beginOpacity = 0.0; // Bắt đầu từ độ mờ 0
const endOpacity = 1.0; // Kết thúc ở độ mờ 1
var offsetTween = Tween(begin: beginOffset, end: endOffset);
var opacityTween = Tween(begin: beginOpacity, end: endOpacity);
var offsetAnimation =
animation.drive(offsetTween.chain(CurveTween(curve: Curves.easeInOut)));
var opacityAnimation =
animation.drive(opacityTween.chain(CurveTween(curve: Curves.easeInOut)));
return FadeTransition(
opacity: opacityAnimation,
child: SlideTransition(
position: offsetAnimation,
child: child,
),
);
}

View File

@@ -0,0 +1,5 @@
import 'package:flutter/material.dart';
abstract class AppTheme {
ThemeData? theme;
}

View File

@@ -0,0 +1,53 @@
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:sfm_app/product/theme/app_theme.dart';
class AppThemeDark extends AppTheme {
static AppThemeDark? _instance;
static AppThemeDark get instance {
_instance ??= AppThemeDark._init();
return _instance!;
}
AppThemeDark._init();
@override
ThemeData get theme => FlexThemeData.dark(
useMaterial3: true,
scheme: FlexScheme.flutterDash,
subThemesData: const FlexSubThemesData(
inputDecoratorRadius: 30,
interactionEffects: true,
tintedDisabledControls: true,
blendOnColors: true,
useM2StyleDividerInM3: true,
inputDecoratorBorderType: FlexInputBorderType.outline,
tooltipRadius: 20,
tooltipWaitDuration: Duration(milliseconds: 500),
tooltipShowDuration: Duration(milliseconds: 500),
navigationRailUseIndicator: true,
navigationRailLabelType: NavigationRailLabelType.all,
),
visualDensity: FlexColorScheme.comfortablePlatformDensity,
);
// ThemeData.dark().copyWith(
// useMaterial3: true,
// colorScheme: _buildColorScheme,
// );
// ColorScheme get _buildColorScheme => FlexColorScheme.dark().toScheme;
// ColorScheme(
// brightness: Brightness.dark,
// primary: Colors.blue.shade900,
// onPrimary: Colors.blue,
// secondary: Colors.white,
// onSecondary: Colors.white70,
// error: Colors.red,
// onError: Colors.orange,
// background: Colors.black,
// onBackground: Colors.white70,
// surface: Colors.grey,
// onSurface: Colors.white,
// );
}

View File

@@ -0,0 +1,54 @@
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:sfm_app/product/theme/app_theme.dart';
class AppThemeLight extends AppTheme {
static AppThemeLight? _instance;
static AppThemeLight get instance {
_instance ??= AppThemeLight._init();
return _instance!;
}
AppThemeLight._init();
@override
ThemeData get theme => FlexThemeData.light(
useMaterial3: true,
scheme: FlexScheme.flutterDash,
bottomAppBarElevation: 20.0,
subThemesData: const FlexSubThemesData(
inputDecoratorRadius: 30,
interactionEffects: true,
tintedDisabledControls: true,
useM2StyleDividerInM3: true,
inputDecoratorBorderType: FlexInputBorderType.outline,
tooltipRadius: 20,
tooltipWaitDuration: Duration(milliseconds: 1600),
tooltipShowDuration: Duration(milliseconds: 1500),
navigationRailUseIndicator: true,
navigationRailLabelType: NavigationRailLabelType.all,
),
visualDensity: FlexColorScheme.comfortablePlatformDensity,
);
// ThemeData.light().copyWith(
// useMaterial3: true,
// colorScheme: _buildColorScheme,
// );
// ColorScheme get _buildColorScheme => FlexColorScheme.light(
// ).toScheme;
// const ColorScheme(
// brightness: Brightness.light,
// primary: Colors.black,
// onPrimary: Colors.white,
// secondary: Colors.black,
// onSecondary: Colors.black45,
// error: Colors.red,
// onError: Colors.orange,
// background: Colors.white,
// onBackground: Colors.red,
// surface: Colors.white,
// onSurface: Colors.black,
// );
}

View File

@@ -0,0 +1,23 @@
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';
import '../theme_notifier.dart';
class ApplicationProvider {
static ApplicationProvider? _instance;
static ApplicationProvider get instance {
_instance ??= ApplicationProvider._init();
return _instance!;
}
ApplicationProvider._init();
List<SingleChildWidget> singleItems = [];
List<SingleChildWidget> dependItems = [
ChangeNotifierProvider(
create: (context) => ThemeNotifier(),
),
// Provider.value(value: NavigationService.instance)
];
List<SingleChildWidget> uiChangesItems = [];
}

View File

@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'app_theme_dark.dart';
import 'app_theme_light.dart';
import '../constant/enums/app_theme_enums.dart';
class ThemeNotifier extends ChangeNotifier {
// ThemeData _currentTheme = AppThemeLight.instance.theme;
// // ThemeData get currentTheme => LocaleManager.instance.getStringValue(PreferencesKeys.THEME) ==
// // AppThemes.LIGHT.name
// // ? AppThemeLight.instance.theme
// // : AppThemeDark.instance.theme;
// ThemeData get currentTheme {
// log("ThemeKey: ${LocaleManager.instance.getStringValue(PreferencesKeys.THEME)}");
// if (LocaleManager.instance.getStringValue(PreferencesKeys.THEME) ==
// AppThemes.LIGHT.name) {
// log("light");
// } else {
// log("dark");
// }
// return LocaleManager.instance.getStringValue(PreferencesKeys.THEME) ==
// AppThemes.LIGHT.name
// ? AppThemeLight.instance.theme
// : AppThemeDark.instance.theme;
// }
// ThemeData _currentTheme = AppThemeLight.instance.theme; // Mặc định là light
// ThemeNotifier() {
// loadThemeFromPreferences();
// }
// Future<void> loadThemeFromPreferences() async {
// String themeKey =
// LocaleManager.instance.getStringValue(PreferencesKeys.THEME);
// log("ThemeNotifierKey:$themeKey ");
// if (themeKey == AppThemes.LIGHT.name) {
// _currentTheme = AppThemeLight.instance.theme;
// } else {
// _currentTheme = AppThemeDark.instance.theme;
// }
// notifyListeners(); // Thông báo cho các widget lắng nghe
// }
// ThemeData get currentTheme => _currentTheme;
// AppThemes _currenThemeEnum = AppThemes.LIGHT;
// AppThemes get currenThemeEnum =>
// LocaleManager.instance.getStringValue(PreferencesKeys.THEME) ==
// AppThemes.LIGHT.name
// ? AppThemes.LIGHT
// : AppThemes.DARK;
// // AppThemes get currenThemeEnum => _currenThemeEnum;
// void changeValue(AppThemes theme) {
// if (theme == AppThemes.LIGHT) {
// _currentTheme = ThemeData.dark();
// } else {
// _currentTheme = ThemeData.light();
// }
// notifyListeners();
// }
// void changeTheme() {
// if (_currenThemeEnum == AppThemes.LIGHT) {
// _currentTheme = AppThemeDark.instance.theme;
// _currenThemeEnum = AppThemes.DARK;
// LocaleManager.instance
// .setString(PreferencesKeys.THEME, AppThemes.DARK.name);
// } else {
// _currentTheme = AppThemeLight.instance.theme;
// _currenThemeEnum = AppThemes.LIGHT;
// LocaleManager.instance
// .setString(PreferencesKeys.THEME, AppThemes.LIGHT.name);
// }
// notifyListeners();
// }
ThemeData _currentTheme = AppThemeLight.instance.theme;
ThemeData get currentTheme => _currentTheme;
AppThemes _currenThemeEnum = AppThemes.LIGHT;
AppThemes get currenThemeEnum => _currenThemeEnum;
void changeValue(AppThemes theme) {
if (theme == AppThemes.LIGHT) {
_currentTheme = AppThemeLight.instance.theme;
} else {
_currentTheme = AppThemeDark.instance.theme;
}
notifyListeners();
}
void changeTheme() {
if (_currenThemeEnum == AppThemes.LIGHT) {
_currentTheme = AppThemeDark.instance.theme;
_currenThemeEnum = AppThemes.DARK;
} else {
_currentTheme = AppThemeLight.instance.theme;
_currenThemeEnum = AppThemes.LIGHT;
}
notifyListeners();
}
}

View File

@@ -0,0 +1,16 @@
import 'package:intl/intl.dart';
class DateTimeUtils {
DateTimeUtils._init();
static DateTimeUtils? _instance;
static DateTimeUtils get instance => _instance ??= DateTimeUtils._init();
String formatDateTimeToString(DateTime dateTime) {
return DateFormat('yyyy-MM-dd\'T\'00:00:00\'Z\'').format(dateTime);
}
String convertCurrentMillisToDateTimeString(int time) {
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch((time) * 1000);
return DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime);
}
}

View File

@@ -0,0 +1,269 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:sfm_app/feature/log/device_logs_model.dart';
import 'package:sfm_app/product/services/api_services.dart';
import 'package:sfm_app/product/services/language_services.dart';
import 'package:sfm_app/product/shared/model/district_model.dart';
import 'package:sfm_app/product/shared/model/province_model.dart';
import '../../feature/devices/device_model.dart';
import '../shared/model/ward_model.dart';
class DeviceUtils {
DeviceUtils._init();
static DeviceUtils? _instance;
static DeviceUtils get instance => _instance ??= DeviceUtils._init();
APIServices apiServices = APIServices();
Map<String, dynamic> getDeviceSensors(
BuildContext context, List<Sensor> sensors) {
Map<String, dynamic> map = {};
if (sensors.isEmpty) {
map['sensorState'] = appLocalization(context).no_data_message;
map['sensorBattery'] = appLocalization(context).no_data_message;
map['sensorCsq'] = appLocalization(context).no_data_message;
map['sensorTemp'] = appLocalization(context).no_data_message;
map['sensorHum'] = appLocalization(context).no_data_message;
map['sensorVolt'] = appLocalization(context).no_data_message;
} else {
for (var sensor in sensors) {
if (sensor.name == "1") {
if (sensor.value == 0) {
map['sensorIn'] = "sensor 0";
} else {
map['sensorIn'] = "sensor 1";
}
}
if (sensor.name == "2") {
if (sensor.value == 0) {
map['sensorMove'] = appLocalization(context).gf_not_move_message;
} else {
map['sensorMove'] = appLocalization(context).gf_moving_message;
}
}
if (sensor.name == "3") {
if (sensor.value == 0) {
map['sensorAlarm'] = appLocalization(context).normal_message;
} else {
map['sensorAlarm'] =
appLocalization(context).warning_status_message;
}
}
if (sensor.name == "6") {
if (sensor.value! > 0 && sensor.value! < 12) {
map['sensorCsq'] = appLocalization(context).low_message_uppercase;
} else if (sensor.value! >= 12 && sensor.value! < 20) {
map['sensorCsq'] =
appLocalization(context).moderate_message_uppercase;
} else if (sensor.value! >= 20) {
map['sensorCsq'] = appLocalization(context).good_message_uppercase;
} else {
map['sensorCsq'] = appLocalization(context).gf_no_signal_message;
}
}
if (sensor.name == "7") {
map['sensorVolt'] = "${(sensor.value!) / 1000} V";
}
if (sensor.name == "8") {
map['sensorTemp'] = "${sensor.value}°C";
}
if (sensor.name == "9") {
map['sensorHum'] = "${sensor.value} %";
}
if (sensor.name == "10") {
map['sensorBattery'] = "${sensor.value}";
}
if (sensor.name == "11") {
if (sensor.value == 0) {
map['sensorState'] = appLocalization(context).normal_message;
} else if (sensor.value == 1) {
map['sensorState'] =
appLocalization(context).smoke_detecting_message;
} else if (sensor.value == 3) {
map['sensorState'] =
appLocalization(context).gf_remove_from_base_message;
} else {
map['sensorState'] = appLocalization(context).undefine_message;
}
}
}
}
return map;
}
Future<String> getFullDeviceLocation(
BuildContext context, String areaPath) async {
if (areaPath != "") {
List<String> parts = areaPath.split('_');
String provinceID = parts[0];
String districtID = parts[1];
String wardID = parts[2];
String provinceBody = await apiServices.getProvinceByID(provinceID);
final provinceItem = jsonDecode(provinceBody);
Province province = Province.fromJson(provinceItem['data']);
String districtBody = await apiServices.getDistrictByID(districtID);
final districtItem = jsonDecode(districtBody);
District district = District.fromJson(districtItem['data']);
String wardBody = await apiServices.getWardByID(wardID);
final wardItem = jsonDecode(wardBody);
Ward ward = Ward.fromJson(wardItem['data']);
return "${ward.fullName}, ${district.fullName}, ${province.fullName}";
}
return appLocalization(context).no_data_message;
}
String checkStateDevice(BuildContext context, int state) {
String message = appLocalization(context).no_data_message;
if (state == 1) {
message = appLocalization(context).smoke_detecting_message;
} else if (state == 0) {
message = appLocalization(context).normal_message;
} else if (state == -1) {
message = appLocalization(context).disconnect_message_uppercase;
} else if (state == 2) {
message = appLocalization(context).in_progress_message;
} else if (state == 3) {
message = appLocalization(context).in_progress_message;
}
return message;
}
List<Device> sortDeviceByState(List<Device> devices) {
List<Device> 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;
}
Color getTableRowColor(int state) {
if (state == 1) {
return Colors.red;
} else if (state == 0) {
return Colors.green;
} else {
return Colors.grey;
}
}
String getDeviceSensorsLog(BuildContext context, SensorLogs sensor) {
String message = "";
if (sensor.detail!.username != null || sensor.detail!.note != null) {
message = "${appLocalization(context).bell_user_uppercase} ";
if (sensor.name! == "0") {
if (sensor.value! == 3) {
String state = "đã thông báo rằng đây là cháy giả";
message = message + state;
}
}
} else {
message = "${appLocalization(context).device_title} ";
if (sensor.name == "11") {
String state = checkStateDevice(context, sensor.value!);
message = message + state;
} else if (sensor.name == "2") {
String state = getDeviceMove(context, sensor.value!);
message = message + state;
} else if (sensor.name == "6") {
String state = getDeviceCSQ(context, sensor.value!);
message = message + state;
} else if (sensor.name == "7") {
String state = getDeviceVolt(context, sensor.value!);
message = message + state;
} else if (sensor.name == "8") {
String state = getDeviceTemp(context, sensor.value!);
message = message + state;
} else if (sensor.name == "9") {
String state = getDeviceHum(context, sensor.value!);
message = message + state;
} else if (sensor.name == "10") {
String state = getDeviceBattery(context, sensor.value!);
message = message + state;
} else {
String state = appLocalization(context).undefine_message;
message = message + state;
}
}
return message;
}
String getDeviceCSQ(BuildContext context, int number) {
if (number >= 0 && number < 12) {
return appLocalization(context).gf_weak_signal_message;
} else if (number >= 12 && number < 20) {
return appLocalization(context).gf_moderate_signal_message;
} else if (number >= 20) {
return appLocalization(context).gf_good_signal_message;
}
return appLocalization(context).gf_no_signal_message;
}
String getDeviceVolt(BuildContext context, int number) {
return "${appLocalization(context).gf_volt_detect_message} ${number / 1000} V";
}
String getDeviceTemp(BuildContext context, int number) {
return "${appLocalization(context).gf_temp_detect_message} $number °C";
}
String getDeviceHum(BuildContext context, int number) {
return "${appLocalization(context).gf_hum_detect_message} $number%";
}
String getDeviceBattery(BuildContext context, int number) {
return "${appLocalization(context).gf_battery_detect_message} $number%";
}
String getDeviceMove(BuildContext context, int number) {
String message = "";
if (number == 0) {
message = appLocalization(context).gf_not_move_message;
} else {
message = appLocalization(context).gf_moving_message;
}
return message;
}
IconData getBatteryIcon(int battery) {
if (battery <= 10) {
return Icons.battery_alert;
} else if (battery <= 40) {
return Icons.battery_2_bar;
} else if (battery <= 70) {
return Icons.battery_4_bar;
} else if (battery <= 90) {
return Icons.battery_6_bar;
} else {
return Icons.battery_full_rounded;
}
}
IconData getSignalIcon(BuildContext context, String signal) {
if (signal == appLocalization(context).gf_weak_signal_message) {
return Icons.signal_cellular_alt_1_bar;
} else if (signal == appLocalization(context).gf_moderate_signal_message) {
return Icons.signal_cellular_alt_2_bar;
} else {
return Icons.signal_cellular_alt;
}
}
Color getColorRiple(int state) {
if (state == 1) {
return Colors.red;
} else if (state == 3) {
return Colors.orange;
} else if (state == -1) {
return Colors.grey;
} else {
return Colors.green;
}
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
import '../services/language_services.dart';
class QRScanUtils {
QRScanUtils._init();
static QRScanUtils? _instance;
static QRScanUtils get instance => _instance ??= QRScanUtils._init();
Future<String> scanQR(BuildContext context) async {
String barcodeScanRes;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
barcodeScanRes = await FlutterBarcodeScanner.scanBarcode(
'#ffffff', appLocalization(context).cancel_button_content, true, ScanMode.QR);
} on PlatformException {
barcodeScanRes = 'Failed to get platform version.';
}
return barcodeScanRes;
}
Map<String, dynamic> getQRData(String data) {
if (data.isEmpty) {
return {};
} else {
final parts = data.split('.');
return {
'group_id': parts.length == 2 ? parts[0] : '',
'group_name': parts.length == 2 ? parts[1] : '',
};
}
}
}

Some files were not shown because too many files have changed in this diff Show More