fix(mobile-next): 清理悬浮窗、语音文案与位置发送

按 B-273 / B-269 收口 Android 客户端:去掉悬浮窗申请与旧浮窗服务,通话入口统一为语音通话,删除位置发送与定位权限,保留历史位置只读展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
编码工程师
2026-08-21 10:04:39 +08:00
co-authored by Cursor multica-agent
parent 092039bc1a
commit 694891c393
20 changed files with 662 additions and 726 deletions
@@ -76,7 +76,6 @@ export 'src/widgets/chat/chat_voice_record_bar.dart';
export 'src/widgets/chat/chat_voice_record_layout.dart';
export 'src/widgets/chat/chat_voice_record_view.dart';
export 'src/widgets/chat/chat_voice_view.dart';
export 'src/widgets/chat/chat_webview_map.dart';
export 'src/widgets/chat/new_message_indicator.dart';
export 'src/widgets/chat/water_mark_view.dart';
export 'src/widgets/custom_pop_up_menu.dart';
@@ -71,6 +71,7 @@ const Map<String, String> en_US = {
"video": "Video",
"voice": "Voice",
"location": "Location",
"locationMessage": "Location",
"file": "File",
"carte": "Card",
"emoji": "Custom Emoji",
@@ -120,7 +121,7 @@ const Map<String, String> en_US = {
"cancel": "Cancel",
"determine": "OK",
"toolboxAlbum": "Album",
"toolboxCall": "Video Call",
"toolboxCall": "Voice Call",
"toolboxCamera": "Camera",
"toolboxCard": "Card",
"toolboxFile": "File",
@@ -224,7 +225,7 @@ const Map<String, String> en_US = {
'position': 'Position',
'personalInfo': 'Personal Info',
'viewDynamics': 'View Dynamics',
'audioAndVideoCall': 'Call',
'audioAndVideoCall': 'Voice Call',
'sendMessage': 'Message',
'avatar': 'Avatar',
'name': 'Name',
@@ -71,6 +71,7 @@ const Map<String, String> zh_CN = {
"video": "视频",
"voice": "语音",
"location": "位置",
"locationMessage": "位置消息",
"file": "文件",
"carte": "名片",
"emoji": "自定义表情",
@@ -120,7 +121,7 @@ const Map<String, String> zh_CN = {
"cancel": "取消",
"determine": "确定",
"toolboxAlbum": "相册",
"toolboxCall": "视频通话",
"toolboxCall": "语音通话",
"toolboxCamera": "拍摄",
"toolboxCard": "名片",
"toolboxFile": "文件",
@@ -224,7 +225,7 @@ const Map<String, String> zh_CN = {
'position': '职位',
'personalInfo': '个人资料',
'viewDynamics': '查看动态',
'audioAndVideoCall': '视频通话',
'audioAndVideoCall': '音通话',
'sendMessage': '发消息',
'avatar': '头像',
'name': '姓名',
@@ -58,7 +58,8 @@ class StrRes {
static String get resendVerificationCode => 'resendVerificationCode'.tr;
static String get verificationCodeTimingReminder => 'verificationCodeTimingReminder'.tr;
static String get verificationCodeTimingReminder =>
'verificationCodeTimingReminder'.tr;
static String get defaultVerificationCode => 'defaultVerificationCode'.tr;
@@ -160,6 +161,8 @@ class StrRes {
static String get location => 'location'.tr;
static String get locationMessage => 'locationMessage'.tr;
static String get file => 'file'.tr;
static String get carte => 'carte'.tr;
@@ -276,7 +279,8 @@ class StrRes {
static String get releaseToSend => 'releaseToSend'.tr;
static String get releaseToSendSwipeUpToCancel => 'releaseToSendSwipeUpToCancel'.tr;
static String get releaseToSendSwipeUpToCancel =>
'releaseToSendSwipeUpToCancel'.tr;
static String get liftFingerToCancelSend => 'liftFingerToCancelSend'.tr;
@@ -704,7 +708,8 @@ class StrRes {
static String get confirm => 'confirm'.tr;
static String get confirmTransferGroupToUser => 'confirmTransferGroupToUser'.tr;
static String get confirmTransferGroupToUser =>
'confirmTransferGroupToUser'.tr;
static String get removeGroupMember => 'removeGroupMember'.tr;
@@ -970,7 +975,8 @@ class StrRes {
static String get confirmTheChanges => 'confirmTheChanges'.tr;
static String get invitesYouToVideoConference => 'invitesYouToVideoConference'.tr;
static String get invitesYouToVideoConference =>
'invitesYouToVideoConference'.tr;
static String get over => 'over'.tr;
@@ -1074,7 +1080,8 @@ class StrRes {
static String get sendAnother => 'sendAnother'.tr;
static String get confirmDelTagNotificationHint => 'confirmDelTagNotificationHint'.tr;
static String get confirmDelTagNotificationHint =>
'confirmDelTagNotificationHint'.tr;
static String get contentNotBlank => 'contentNotBlank'.tr;
@@ -1086,11 +1093,13 @@ class StrRes {
static String get groupRequestHandled => 'groupRequestHandled'.tr;
static String get burnAfterReadingDescription => 'burnAfterReadingDescription'.tr;
static String get burnAfterReadingDescription =>
'burnAfterReadingDescription'.tr;
static String get periodicallyDeleteMessage => 'periodicallyDeleteMessage'.tr;
static String get periodicallyDeleteMessageDescription => 'periodicallyDeleteMessageDescription'.tr;
static String get periodicallyDeleteMessageDescription =>
'periodicallyDeleteMessageDescription'.tr;
static String get nDay => 'nDay'.tr;
@@ -10,10 +10,6 @@ import 'package:sprintf/sprintf.dart';
class Permissions {
Permissions._();
static Future<bool> checkSystemAlertWindow() async {
return Permission.systemAlertWindow.isGranted;
}
static Future<bool> checkStorage() async {
return await Permission.storage.isGranted;
}
@@ -22,7 +18,8 @@ class Permissions {
if (await Permission.camera.request().isGranted) {
onGranted?.call();
}
if (await Permission.camera.isPermanentlyDenied || await Permission.camera.isDenied) {
if (await Permission.camera.isPermanentlyDenied ||
await Permission.camera.isDenied) {
_showPermissionDeniedDialog(Permission.camera.title);
}
}
@@ -52,7 +49,8 @@ class Permissions {
if (await Permission.manageExternalStorage.request().isGranted) {
onGranted?.call();
}
if (await Permission.storage.isPermanentlyDenied || await Permission.storage.isDenied) {
if (await Permission.storage.isPermanentlyDenied ||
await Permission.storage.isDenied) {
_showPermissionDeniedDialog(Permission.storage.title);
}
}
@@ -61,25 +59,18 @@ class Permissions {
if (await Permission.microphone.request().isGranted) {
onGranted?.call();
}
if (await Permission.microphone.isPermanentlyDenied || await Permission.microphone.isDenied) {
if (await Permission.microphone.isPermanentlyDenied ||
await Permission.microphone.isDenied) {
_showPermissionDeniedDialog(Permission.microphone.title);
}
}
static void location(Function()? onGranted) async {
if (await Permission.location.request().isGranted) {
onGranted?.call();
}
if (await Permission.location.isPermanentlyDenied || await Permission.location.isDenied) {
_showPermissionDeniedDialog(Permission.location.title);
}
}
static void speech(Function()? onGranted) async {
if (await Permission.speech.request().isGranted) {
onGranted?.call();
}
if (await Permission.speech.isPermanentlyDenied || await Permission.speech.isDenied) {
if (await Permission.speech.isPermanentlyDenied ||
await Permission.speech.isDenied) {
_showPermissionDeniedDialog(Permission.speech.title);
}
}
@@ -93,7 +84,8 @@ class Permissions {
if (await Permission.photos.request().isGranted) {
onGranted?.call();
}
if (await Permission.photos.isPermanentlyDenied || await Permission.photos.isDenied) {
if (await Permission.photos.isPermanentlyDenied ||
await Permission.photos.isDenied) {
_showPermissionDeniedDialog(Permission.photos.title);
}
}
@@ -101,7 +93,8 @@ class Permissions {
if (await Permission.photos.request().isGranted) {
onGranted?.call();
}
if (await Permission.photos.isPermanentlyDenied || await Permission.photos.isDenied) {
if (await Permission.photos.isPermanentlyDenied ||
await Permission.photos.isDenied) {
_showPermissionDeniedDialog(Permission.photos.title);
}
}
@@ -111,20 +104,14 @@ class Permissions {
if (await Permission.notification.request().isGranted) {
return true;
}
if (await Permission.notification.isPermanentlyDenied || await Permission.notification.isDenied) {
if (await Permission.notification.isPermanentlyDenied ||
await Permission.notification.isDenied) {
_showPermissionDeniedDialog(Permission.notification.title);
}
return false;
}
static void ignoreBatteryOptimizations(Function()? onGranted) async {
if (await Permission.ignoreBatteryOptimizations.request().isGranted) {
onGranted?.call();
}
if (await Permission.ignoreBatteryOptimizations.isPermanentlyDenied) {}
}
static void cameraAndMicrophone(Function()? onGranted) async {
final permissions = [
Permission.camera,
@@ -214,7 +201,8 @@ class Permissions {
}
}
static Future<Map<Permission, PermissionStatus>> request(List<Permission> permissions) async {
static Future<Map<Permission, PermissionStatus>> request(
List<Permission> permissions) async {
Map<Permission, PermissionStatus> statuses = await permissions.request();
return statuses;
}
@@ -45,7 +45,8 @@ class IntervalDo {
void run({required Function() fuc, int milliseconds = 0}) {
DateTime now = DateTime.now();
if (null == last || now.difference(last ?? now).inMilliseconds > milliseconds) {
if (null == last ||
now.difference(last ?? now).inMilliseconds > milliseconds) {
last = now;
fuc();
}
@@ -153,12 +154,14 @@ class IMUtils {
}
}
static String? emptyStrToNull(String? str) => (null != str && str.trim().isEmpty) ? null : str;
static String? emptyStrToNull(String? str) =>
(null != str && str.trim().isEmpty) ? null : str;
static bool isNotNullEmptyStr(String? str) => null != str && "" != str.trim();
static bool isChinaMobile(String mobile) {
RegExp exp = RegExp(r'^((13[0-9])|(14[0-9])|(15[0-9])|(16[0-9])|(17[0-9])|(18[0-9])|(19[0-9]))\d{8}$');
RegExp exp = RegExp(
r'^((13[0-9])|(14[0-9])|(15[0-9])|(16[0-9])|(17[0-9])|(18[0-9])|(19[0-9]))\d{8}$');
return exp.hasMatch(mobile);
}
@@ -172,14 +175,17 @@ class IMUtils {
final directory = await createTempDir(dir: 'video');
final targetPath = '$directory/$name';
final String ffmpegCommand = '-i $path -ss 0 -vframes 1 -q:v 15 -y $targetPath';
final String ffmpegCommand =
'-i $path -ss 0 -vframes 1 -q:v 15 -y $targetPath';
final session = await FFmpegKit.execute(ffmpegCommand);
final state = FFmpegKitConfig.sessionStateToString(await session.getState());
final state =
FFmpegKitConfig.sessionStateToString(await session.getState());
final returnCode = await session.getReturnCode();
if (state == SessionState.failed || !ReturnCode.isSuccess(returnCode)) {
Logger().printError(info: "Command failed. Please check output for the details.");
Logger().printError(
info: "Command failed. Please check output for the details.");
}
session.cancel();
@@ -195,11 +201,14 @@ class IMUtils {
final output = await FFprobeKit.getMediaInformation(path);
final streams = output.getMediaInformation()?.getStreams();
final isH264 = streams?.any((element) => element.getCodec()?.contains('h264') == true) ?? false;
final isH264 = streams
?.any((element) => element.getCodec()?.contains('h264') == true) ??
false;
final size = output.getMediaInformation()?.getSize() ?? '0';
output.cancel();
final audioStream = streams?.firstWhereOrNull((e) => e.getType()?.contains('audio') == true);
final audioStream = streams
?.firstWhereOrNull((e) => e.getType()?.contains('audio') == true);
final isAAC = audioStream?.getCodec()?.toLowerCase() != 'aac';
String ffmpegCommand =
@@ -210,7 +219,8 @@ class IMUtils {
if (isAAC) {
return File(targetPath);
} else {
ffmpegCommand = '-i $path -c:v copy -c:a aac -q:a 2 -threads 4 $targetPath';
ffmpegCommand =
'-i $path -c:v copy -c:a aac -q:a 2 -threads 4 $targetPath';
}
}
@@ -220,7 +230,8 @@ class IMUtils {
return File(targetPath);
} else {
ffmpegCommand = '-i $path -c:v copy -c:a aac -q:a 2 -threads 4 $targetPath';
ffmpegCommand =
'-i $path -c:v copy -c:a aac -q:a 2 -threads 4 $targetPath';
}
}
@@ -230,7 +241,8 @@ class IMUtils {
final returnCode = await session.getReturnCode();
if (state == SessionState.failed || !ReturnCode.isSuccess(returnCode)) {
Logger().printError(info: "Command failed. Please check output for the details.");
Logger().printError(
info: "Command failed. Please check output for the details.");
file.copySync(targetPath);
return File(targetPath);
@@ -241,7 +253,8 @@ class IMUtils {
return File(targetPath);
}
static Future<File?> compressImageAndGetFile(File file, {int quality = 80}) async {
static Future<File?> compressImageAndGetFile(File file,
{int quality = 80}) async {
var path = file.path;
var name = path.substring(path.lastIndexOf("/") + 1).toLowerCase();
@@ -360,14 +373,16 @@ class IMUtils {
String? externalStorageDirPath;
if (Platform.isAndroid) {
try {
externalStorageDirPath = await PathProviderPlatform.instance.getDownloadsPath();
externalStorageDirPath =
await PathProviderPlatform.instance.getDownloadsPath();
} catch (err, st) {
Logger.print('failed to get downloads path: $err, $st');
final directory = await getExternalStorageDirectory();
externalStorageDirPath = directory?.path;
}
} else if (Platform.isIOS) {
externalStorageDirPath = (await getApplicationDocumentsDirectory()).absolute.path;
externalStorageDirPath =
(await getApplicationDocumentsDirectory()).absolute.path;
}
return externalStorageDirPath!;
}
@@ -384,7 +399,8 @@ class IMUtils {
return path;
}
static List<Message> calChatTimeInterval(List<Message> list, {bool calculate = true}) {
static List<Message> calChatTimeInterval(List<Message> list,
{bool calculate = true}) {
if (!calculate) return list;
var milliseconds = list.firstOrNull?.sendTime;
if (null == milliseconds) return list;
@@ -421,7 +437,9 @@ class IMUtils {
final yesterday = now.subtract(Duration(days: 1));
if (isSameDay(dateTime, yesterday)) {
return isChinese ? '昨天 ${formatter.format(dateTime)}' : 'Yesterday ${formatter.format(dateTime)}';
return isChinese
? '昨天 ${formatter.format(dateTime)}'
: 'Yesterday ${formatter.format(dateTime)}';
}
if (isSameWeek(dateTime, now)) {
@@ -448,13 +466,16 @@ class IMUtils {
}
static bool isSameDay(DateTime date1, DateTime date2) {
return date1.year == date2.year && date1.month == date2.month && date1.day == date2.day;
return date1.year == date2.year &&
date1.month == date2.month &&
date1.day == date2.day;
}
static bool isSameWeek(DateTime date1, DateTime date2) {
final weekStart = date2.subtract(Duration(days: date2.weekday - 1));
final weekEnd = weekStart.add(Duration(days: 6));
return date1.isAfter(weekStart.subtract(Duration(days: 1))) && date1.isBefore(weekEnd.add(Duration(days: 1)));
return date1.isAfter(weekStart.subtract(Duration(days: 1))) &&
date1.isBefore(weekEnd.add(Duration(days: 1)));
}
static String getCallTimeline(int milliseconds) {
@@ -530,7 +551,8 @@ class IMUtils {
return "${_combTime(days, StrRes.day)}${_combTime(hours, StrRes.hours)}${_combTime(minutes, StrRes.minute)}${_combTime(seconds, StrRes.seconds)}";
}
static String _combTime(int value, String unit) => value > 0 ? '$value$unit' : '';
static String _combTime(int value, String unit) =>
value > 0 ? '$value$unit' : '';
static String calContent({
required String content,
@@ -566,9 +588,11 @@ class IMUtils {
int maxLines = 1,
double maxWidth = double.infinity,
}) {
final TextPainter textPainter =
TextPainter(text: TextSpan(text: text, style: style), maxLines: maxLines, textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: maxWidth);
final TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: maxLines,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: maxWidth);
return textPainter.size;
}
@@ -578,7 +602,10 @@ class IMUtils {
int maxLines = 1,
double maxWidth = double.infinity,
}) =>
TextPainter(text: TextSpan(text: text, style: style), maxLines: maxLines, textDirection: TextDirection.ltr)
TextPainter(
text: TextSpan(text: text, style: style),
maxLines: maxLines,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: maxWidth);
static bool isUrlValid(String? url) {
@@ -600,11 +627,16 @@ class IMUtils {
}
static String getGroupMemberShowName(GroupMembersInfo membersInfo) {
return membersInfo.userID == OpenIM.iMManager.userID ? StrRes.you : membersInfo.nickname!;
return membersInfo.userID == OpenIM.iMManager.userID
? StrRes.you
: membersInfo.nickname!;
}
static String getShowName(String? userID, String? nickname) {
return (userID == OpenIM.iMManager.userID ? OpenIM.iMManager.userInfo.nickname : nickname) ?? '';
return (userID == OpenIM.iMManager.userID
? OpenIM.iMManager.userInfo.nickname
: nickname) ??
'';
}
static String? parseNtf(
@@ -628,7 +660,8 @@ class IMUtils {
case MessageType.groupInfoSetNotification:
{
final ntf = GroupNotification.fromJson(map);
if (ntf.group?.notification != null && ntf.group!.notification!.isNotEmpty) {
if (ntf.group?.notification != null &&
ntf.group!.notification!.isNotEmpty) {
return isConversation ? ntf.group!.notification! : null;
}
@@ -649,8 +682,12 @@ class IMUtils {
final ntf = InvitedJoinGroupNotification.fromJson(map);
final label = StrRes.invitedJoinGroupNtf;
final b = ntf.invitedUserList?.map((e) => getGroupMemberShowName(e)).toList().join('');
text = sprintf(label, [getGroupMemberShowName(ntf.opUser!), b ?? '']);
final b = ntf.invitedUserList
?.map((e) => getGroupMemberShowName(e))
.toList()
.join('');
text = sprintf(
label, [getGroupMemberShowName(ntf.opUser!), b ?? '']);
}
break;
case MessageType.memberKickedNotification:
@@ -658,7 +695,10 @@ class IMUtils {
final ntf = KickedGroupMemeberNotification.fromJson(map);
final label = StrRes.kickedGroupNtf;
final b = ntf.kickedUserList!.map((e) => getGroupMemberShowName(e)).toList().join('');
final b = ntf.kickedUserList!
.map((e) => getGroupMemberShowName(e))
.toList()
.join('');
text = sprintf(label, [b, getGroupMemberShowName(ntf.opUser!)]);
}
break;
@@ -683,7 +723,10 @@ class IMUtils {
final ntf = GroupRightsTransferNoticication.fromJson(map);
final label = StrRes.transferredGroupNtf;
text = sprintf(label, [getGroupMemberShowName(ntf.opUser!), getGroupMemberShowName(ntf.newGroupOwner!)]);
text = sprintf(label, [
getGroupMemberShowName(ntf.opUser!),
getGroupMemberShowName(ntf.newGroupOwner!)
]);
}
break;
case MessageType.groupMemberMutedNotification:
@@ -692,8 +735,11 @@ class IMUtils {
final label = StrRes.muteMemberNtf;
final c = ntf.mutedSeconds;
text = sprintf(
label, [getGroupMemberShowName(ntf.mutedUser!), getGroupMemberShowName(ntf.opUser!), mutedTime(c!)]);
text = sprintf(label, [
getGroupMemberShowName(ntf.mutedUser!),
getGroupMemberShowName(ntf.opUser!),
mutedTime(c!)
]);
}
break;
case MessageType.groupMemberCancelMutedNotification:
@@ -701,7 +747,10 @@ class IMUtils {
final ntf = MuteMemberNotification.fromJson(map);
final label = StrRes.muteCancelMemberNtf;
text = sprintf(label, [getGroupMemberShowName(ntf.mutedUser!), getGroupMemberShowName(ntf.opUser!)]);
text = sprintf(label, [
getGroupMemberShowName(ntf.mutedUser!),
getGroupMemberShowName(ntf.opUser!)
]);
}
break;
case MessageType.groupMutedNotification:
@@ -737,7 +786,8 @@ class IMUtils {
break;
case MessageType.groupMemberInfoChangedNotification:
final ntf = GroupMemberInfoChangedNotification.fromJson(map);
text = sprintf(StrRes.memberInfoChangedNtf, [getGroupMemberShowName(ntf.opUser!)]);
text = sprintf(StrRes.memberInfoChangedNtf,
[getGroupMemberShowName(ntf.opUser!)]);
break;
case MessageType.groupInfoSetAnnouncementNotification:
if (isConversation) {
@@ -747,7 +797,8 @@ class IMUtils {
break;
case MessageType.groupInfoSetNameNotification:
final ntf = GroupNotification.fromJson(map);
text = sprintf(StrRes.whoModifyGroupName, [getGroupMemberShowName(ntf.opUser!), ntf.group?.groupName]);
text = sprintf(StrRes.whoModifyGroupName,
[getGroupMemberShowName(ntf.opUser!), ntf.group?.groupName]);
break;
}
}
@@ -851,7 +902,8 @@ class IMUtils {
switch (customType) {
case CustomMessageType.call:
var type = map['data']['type'];
content = '[${type == 'video' ? StrRes.callVideo : StrRes.callVoice}]';
content =
'[${type == 'video' ? StrRes.callVideo : StrRes.callVoice}]';
break;
case CustomMessageType.emoji:
content = '[${StrRes.emoji}]';
@@ -924,7 +976,8 @@ class IMUtils {
switch (state) {
case 'beHangup':
case 'hangup':
content = sprintf(StrRes.callDuration, [seconds2HMS(duration)]);
content =
sprintf(StrRes.callDuration, [seconds2HMS(duration)]);
break;
case 'cancel':
content = StrRes.cancelled;
@@ -991,8 +1044,11 @@ class IMUtils {
final atUserInfos = message.atTextElem!.atUsersInfo!;
for (final userID in atUserIDs) {
final groupNickname =
(newMapping[userID] ?? atUserInfos.firstWhere((e) => e.atUserID == userID).groupNickname) ?? userID;
final groupNickname = (newMapping[userID] ??
atUserInfos
.firstWhere((e) => e.atUserID == userID)
.groupNickname) ??
userID;
mapping[userID] = getAtNickname(userID, groupNickname);
}
}
@@ -1065,20 +1121,27 @@ class IMUtils {
previewUrlPicture(
[
MediaSource(
url: message.pictureElem!.sourcePicture!.url!, thumbnail: message.pictureElem!.snapshotPicture!.url!)
url: message.pictureElem!.sourcePicture!.url!,
thumbnail: message.pictureElem!.snapshotPicture!.url!)
],
currentIndex: 0,
);
} else {
final picList = allList
.where((element) => element.contentType == MessageType.picture || element.contentType == MessageType.video)
.where((element) =>
element.contentType == MessageType.picture ||
element.contentType == MessageType.video)
.toList();
final index = picList.indexOf(message);
final urls = picList.map((e) {
if (e.contentType == MessageType.picture) {
return MediaSource(url: e.pictureElem!.sourcePicture!.url!, thumbnail: e.pictureElem!.snapshotPicture!.url!);
return MediaSource(
url: e.pictureElem!.sourcePicture!.url!,
thumbnail: e.pictureElem!.snapshotPicture!.url!);
} else {
return MediaSource(url: e.videoElem!.videoUrl!, thumbnail: e.videoElem!.snapshotUrl!);
return MediaSource(
url: e.videoElem!.videoUrl!,
thumbnail: e.videoElem!.snapshotUrl!);
}
}).toList();
previewUrlPicture(urls, currentIndex: index == -1 ? 0 : index);
@@ -1106,7 +1169,8 @@ class IMUtils {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.easeOut;
final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
final tween =
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
final offsetAnimation = animation.drive(tween);
return SlideTransition(
@@ -1136,7 +1200,8 @@ class IMUtils {
final isExitCachePath = await isExitFile(cachePath);
Logger.print('isExitSourcePath:$isExitSourcePath, isExitCachePath:$isExitCachePath, cachePath:$cachePath');
Logger.print(
'isExitSourcePath:$isExitSourcePath, isExitCachePath:$isExitCachePath, cachePath:$cachePath');
final isExitNetwork = isUrlValid(url);
String? availablePath;
@@ -1145,9 +1210,11 @@ class IMUtils {
} else if (isExitCachePath) {
availablePath = cachePath;
}
final isAvailableFileSize =
isExitSourcePath || isExitCachePath ? (await File(availablePath!).length() == fileSize) : false;
Logger.print('previewFile isAvailableFileSize: $isAvailableFileSize isExitNetwork: $isExitNetwork');
final isAvailableFileSize = isExitSourcePath || isExitCachePath
? (await File(availablePath!).length() == fileSize)
: false;
Logger.print(
'previewFile isAvailableFileSize: $isAvailableFileSize isExitNetwork: $isExitNetwork');
if (isAvailableFileSize) {
String? mimeType = lookupMimeType(fileName ?? '');
if (null != mimeType && allowVideoType(mimeType)) {
@@ -1159,7 +1226,9 @@ class IMUtils {
previewPicture(Message()
..clientMsgID = message.clientMsgID
..contentType = MessageType.picture
..pictureElem = PictureElem(sourcePath: availablePath, sourcePicture: PictureInfo(url: url)));
..pictureElem = PictureElem(
sourcePath: availablePath,
sourcePicture: PictureInfo(url: url)));
} else {
openFileByOtherApp(availablePath);
}
@@ -1183,7 +1252,8 @@ class IMUtils {
bool onlySave = false,
ValueChanged<OperateType>? onOperate}) {
void saveVideo(BuildContext ctx, String url, {int? length}) async {
final cachedVideoControllerService = CachedVideoControllerService(DefaultCacheManager());
final cachedVideoControllerService =
CachedVideoControllerService(DefaultCacheManager());
final cached = await cachedVideoControllerService.getCacheFile(url);
if (cached != null) {
@@ -1196,7 +1266,8 @@ class IMUtils {
} else {
LoadingView.singleton.show();
final downloader = MultiThreadDownloader(url: url, fileName: url.split('/').last, length: length);
final downloader = MultiThreadDownloader(
url: url, fileName: url.split('/').last, length: length);
callback(EasyLoadingStatus status) {
if (status == EasyLoadingStatus.dismiss) {
@@ -1235,7 +1306,8 @@ class IMUtils {
switch (type) {
case OperateType.save:
if (msg.videoElem != null) {
saveVideo(context, msg.videoElem!.videoUrl!, length: msg.videoElem!.videoSize);
saveVideo(context, msg.videoElem!.videoUrl!,
length: msg.videoElem!.videoSize);
} else {
final url = msg.pictureElem?.sourcePicture?.url;
if (url?.isNotEmpty == true) {
@@ -1254,14 +1326,18 @@ class IMUtils {
final sources = message.isVideoType
? MediaSource(
url: message.videoElem?.videoUrl,
thumbnail: message.videoElem!.snapshotUrl?.adjustThumbnailAbsoluteString(960) ?? '',
thumbnail: message.videoElem!.snapshotUrl
?.adjustThumbnailAbsoluteString(960) ??
'',
file: File(message.videoElem!.videoPath!),
tag: message.clientMsgID,
isVideo: true,
)
: MediaSource(
url: message.pictureElem?.sourcePicture?.url,
thumbnail: message.pictureElem!.snapshotPicture?.url?.adjustThumbnailAbsoluteString(960) ?? '',
thumbnail: message.pictureElem!.snapshotPicture?.url
?.adjustThumbnailAbsoluteString(960) ??
'',
file: File(message.pictureElem!.sourcePath!),
tag: message.clientMsgID,
);
@@ -1326,19 +1402,36 @@ class IMUtils {
}
static void previewLocation(Message message) {
var location = message.locationElem;
Map detail = json.decode(location!.description!);
Logger.print('previewLocation ${location.latitude} ${location.longitude}');
Get.to(
() => MapView(
try {
final location = message.locationElem;
if (location == null ||
location.latitude == null ||
location.longitude == null) {
IMViews.showToast(StrRes.locationMessage);
return;
}
final data = LocationBubbleData.parse(
description: location.description ?? '',
latitude: location.latitude!,
longitude: location.longitude!,
address1: detail['name'],
address2: detail['addr'],
),
transition: Transition.cupertino,
popGesture: true,
);
fallbackTitle: StrRes.locationMessage,
);
Logger.print(
'previewLocation ${location.latitude} ${location.longitude}');
Get.to(
() => MapView(
latitude: location.latitude!,
longitude: location.longitude!,
address1: data.title,
address2: data.address,
),
transition: Transition.cupertino,
popGesture: true,
);
} catch (e, s) {
Logger.print('previewLocation failed: $e $s');
IMViews.showToast(StrRes.locationMessage);
}
}
static void previewCarteMessage(
@@ -1353,7 +1446,8 @@ class IMUtils {
Function(Message msg)? meetingItemClick,
VoidCallback? onForward,
}) async {
if (message.contentType == MessageType.picture || message.contentType == MessageType.video) {
if (message.contentType == MessageType.picture ||
message.contentType == MessageType.video) {
previewMediaFile(
context: Get.context!,
message: message,
@@ -1464,15 +1558,18 @@ class IMUtils {
if (mimeType == 'application/pdf') {
return ImageRes.filePdf;
} else if (mimeType == 'application/msword' ||
mimeType == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
mimeType ==
'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
return ImageRes.fileWord;
} else if (mimeType == 'application/vnd.ms-excel' ||
mimeType == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
mimeType ==
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
return ImageRes.fileExcel;
} else if (mimeType == 'application/vnd.ms-powerpoint') {
return ImageRes.filePpt;
} else if (mimeType.startsWith('audio/')) {
} else if (mimeType == 'application/zip' || mimeType == 'application/x-rar-compressed') {
} else if (mimeType == 'application/zip' ||
mimeType == 'application/x-rar-compressed') {
return ImageRes.fileZip;
}
/*else if (mimeType.startsWith('audio/')) {
@@ -1516,7 +1613,10 @@ class IMUtils {
final checkedList = <String>[];
final values = result.values;
for (final value in values) {
if (value is UserInfo || value is FriendInfo || value is UserFullInfo || value is ISUserInfo) {
if (value is UserInfo ||
value is FriendInfo ||
value is UserFullInfo ||
value is ISUserInfo) {
checkedList.add(value.userID!);
}
}
@@ -1531,7 +1631,10 @@ class IMUtils {
for (var item in checkedList) {
if (item is ConversationInfo) {
checkedMap[item.isSingleChat ? item.userID! : item.groupID!] = item;
} else if (item is UserInfo || item is UserFullInfo || item is ISUserInfo || item is FriendInfo) {
} else if (item is UserInfo ||
item is UserFullInfo ||
item is ISUserInfo ||
item is FriendInfo) {
checkedMap[item.userID!] = item;
} else if (item is GroupInfo) {
checkedMap[item.groupID] = item;
@@ -1540,10 +1643,14 @@ class IMUtils {
return checkedMap;
}
static List<Map<String, String?>> convertCheckedListToForwardObj(List<dynamic> checkedList) {
static List<Map<String, String?>> convertCheckedListToForwardObj(
List<dynamic> checkedList) {
final map = <Map<String, String?>>[];
for (var item in checkedList) {
if (item is UserInfo || item is UserFullInfo || item is ISUserInfo || item is FriendInfo) {
if (item is UserInfo ||
item is UserFullInfo ||
item is ISUserInfo ||
item is FriendInfo) {
map.add({'nickname': item.nickname, 'faceURL': item.faceURL});
} else if (item is GroupInfo) {
map.add({'nickname': item.groupName, 'faceURL': item.faceURL});
@@ -1555,7 +1662,10 @@ class IMUtils {
}
static String? convertCheckedToUserID(dynamic info) {
if (info is UserInfo || info is UserFullInfo || info is ISUserInfo || info is FriendInfo) {
if (info is UserInfo ||
info is UserFullInfo ||
info is ISUserInfo ||
info is FriendInfo) {
return info.userID;
} else if (info is ConversationInfo) {
return info.userID;
@@ -1570,14 +1680,18 @@ class IMUtils {
} else if (info is ConversationInfo) {
return info.groupID;
}
return null;
}
static List<Map<String, String?>> convertCheckedListToShare(Iterable<dynamic> checkedList) {
static List<Map<String, String?>> convertCheckedListToShare(
Iterable<dynamic> checkedList) {
final map = <Map<String, String?>>[];
for (var item in checkedList) {
if (item is UserInfo || item is UserFullInfo || item is ISUserInfo || item is FriendInfo) {
if (item is UserInfo ||
item is UserFullInfo ||
item is ISUserInfo ||
item is FriendInfo) {
map.add({'userID': item.userID, 'groupID': null});
} else if (item is GroupInfo) {
map.add({'userID': null, 'groupID': item.groupID});
@@ -1612,8 +1726,10 @@ class IMUtils {
return formatDateMs(ms, format: isZH ? 'yyyy年MM月dd' : 'yyyy/MM/dd');
}
static Future<bool> checkingBiometric(LocalAuthentication auth) => auth.authenticate(
localizedReason: 'Scan your fingerprint (or face or other) to authenticate.',
static Future<bool> checkingBiometric(LocalAuthentication auth) =>
auth.authenticate(
localizedReason:
'Scan your fingerprint (or face or other) to authenticate.',
options: const AuthenticationOptions(
biometricOnly: true,
),
@@ -1636,7 +1752,8 @@ class IMUtils {
goToSettingsButton: 'Go to settings',
goToSettingsDescription:
'No biometric authentication is set up on your device. Please enable Touch ID or Face ID on your phone.',
lockOut: 'Biometric authentication is disabled. Please lock and unlock your screen to enable it.',
lockOut:
'Biometric authentication is disabled. Please lock and unlock your screen to enable it.',
),
],
);
@@ -1664,11 +1781,15 @@ class IMUtils {
r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d\S]{6,20}$',
).hasMatch(password);
static TextInputFormatter getPasswordFormatter() => FilteringTextInputFormatter.allow(
static TextInputFormatter getPasswordFormatter() =>
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9\S]'),
);
static Future requestBackgroundPermission({required String title, required String text, bool isRetry = false}) async {
static Future requestBackgroundPermission(
{required String title,
required String text,
bool isRetry = false}) async {
if (!Platform.isAndroid) {
return;
}
@@ -1680,7 +1801,8 @@ class IMUtils {
notificationTitle: title,
notificationText: text,
notificationImportance: AndroidNotificationImportance.normal,
notificationIcon: const AndroidResource(name: 'ic_launcher', defType: 'mipmap'),
notificationIcon: const AndroidResource(
name: 'ic_launcher', defType: 'mipmap'),
shouldRequestBatteryOptimizationsOff: false));
}
if (hasPermissions && !FlutterBackground.isBackgroundExecutionEnabled) {
@@ -1689,7 +1811,9 @@ class IMUtils {
} catch (e) {
if (!isRetry) {
return await Future<void>.delayed(
const Duration(seconds: 1), () => requestBackgroundPermission(title: title, text: text, isRetry: true));
const Duration(seconds: 1),
() => requestBackgroundPermission(
title: title, text: text, isRetry: true));
}
}
}
@@ -1698,5 +1822,6 @@ class IMUtils {
extension PlatformExt on Platform {
static bool get isMobile => Platform.isIOS || Platform.isAndroid;
static bool get isDesktop => Platform.isLinux || Platform.isMacOS || Platform.isWindows;
static bool get isDesktop =>
Platform.isLinux || Platform.isMacOS || Platform.isWindows;
}
@@ -38,7 +38,9 @@ class Button extends StatelessWidget {
child: Ink(
height: height ?? 44.h,
decoration: BoxDecoration(
color: enabled ? enabledColor ?? Styles.c_0089FF : disabledColor ?? Styles.c_0089FF_opacity50,
color: enabled
? enabledColor ?? Styles.c_0089FF
: disabledColor ?? Styles.c_0089FF_opacity50,
borderRadius: BorderRadius.circular(radius ?? 4.r),
),
child: InkWell(
@@ -78,7 +80,7 @@ class ImageTextButton extends StatelessWidget {
final Function()? onTap;
ImageTextButton.call({super.key, this.onTap})
: icon = ImageRes.audioAndVideoCall,
: icon = ImageRes.callVoice,
text = StrRes.audioAndVideoCall,
color = Styles.c_FFFFFF,
textStyle = null,
@@ -4,6 +4,48 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:openim_common/openim_common.dart';
class LocationBubbleData {
const LocationBubbleData({
required this.title,
required this.address,
this.thumbnailUrl,
});
final String title;
final String address;
final String? thumbnailUrl;
static LocationBubbleData parse({
required String description,
required double latitude,
required double longitude,
String fallbackTitle = '位置消息',
}) {
Map<String, dynamic>? map;
try {
final decoded = json.decode(description);
if (decoded is Map) {
map = decoded.map((key, value) => MapEntry(key.toString(), value));
}
} catch (_) {}
final name = map?['name']?.toString().trim() ?? '';
final addr = map?['addr']?.toString().trim() ?? '';
final url = map?['url']?.toString().trim() ?? '';
final coord =
'${latitude.toStringAsFixed(5)}, ${longitude.toStringAsFixed(5)}';
final raw = description.trim();
return LocationBubbleData(
title: name.isNotEmpty ? name : fallbackTitle,
address: addr.isNotEmpty
? addr
: (map == null && raw.isNotEmpty ? raw : coord),
thumbnailUrl: url.isNotEmpty ? url : null,
);
}
}
class ChatLocationView extends StatelessWidget {
const ChatLocationView({
Key? key,
@@ -14,53 +56,62 @@ class ChatLocationView extends StatelessWidget {
final String description;
final double latitude;
final double longitude;
final _decoder = const JsonDecoder();
@override
Widget build(BuildContext context) {
try {
final map = _decoder.convert(description);
String url = map['url'] ?? '';
String name = map['name'] ?? '';
String addr = map['addr'] ?? '';
return Container(
width: locationWidth,
height: 130.h,
decoration: BoxDecoration(
color: Styles.c_FFFFFF,
border: Border.all(color: Styles.c_E8EAEF, width: 1),
borderRadius: BorderRadius.circular(6.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
4.verticalSpace,
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: name.toText
..style = Styles.ts_0C1C33_14sp
..maxLines = 1
..overflow = TextOverflow.ellipsis,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: addr.toText
..style = Styles.ts_8E9AB0_12sp
..maxLines = 1
..overflow = TextOverflow.ellipsis,
),
2.verticalSpace,
Expanded(
child: ImageUtil.networkImage(
url: url,
width: locationWidth,
fit: BoxFit.cover,
),
),
],
),
);
} catch (e) {}
return Container();
final data = LocationBubbleData.parse(
description: description,
latitude: latitude,
longitude: longitude,
fallbackTitle: StrRes.locationMessage,
);
return Container(
width: locationWidth,
height: 130.h,
decoration: BoxDecoration(
color: Styles.c_FFFFFF,
border: Border.all(color: Styles.c_E8EAEF, width: 1),
borderRadius: BorderRadius.circular(6.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
4.verticalSpace,
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: data.title.toText
..style = Styles.ts_0C1C33_14sp
..maxLines = 1
..overflow = TextOverflow.ellipsis,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: data.address.toText
..style = Styles.ts_8E9AB0_12sp
..maxLines = 1
..overflow = TextOverflow.ellipsis,
),
2.verticalSpace,
Expanded(
child: data.thumbnailUrl == null
? ColoredBox(
color: Styles.c_F0F2F6,
child: Center(
child: Icon(
Icons.location_on_outlined,
color: Styles.c_8E9AB0,
size: 28.w,
),
),
)
: ImageUtil.networkImage(
url: data.thumbnailUrl!,
width: locationWidth,
fit: BoxFit.cover,
),
),
],
),
);
}
}
@@ -10,7 +10,6 @@ class ChatToolBox extends StatelessWidget {
this.onTapCamera,
this.onTapCard,
this.onTapFile,
this.onTapLocation,
this.onTapDirectionalMessage,
});
final Function()? onTapAlbum;
@@ -18,7 +17,6 @@ class ChatToolBox extends StatelessWidget {
final Function()? onTapCall;
final Function()? onTapFile;
final Function()? onTapCard;
final Function()? onTapLocation;
final VoidCallback? onTapDirectionalMessage;
@override
@@ -50,11 +48,6 @@ class ChatToolBox extends StatelessWidget {
icon: ImageRes.toolboxCard,
onTap: onTapCard,
),
ToolboxItemInfo(
text: StrRes.toolboxLocation,
icon: ImageRes.toolboxLocation,
onTap: () => Permissions.location(onTapLocation),
),
if (onTapDirectionalMessage != null)
ToolboxItemInfo(
text: StrRes.toolboxDirectionalMessage,
@@ -63,9 +56,11 @@ class ChatToolBox extends StatelessWidget {
),
];
final rowCount = (items.length / 4).ceil().clamp(1, 2);
return Container(
color: Styles.c_F0F2F6,
height: 224.h,
height: rowCount <= 1 ? 118.h : 224.h,
child: GridView.builder(
itemCount: items.length,
padding: EdgeInsets.only(
@@ -1,269 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:openim_common/openim_common.dart';
import 'package:geolocator/geolocator.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
class ChatWebViewMap extends StatefulWidget {
const ChatWebViewMap({
super.key,
required this.host,
required this.webKey,
required this.webServerKey,
this.mapThumbnailSize = "1200*600",
this.mapBackUrl = "http://callback",
this.latitude,
this.longitude,
});
final String host;
final String webKey;
final String webServerKey;
final String mapThumbnailSize;
final String mapBackUrl;
final double? latitude;
final double? longitude;
@override
State<ChatWebViewMap> createState() => _ChatWebViewMapState();
}
class _ChatWebViewMapState extends State<ChatWebViewMap> {
WebViewController? _controller;
String url = "";
double progress = 0;
double? latitude;
double? longitude;
String? description;
late String locationUrl;
late String thumbnailUrl;
late String previewLocationUrl;
late String webKey;
late String webServerKey;
late String host;
void _configUrl() {
locationUrl = "$host?key=$webKey&serverKey=$webServerKey#/";
previewLocationUrl = "$host?key=$webKey&serverKey=$webServerKey&location=$longitude,$latitude#/";
}
String getStaticMapURL(double longitude, double latitude) {
final url =
'https://restapi.amap.com/v3/staticmap?location=$longitude,$latitude&zoom=13&size=200*200&markers=mid,,A:$longitude,$latitude&key=$webServerKey';
return url;
}
bool get isPreview => widget.longitude != null && widget.latitude != null;
@override
void initState() {
super.initState();
host = widget.host;
webKey = widget.webKey;
webServerKey = widget.webServerKey;
longitude = widget.longitude;
latitude = widget.latitude;
_determinePosition().then((position) {
longitude = position.longitude;
latitude = position.latitude;
_configUrl();
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
final WebViewController controller = WebViewController.fromPlatformCreationParams(params);
controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
debugPrint('WebView is loading (progress : $progress%)');
setState(() {
this.progress = progress / 100;
});
},
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
onPageFinished: (String url) {
debugPrint('Page finished loading: $url');
},
onWebResourceError: (WebResourceError error) {
debugPrint(
'Page resource error: code: ${error.errorCode} description: ${error.description} errorType: ${error.errorType} isForMainFrame: ${error.isForMainFrame}');
},
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
debugPrint('blocking navigation to ${request.url}');
return NavigationDecision.prevent;
}
debugPrint('allowing navigation to ${request.url}');
return NavigationDecision.navigate;
},
onHttpError: (HttpResponseError error) {
debugPrint('Error occurred on page: ${error.response?.statusCode}');
},
onUrlChange: (UrlChange change) {
debugPrint('url change to ${change.url}');
},
onHttpAuthRequest: (HttpAuthRequest request) {},
),
)
..addJavaScriptChannel(
'getLocaltion',
onMessageReceived: (JavaScriptMessage message) {
final value = message.message;
final params = jsonDecode(value);
final locationStr = params['location'] as String;
final locations = locationStr.split(',');
longitude = double.parse(locations[0]);
latitude = double.parse(locations[1]);
final address = params['address'];
final url = getStaticMapURL(longitude!, latitude!);
final result = {
'longitude': longitude,
'latitude': latitude,
'url': url,
'addr': address,
'name': '',
};
description = jsonEncode(result);
Logger.print('$result');
_confirm();
},
)
..loadRequest(Uri.parse(previewLocationUrl));
print('previewLocationUrl: $previewLocationUrl');
if (!Platform.isMacOS) {
controller.setBackgroundColor(const Color(0x80000000));
}
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(controller.platform as AndroidWebViewController).setMediaPlaybackRequiresUserGesture(false);
}
setState(() {
_controller = controller;
});
});
}
Future<Position> _determinePosition() async {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permissions are denied');
}
}
if (permission == LocationPermission.deniedForever) {
return Future.error('Location permissions are permanently denied, we cannot request permissions.');
}
return await Geolocator.getCurrentPosition();
}
@override
void dispose() {
super.dispose();
}
void _confirm() async {
if (null == latitude || null == longitude) {
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: StrRes.plsSelectLocation.toText..style = Styles.ts_0C1C33_17sp_semibold,
actions: [
GestureDetector(
onTap: () => Navigator.pop(context),
behavior: HitTestBehavior.translucent,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
child: StrRes.determine.toText..style = Styles.ts_0089FF_17sp_semibold,
),
),
],
),
);
return;
}
Navigator.pop(context, {
'latitude': latitude,
'longitude': longitude,
'description': description,
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: TitleBar.back(
onTap: () async {
Get.back();
},
title: StrRes.location,
),
body: SafeArea(
child: Stack(
children: [
_controller == null
? const Align(
child: CupertinoActivityIndicator(),
)
: WebViewWidget(controller: _controller!),
progress < 1.0
? LinearProgressIndicator(
value: progress,
color: Colors.blue,
)
: const SizedBox(),
],
),
),
);
}
}
@@ -34,7 +34,8 @@ class MapView extends StatelessWidget {
),
children: [
TileLayer(
urlTemplate: 'https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
urlTemplate:
'https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
userAgentPackageName: '',
),
MarkerLayer(
@@ -88,19 +89,27 @@ class MapView extends StatelessWidget {
}
_openMapSheet() async {
final availableMaps = await ml.MapLauncher.installedMaps;
Get.bottomSheet(
BottomSheetView(
items: availableMaps
.map((e) => SheetItem(
label: _mapLabel(e),
onTap: () async {
_launcherMap(e);
},
))
.toList(),
),
);
try {
final availableMaps = await ml.MapLauncher.installedMaps;
if (availableMaps.isEmpty) {
IMViews.showToast(StrRes.locationMessage);
return;
}
Get.bottomSheet(
BottomSheetView(
items: availableMaps
.map((e) => SheetItem(
label: _mapLabel(e),
onTap: () async {
_launcherMap(e);
},
))
.toList(),
),
);
} catch (_) {
IMViews.showToast(StrRes.locationMessage);
}
}
String _mapLabel(ml.AvailableMap map) {
@@ -119,11 +128,15 @@ class MapView extends StatelessWidget {
}
_launcherMap(ml.AvailableMap map) async {
await ml.MapLauncher.showMarker(
mapType: map.mapType,
coords: ml.Coords(latitude, longitude),
title: address1,
description: address2,
);
try {
await ml.MapLauncher.showMarker(
mapType: map.mapType,
coords: ml.Coords(latitude, longitude),
title: address1,
description: address2,
);
} catch (_) {
IMViews.showToast(StrRes.locationMessage);
}
}
}
@@ -14,11 +14,9 @@ class RichTextInputBox extends StatefulWidget {
this.showCameraIcon = true,
this.showCardIcon = true,
this.showFileIcon = true,
this.showLocationIcon = true,
this.onTapAlbum,
this.onTapCard,
this.onTapFile,
this.onTapLocation,
this.onSend,
}) : super(key: key);
final TextEditingController? controller;
@@ -29,12 +27,10 @@ class RichTextInputBox extends StatefulWidget {
final bool showCameraIcon;
final bool showFileIcon;
final bool showCardIcon;
final bool showLocationIcon;
final Function()? onTapAlbum;
final Function()? onTapCamera;
final Function()? onTapFile;
final Function()? onTapCard;
final Function()? onTapLocation;
final Function()? onSend;
@override
@@ -113,19 +109,12 @@ class _RichTextInputBoxState extends State<RichTextInputBox> {
..height = 22.h
..opacity = _opacity
..onTap = widget.onTapCard,
if (widget.showLocationIcon)
ImageRes.toolboxLocation1.toImage
..width = 16.w
..height = 22.h
..opacity = _opacity
..onTap = widget.onTapLocation,
],
),
if (widget.showAlbumIcon ||
widget.showCameraIcon ||
widget.showCardIcon ||
widget.showFileIcon ||
widget.showLocationIcon)
widget.showFileIcon)
15.verticalSpace,
Row(
children: [
@@ -72,12 +72,6 @@ class IMViews {
alignment: MainAxisAlignment.start,
onTap: () => onTapSheetItem.call(0),
),
SheetItem(
label: StrRes.callVideo,
icon: ImageRes.callVideo,
alignment: MainAxisAlignment.start,
onTap: () => onTapSheetItem.call(1),
),
],
),
);
@@ -96,11 +90,6 @@ class IMViews {
icon: ImageRes.callVoice,
onTap: () => onTapSheetItem.call(0),
),
SheetItem(
label: StrRes.callVideo,
icon: ImageRes.callVideo,
onTap: () => onTapSheetItem.call(1),
),
],
),
);
@@ -115,7 +104,8 @@ class IMViews {
List<SheetItem> items = const [],
int quality = 80}) {
bool allowSendImageTypeHelper(String? mimeType) {
final result = mimeType?.contains('png') == true || mimeType?.contains('jpeg') == true;
final result = mimeType?.contains('png') == true ||
mimeType?.contains('jpeg') == true;
return result;
}
@@ -134,23 +124,25 @@ class IMViews {
SheetItem(
label: StrRes.toolboxAlbum,
onTap: () async {
final List<AssetEntity>? assets = await AssetPicker.pickAssets(Get.context!,
pickerConfig: AssetPickerConfig(
requestType: RequestType.image,
maxAssets: 1,
selectPredicate: (_, entity, isSelected) async {
if (await allowSendImageType(entity)) {
return true;
}
final List<AssetEntity>? assets =
await AssetPicker.pickAssets(Get.context!,
pickerConfig: AssetPickerConfig(
requestType: RequestType.image,
maxAssets: 1,
selectPredicate: (_, entity, isSelected) async {
if (await allowSendImageType(entity)) {
return true;
}
IMViews.showToast(StrRes.supportsTypeHint);
IMViews.showToast(StrRes.supportsTypeHint);
return false;
}));
return false;
}));
final file = await assets?.firstOrNull?.file;
if (file?.path != null) {
final map = await uCropPic(file!.path, crop: crop, toUrl: toUrl, quality: quality);
final map = await uCropPic(file!.path,
crop: crop, toUrl: toUrl, quality: quality);
onData?.call(map['path'], map['url']);
}
},
@@ -176,7 +168,8 @@ class IMViews {
final file = await entity?.file;
if (file?.path != null) {
final map = await uCropPic(file!.path, crop: crop, toUrl: toUrl, quality: quality);
final map = await uCropPic(file!.path,
crop: crop, toUrl: toUrl, quality: quality);
onData?.call(map['path'], map['url']);
}
},
@@ -206,7 +199,9 @@ class IMViews {
if (null != cropFile) {
Logger.print('-----------crop path: ${cropFile.path}');
result = await LoadingView.singleton.wrap(asyncFunction: () async {
final image = await IMUtils.compressImageAndGetFile(File(cropFile!.path), quality: quality);
final image = await IMUtils.compressImageAndGetFile(
File(cropFile!.path),
quality: quality);
return OpenIM.iMManager.uploadFile(
id: putID,
@@ -217,7 +212,8 @@ class IMViews {
} else {
Logger.print('-----------source path: $path');
result = await LoadingView.singleton.wrap(asyncFunction: () async {
final image = await IMUtils.compressImageAndGetFile(File(path), quality: quality);
final image = await IMUtils.compressImageAndGetFile(File(path),
quality: quality);
return OpenIM.iMManager.uploadFile(
id: putID,