Complete refactoring SFM App Source Code
This commit is contained in:
18
lib/feature/auth/login/bloc/login_bloc.dart
Normal file
18
lib/feature/auth/login/bloc/login_bloc.dart
Normal 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() {
|
||||
}
|
||||
|
||||
}
|
||||
17
lib/feature/auth/login/model/login_model.dart
Normal file
17
lib/feature/auth/login/model/login_model.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
234
lib/feature/auth/login/screen/login_screen.dart
Normal file
234
lib/feature/auth/login/screen/login_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
22
lib/feature/bell/bell_bloc.dart
Normal file
22
lib/feature/bell/bell_bloc.dart
Normal 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() {}
|
||||
}
|
||||
75
lib/feature/bell/bell_model.dart
Normal file
75
lib/feature/bell/bell_model.dart
Normal 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"];
|
||||
}
|
||||
}
|
||||
253
lib/feature/bell/bell_screen.dart
Normal file
253
lib/feature/bell/bell_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
97
lib/feature/devices/add_new_device_widget.dart
Normal file
97
lib/feature/devices/add_new_device_widget.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
61
lib/feature/devices/delete_device_widget.dart
Normal file
61
lib/feature/devices/delete_device_widget.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
79
lib/feature/devices/device_detail/device_detail_bloc.dart
Normal file
79
lib/feature/devices/device_detail/device_detail_bloc.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
361
lib/feature/devices/device_detail/device_detail_screen.dart
Normal file
361
lib/feature/devices/device_detail/device_detail_screen.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
171
lib/feature/devices/device_model.dart
Normal file
171
lib/feature/devices/device_model.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
255
lib/feature/devices/device_update/device_update_bloc.dart
Normal file
255
lib/feature/devices/device_update/device_update_bloc.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
515
lib/feature/devices/device_update/device_update_screen.dart
Normal file
515
lib/feature/devices/device_update/device_update_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
47
lib/feature/devices/device_update/geocode_model.dart
Normal file
47
lib/feature/devices/device_update/geocode_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
298
lib/feature/devices/device_update/map_dialog.dart
Normal file
298
lib/feature/devices/device_update/map_dialog.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
35
lib/feature/devices/devices_manager_bloc.dart
Normal file
35
lib/feature/devices/devices_manager_bloc.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
274
lib/feature/devices/devices_manager_screen.dart
Normal file
274
lib/feature/devices/devices_manager_screen.dart
Normal 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;
|
||||
}
|
||||
36
lib/feature/error/not_found_screen.dart
Normal file
36
lib/feature/error/not_found_screen.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
79
lib/feature/home/device_alias_model.dart
Normal file
79
lib/feature/home/device_alias_model.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
92
lib/feature/home/home_bloc.dart
Normal file
92
lib/feature/home/home_bloc.dart
Normal 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() {}
|
||||
}
|
||||
411
lib/feature/home/home_screen.dart
Normal file
411
lib/feature/home/home_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
130
lib/feature/home/shared/alert_card.dart
Normal file
130
lib/feature/home/shared/alert_card.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
77
lib/feature/home/shared/overview_card.dart
Normal file
77
lib/feature/home/shared/overview_card.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
lib/feature/home/shared/status_card.dart
Normal file
42
lib/feature/home/shared/status_card.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
248
lib/feature/home/shared/warning_card.dart
Normal file
248
lib/feature/home/shared/warning_card.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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))
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
118
lib/feature/inter_family/group_detail/group_detail_bloc.dart
Normal file
118
lib/feature/inter_family/group_detail/group_detail_bloc.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
133
lib/feature/inter_family/group_detail/group_detail_model.dart
Normal file
133
lib/feature/inter_family/group_detail/group_detail_model.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
552
lib/feature/inter_family/group_detail/group_detail_screen.dart
Normal file
552
lib/feature/inter_family/group_detail/group_detail_screen.dart
Normal 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;
|
||||
}
|
||||
46
lib/feature/inter_family/groups/groups_model.dart
Normal file
46
lib/feature/inter_family/groups/groups_model.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
161
lib/feature/inter_family/groups/groups_screen.dart
Normal file
161
lib/feature/inter_family/groups/groups_screen.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
lib/feature/inter_family/groups/groups_widget.dart
Normal file
95
lib/feature/inter_family/groups/groups_widget.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
119
lib/feature/inter_family/inter_family_bloc.dart
Normal file
119
lib/feature/inter_family/inter_family_bloc.dart
Normal 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 ?? ''));
|
||||
}
|
||||
}
|
||||
163
lib/feature/inter_family/inter_family_screen.dart
Normal file
163
lib/feature/inter_family/inter_family_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
152
lib/feature/inter_family/inter_family_widget.dart
Normal file
152
lib/feature/inter_family/inter_family_widget.dart
Normal 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))
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
82
lib/feature/log/device_logs_bloc.dart
Normal file
82
lib/feature/log/device_logs_bloc.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
lib/feature/log/device_logs_model.dart
Normal file
66
lib/feature/log/device_logs_model.dart
Normal 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"];
|
||||
}
|
||||
}
|
||||
295
lib/feature/log/device_logs_screen.dart
Normal file
295
lib/feature/log/device_logs_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
39
lib/feature/main/main_bloc.dart
Normal file
39
lib/feature/main/main_bloc.dart
Normal 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() {}
|
||||
}
|
||||
393
lib/feature/main/main_screen.dart
Normal file
393
lib/feature/main/main_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
lib/feature/map/map_bloc.dart
Normal file
74
lib/feature/map/map_bloc.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
237
lib/feature/map/map_screen.dart
Normal file
237
lib/feature/map/map_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
344
lib/feature/map/widget/on_tap_marker_widget.dart
Normal file
344
lib/feature/map/widget/on_tap_marker_widget.dart
Normal 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;
|
||||
}
|
||||
124
lib/feature/map/widget/show_direction_widget.dart
Normal file
124
lib/feature/map/widget/show_direction_widget.dart
Normal 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]!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
226
lib/feature/map/widget/show_nearest_place.dart
Normal file
226
lib/feature/map/widget/show_nearest_place.dart
Normal 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 : [];
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
37
lib/feature/settings/profile/profile_model.dart
Normal file
37
lib/feature/settings/profile/profile_model.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
440
lib/feature/settings/profile/profile_screen.dart
Normal file
440
lib/feature/settings/profile/profile_screen.dart
Normal 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()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
25
lib/feature/settings/settings_bloc.dart
Normal file
25
lib/feature/settings/settings_bloc.dart
Normal 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() {
|
||||
}
|
||||
}
|
||||
153
lib/feature/settings/settings_screen.dart
Normal file
153
lib/feature/settings/settings_screen.dart
Normal 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
86
lib/firebase_options.dart
Normal 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
80
lib/main.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
72
lib/product/base/bloc/base_bloc.dart
Normal file
72
lib/product/base/bloc/base_bloc.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
142
lib/product/base/screen/base_screen.dart
Normal file
142
lib/product/base/screen/base_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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
42
lib/product/cache/local_manager.dart
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
24
lib/product/constant/app/api_path_constant.dart
Normal file
24
lib/product/constant/app/api_path_constant.dart
Normal 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";
|
||||
}
|
||||
21
lib/product/constant/app/app_constants.dart
Normal file
21
lib/product/constant/app/app_constants.dart
Normal 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";
|
||||
}
|
||||
16
lib/product/constant/enums/app_route_enums.dart
Normal file
16
lib/product/constant/enums/app_route_enums.dart
Normal 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,
|
||||
}
|
||||
3
lib/product/constant/enums/app_theme_enums.dart
Normal file
3
lib/product/constant/enums/app_theme_enums.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
enum AppThemes { LIGHT, DARK, SYSTEM }
|
||||
10
lib/product/constant/enums/local_keys_enums.dart
Normal file
10
lib/product/constant/enums/local_keys_enums.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
enum PreferencesKeys{
|
||||
TOKEN,
|
||||
UID,
|
||||
EXP,
|
||||
ROLE,
|
||||
LANGUAGE_CODE,
|
||||
THEME
|
||||
}
|
||||
7
lib/product/constant/enums/role_enums.dart
Normal file
7
lib/product/constant/enums/role_enums.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
enum RoleEnums{
|
||||
USER,
|
||||
ADMIN,
|
||||
MOD
|
||||
}
|
||||
13
lib/product/constant/icon/icon_constants.dart
Normal file
13
lib/product/constant/icon/icon_constants.dart
Normal 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);
|
||||
}
|
||||
9
lib/product/constant/image/image_constants.dart
Normal file
9
lib/product/constant/image/image_constants.dart
Normal 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";
|
||||
}
|
||||
6
lib/product/constant/lang/language_constants.dart
Normal file
6
lib/product/constant/lang/language_constants.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
class LanguageConstants {
|
||||
static const ENGLISH = "en";
|
||||
static const VIETNAM = "vi";
|
||||
}
|
||||
156
lib/product/constant/navigation/navigation_router.dart
Normal file
156
lib/product/constant/navigation/navigation_router.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
103
lib/product/extention/context_extention.dart
Normal file
103
lib/product/extention/context_extention.dart
Normal 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!;
|
||||
}
|
||||
|
||||
242
lib/product/lang/l10n/app_en.arb
Normal file
242
lib/product/lang/l10n/app_en.arb
Normal 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"
|
||||
}
|
||||
242
lib/product/lang/l10n/app_vi.arb
Normal file
242
lib/product/lang/l10n/app_vi.arb
Normal 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"
|
||||
}
|
||||
18
lib/product/lang/language_model.dart
Normal file
18
lib/product/lang/language_model.dart
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
114
lib/product/network/network_manager.dart
Normal file
114
lib/product/network/network_manager.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
31
lib/product/permission/notification_permission.dart
Normal file
31
lib/product/permission/notification_permission.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
386
lib/product/services/api_services.dart
Normal file
386
lib/product/services/api_services.dart
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
36
lib/product/services/language_services.dart
Normal file
36
lib/product/services/language_services.dart
Normal 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)!;
|
||||
}
|
||||
63
lib/product/services/map_services.dart
Normal file
63
lib/product/services/map_services.dart
Normal 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
103
lib/product/services/notification_services.dart
Normal file
103
lib/product/services/notification_services.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
43
lib/product/shared/model/district_model.dart
Normal file
43
lib/product/shared/model/district_model.dart
Normal 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!;
|
||||
}
|
||||
172
lib/product/shared/model/near_by_search_model.dart
Normal file
172
lib/product/shared/model/near_by_search_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
42
lib/product/shared/model/province_model.dart
Normal file
42
lib/product/shared/model/province_model.dart
Normal 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!;
|
||||
}
|
||||
43
lib/product/shared/model/ward_model.dart
Normal file
43
lib/product/shared/model/ward_model.dart
Normal 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!;
|
||||
}
|
||||
44
lib/product/shared/shared_background.dart
Normal file
44
lib/product/shared/shared_background.dart
Normal 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)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/product/shared/shared_input_decoration.dart
Normal file
18
lib/product/shared/shared_input_decoration.dart
Normal 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),
|
||||
));
|
||||
56
lib/product/shared/shared_snack_bar.dart
Normal file
56
lib/product/shared/shared_snack_bar.dart
Normal 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);
|
||||
}
|
||||
137
lib/product/shared/shared_transition.dart
Normal file
137
lib/product/shared/shared_transition.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
5
lib/product/theme/app_theme.dart
Normal file
5
lib/product/theme/app_theme.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class AppTheme {
|
||||
ThemeData? theme;
|
||||
}
|
||||
53
lib/product/theme/app_theme_dark.dart
Normal file
53
lib/product/theme/app_theme_dark.dart
Normal 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,
|
||||
// );
|
||||
}
|
||||
54
lib/product/theme/app_theme_light.dart
Normal file
54
lib/product/theme/app_theme_light.dart
Normal 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,
|
||||
// );
|
||||
}
|
||||
23
lib/product/theme/provider/app_provider.dart
Normal file
23
lib/product/theme/provider/app_provider.dart
Normal 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 = [];
|
||||
}
|
||||
104
lib/product/theme/theme_notifier.dart
Normal file
104
lib/product/theme/theme_notifier.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
16
lib/product/utils/date_time_utils.dart
Normal file
16
lib/product/utils/date_time_utils.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
269
lib/product/utils/device_utils.dart
Normal file
269
lib/product/utils/device_utils.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
lib/product/utils/qr_utils.dart
Normal file
34
lib/product/utils/qr_utils.dart
Normal 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
Reference in New Issue
Block a user