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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user