This commit is contained in:
2025-06-30 16:24:20 +08:00
commit 68de5eb048
454 changed files with 56921 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
class TencentUtils{
static bool isTextNotEmpty(String? text){
return text != null && text.isNotEmpty;
}
static String? checkString(String? text){
return (text != null && text.isEmpty) ? null : text;
}
static String? checkStringWithoutSpace(String? text){
if (text == null || text.trim().isEmpty || text.contains(' ')) {
return null;
}
return text;
}
static String getFileType(String fileType) {
switch (fileType) {
case "3gp":
return "video/3gpp";
case "torrent":
return "application/x-bittorrent";
case "kml":
return "application/vnd.google-earth.kml+xml";
case "gpx":
return "application/gpx+xml";
case "asf":
return "video/x-ms-asf";
case "avi":
return "video/x-msvideo";
case "bin":
case "class":
case "exe":
return "application/octet-stream";
case "bmp":
return "image/bmp";
case "c":
return "text/plain";
case "conf":
return "text/plain";
case "cpp":
return "text/plain";
case "doc":
return "application/msword";
case "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "xls":
case "csv":
return "application/vnd.ms-excel";
case "xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
case "gif":
return "image/gif";
case "gtar":
return "application/x-gtar";
case "gz":
return "application/x-gzip";
case "h":
return "text/plain";
case "htm":
return "text/html";
case "html":
return "text/html";
case "jar":
return "application/java-archive";
case "java":
return "text/plain";
case "jpeg":
return "image/jpeg";
case "jpg":
return "image/jpeg";
case "js":
return "application/x-javascript";
case "log":
return "text/plain";
case "m3u":
return "audio/x-mpegurl";
case "m4a":
return "audio/mp4a-latm";
case "m4b":
return "audio/mp4a-latm";
case "m4p":
return "audio/mp4a-latm";
case "m4u":
return "video/vnd.mpegurl";
case "m4v":
return "video/x-m4v";
case "mov":
return "video/quicktime";
case "mp2":
return "audio/x-mpeg";
case "mp3":
return "audio/x-mpeg";
case "mp4":
return "video/mp4";
case "mpc":
return "application/vnd.mpohun.certificate";
case "mpe":
return "video/mpeg";
case "mpeg":
return "video/mpeg";
case "mpg":
return "video/mpeg";
case "mpg4":
return "video/mp4";
case "mpga":
return "audio/mpeg";
case "msg":
return "application/vnd.ms-outlook";
case "ogg":
return "audio/ogg";
case "pdf":
return "application/pdf";
case "png":
return "image/png";
case "pps":
return "application/vnd.ms-powerpoint";
case "ppt":
return "application/vnd.ms-powerpoint";
case "pptx":
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
case "prop":
return "text/plain";
case "rc":
return "text/plain";
case "rmvb":
return "audio/x-pn-realaudio";
case "rtf":
return "application/rtf";
case "sh":
return "text/plain";
case "tar":
return "application/x-tar";
case "tgz":
return "application/x-compressed";
case "txt":
return "text/plain";
case "wav":
return "audio/x-wav";
case "wma":
return "audio/x-ms-wma";
case "wmv":
return "audio/x-ms-wmv";
case "wps":
return "application/vnd.ms-works";
case "xml":
return "text/plain";
case "z":
return "application/x-compress";
case "zip":
return "application/x-zip-compressed";
default:
return "*/*";
}
}
}
+9
View File
@@ -0,0 +1,9 @@
class CustomSticker {
const CustomSticker(
{required this.name, required this.index, this.url, this.unicode});
final int? unicode;
final String name;
final int index;
final String? url;
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
class ErrorMessageConverter {
static Map<int, String> errorCodeMap = {
10007: TIM_t("操作权限不足"),
20007: TIM_t("发送单聊消息,被对方拉黑,禁止发送。"),
30010: TIM_t("您的好友数已达系统上限"),
30014: TIM_t("对方的好友数已达系统上限"),
30015: TIM_t("对方已是您的好友"),
30515: TIM_t("被加好友在自己的黑名单中"),
30516: TIM_t("对方已禁止加好友"),
30525: TIM_t("您已被被对方设置为黑名单"),
30539: TIM_t("等待好友审核同意"),
};
static String getErrorMessage(int code) {
return errorCodeMap[code] ?? "";
}
}
+55
View File
@@ -0,0 +1,55 @@
// ignore_for_file: avoid_print, prefer_typing_uninitialized_variables
import 'dart:ui';
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
class Frame {
static var orginalCallback;
static init() {
orginalCallback = window.onReportTimings;
window.onReportTimings = onReportTimings;
}
// 仅缓存最近 25 帧绘制耗时
static const maxframes = 25;
static final List<FrameTiming> lastFrames = [];
// 基准 VSync 信号周期
static const frameInterval =
Duration(microseconds: Duration.microsecondsPerSecond ~/ 60);
static void onReportTimings(List<FrameTiming> timings) {
lastFrames.addAll(timings);
// 仅保留 25 帧
if (lastFrames.length > maxframes) {
lastFrames.removeRange(0, lastFrames.length - maxframes);
}
// 如果有原始帧回调函数,则执行
if (orginalCallback != null) {
orginalCallback(timings);
}
outputLogger.i("fps: $fps");
}
static double get fps {
int sum = 0;
for (FrameTiming timing in lastFrames) {
// 计算渲染耗时
int duration = timing.timestampInMicroseconds(FramePhase.rasterFinish) -
timing.timestampInMicroseconds(FramePhase.buildStart);
// 判断耗时是否在 Vsync 信号周期内
if (duration < frameInterval.inMicroseconds) {
sum += 1;
} else {
// 有丢帧,向上取整
int count = (duration / frameInterval.inMicroseconds).ceil();
sum += count;
}
}
return lastFrames.length / sum * 60;
}
static destroy() {
window.onReportTimings = null;
}
}
+13
View File
@@ -0,0 +1,13 @@
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
final outputLogger = TencentCloudChatLog();
class TencentCloudChatLog{
void i(String text){
if(!PlatformUtils().isWeb){
TencentImSDKPlugin.v2TIMManager
.uikitTrace(trace: text);
}
}
}
+346
View File
@@ -0,0 +1,346 @@
// ignore_for_file: unrelated_type_equality_checks, avoid_print
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_change_info_type.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_tips_elem_type.dart';
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_change_info.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_change_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_tips_elem.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_tips_elem.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_image.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_image.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_user_full_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart';
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
import 'package:tencent_cloud_chat_uikit/ui/constants/time.dart';
import 'package:collection/collection.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
class MessageUtils {
// 判断CallingData的方式和Trtc的方法一致
static isCallingData(String data) {
try {
Map<String, dynamic> customMap = jsonDecode(data);
if (customMap.containsKey('businessID') && customMap['businessID'] == 1) {
return true;
}
} catch (e) {
outputLogger.i("isCallingData json parse error");
return false;
}
return false;
}
// 是否是群组TRTC信息
static isGroupCallingMessage(V2TimMessage message) {
final isGroup = message.groupID != null;
final isCustomMessage = message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM;
if (isCustomMessage) {
final customElemData = message.customElem?.data ?? "";
return isCallingData(customElemData) && isGroup;
}
return false;
}
static String getCustomGroupCreatedOrDismissedString(V2TimMessage message) {
try {
final isGroup = message.groupID != null;
final isCustomMessage = message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM;
if (isCustomMessage && isGroup) {
final data = message.customElem?.data ?? "";
Map<String, dynamic> customMap = jsonDecode(data);
if (customMap.containsKey('businessID') && customMap['businessID'] == "group_create") {
final content = "${customMap['opUser']}${customMap['content']}";
return content;
}
return "";
}
return "";
} catch (e) {
outputLogger.i("getCustomGroupCreatedOrDismissedString json parse error");
return "";
}
}
static Future<String> _getGroupChangeType(
V2TimGroupChangeInfo info, List<V2TimGroupMemberFullInfo?> groupMemberList) async {
int? type = info.type;
var value = info.value;
String s = TIM_t('群资料信息');
switch (type) {
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_CUSTOM:
s = TIM_t("自定义字段");
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_FACE_URL:
s = TIM_t("群头像");
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_INTRODUCTION:
s = TIM_t("群简介");
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NAME:
s = TIM_t("群名称");
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NOTIFICATION:
s = TIM_t("群公告");
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_OWNER:
s = TIM_t("群主");
final V2TimGroupMemberFullInfo? groupMemberInfo =
groupMemberList.firstWhereOrNull((element) => element?.userID == value);
if (groupMemberInfo != null) {
value = TencentUtils.checkString(groupMemberInfo.friendRemark) ??
TencentUtils.checkString(groupMemberInfo.nameCard) ??
TencentUtils.checkString(groupMemberInfo.nickName) ??
TencentUtils.checkString(groupMemberInfo.userID);
} else {
final res = await TencentImSDKPlugin.v2TIMManager.getUsersInfo(userIDList: [value ?? ""]);
if (res.code == 0) {
final List<V2TimUserFullInfo> data = res.data ?? [];
if (data.isNotEmpty) {
final firstPerson = data[0];
value = TencentUtils.checkString(firstPerson.nickName) ?? TencentUtils.checkString(firstPerson.userID);
}
}
}
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_SHUT_UP_ALL:
s = TIM_t("全员禁言状态");
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_RECEIVE_MESSAGE_OPT:
s = TIM_t("消息接收方式");
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_GROUP_ADD_OPT:
s = TIM_t("加群方式");
break;
}
final String option8 = s;
if (value != null && value.isNotEmpty) {
return TIM_t_para("{{option8}}为 ", "$option8为 ")(option8: option8) + value;
} else {
return option8;
}
}
static String? _getOpUserNick(V2TimGroupMemberInfo? opUser) {
if (opUser == null) {
return "";
}
return TencentUtils.checkString(opUser.friendRemark) ??
TencentUtils.checkString(opUser.nickName) ??
TencentUtils.checkString(opUser.userID);
}
static String? _getMemberNickName(V2TimGroupMemberInfo e) {
final friendRemark = e.friendRemark;
final nameCard = e.nameCard;
final nickName = e.nickName;
final userID = e.userID;
if (friendRemark != null && friendRemark != "") {
return friendRemark;
} else if (nameCard != null && nameCard != "") {
return nameCard;
} else if (nickName != null && nickName != "") {
return nickName;
} else {
return userID;
}
}
static Future<String> groupTipsMessageAbstract(
V2TimGroupTipsElem groupTipsElem, List<V2TimGroupMemberFullInfo?> groupMemberList) async {
String displayMessage;
final operationType = groupTipsElem.type;
final operationMember = groupTipsElem.opMember;
final memberList = groupTipsElem.memberList;
final opUserNickName = _getOpUserNick(operationMember);
switch (operationType) {
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE:
final String? option7 = opUserNickName ?? "";
final groupChangeInfoList = groupTipsElem.groupChangeInfoList ?? [];
String changedInfoString = "";
bool changedValue = false;
for (V2TimGroupChangeInfo? element in groupChangeInfoList) {
final newText = await _getGroupChangeType(element!, groupMemberList);
changedInfoString += (changedInfoString.isEmpty ? "" : " / ") + newText;
changedValue = element!.boolValue ?? false;
}
if (changedInfoString.isEmpty) {
changedInfoString = TIM_t("群资料");
}
if (changedInfoString == TIM_t("全员禁言状态")) {
changedInfoString = TIM_t("全员禁言");
displayMessage = changedValue == false
? TIM_t_para("{{option7}} 取消", "$option7 取消")(option7: option7) + changedInfoString
: TIM_t_para("{{option7}} 开启", "$option7 开启")(option7: option7) + changedInfoString;
} else {
displayMessage = TIM_t_para("{{option7}}修改", "$option7修改")(option7: option7) + changedInfoString;
}
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_QUIT:
final String? option6 = opUserNickName ?? "";
displayMessage = TIM_t_para("{{option6}}退出群聊", "$option6退出群聊")(option6: option6);
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE:
final option5 = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
final inviteUser = _getOpUserNick(operationMember);
displayMessage = '$inviteUser' + TIM_t_para("邀请{{option5}}加入群组", "邀请$option5加入群组")(option5: option5);
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED:
final option4 = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
final kickUser = _getOpUserNick(operationMember);
displayMessage = '$kickUser' + TIM_t_para("将{{option4}}踢出群组", "将$option4踢出群组")(option4: option4);
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_JOIN:
final option3 = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
displayMessage = TIM_t_para("用户{{option3}}加入了群聊", "用户$option3加入了群聊")(option3: option3);
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_MEMBER_INFO_CHANGE:
displayMessage = groupTipsElem.memberList!.map((e) {
final changedMember =
groupTipsElem.memberChangeInfoList!.firstWhere((element) => element!.userID == e!.userID);
final isMute = changedMember!.muteTime != 0;
final option2 = _getMemberNickName(e!);
final displayMessage = isMute ? TIM_t("禁言") : TIM_t("解除禁言");
return TIM_t_para("{{option2}} 被", "$option2 被")(option2: option2) + displayMessage;
}).join("、");
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_SET_ADMIN:
final adminMember = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
final opMember = _getOpUserNick(operationMember);
final option1 = adminMember;
displayMessage = '$opMember' + TIM_t_para("将 {{option1}} 设置为管理员", "将 $option1 设置为管理员")(option1: option1);
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_CANCEL_ADMIN:
final adminMember = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
final opMember = _getOpUserNick(operationMember);
final option1 = adminMember;
displayMessage = '$opMember' + TIM_t_para("将 {{option1}} 取消管理员", "将 $option1 取消管理员")(option1: option1);
break;
default:
final String option2 = operationType.toString();
displayMessage = TIM_t_para("系统消息 {{option2}}", "系统消息 $option2")(option2: option2);
break;
}
return displayMessage;
}
static String formatVideoTime(int time) {
List<int> times = [];
if (time <= 0) return '0:01';
if (time >= TimeConst.DAY_SEC) return '1d+';
for (int idx = 0; idx < TimeConst.SEC_SERIES.length; idx++) {
int sec = TimeConst.SEC_SERIES[idx];
if (time >= sec) {
times.add((time / sec).floor());
time = time % sec;
} else if (idx > 0) {
times.add(0);
}
}
times.add(time);
String formatTime = times[0].toString();
for (int idx = 1; idx < times.length; idx++) {
if (times[idx] < 10) {
formatTime += ':0${times[idx].toString()}';
} else {
formatTime += ':${times[idx].toString()}';
}
}
return formatTime;
}
static String handleCustomMessageString(V2TimMessage message) {
return TIM_t("消息");
}
static Widget wrapMessageTips(Widget child, TUITheme? theme) {
return Container(margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 30), child: child);
}
static String getAbstractMessageAsync(V2TimMessage message, List<V2TimGroupMemberFullInfo?> groupMemberList) {
final msgType = message.elemType;
switch (msgType) {
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
return handleCustomMessageString(message);
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
return TIM_t("[语音]");
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
return message.textElem!.text as String;
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
return TIM_t("[表情]");
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
final String? option2 = message.fileElem!.fileName ?? "";
return TIM_t_para("[文件] {{option2}}", "[文件] $option2")(option2: option2);
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
return TIM_t("群提示");
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
return TIM_t("[图片]");
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
return TIM_t("[视频]");
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
return TIM_t("[位置]");
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
return TIM_t("[聊天记录]");
default:
return TIM_t("未知消息");
}
}
static V2TimImage? getImageFromImgList(List<V2TimImage?>? list, List<String> order) {
V2TimImage? img;
try {
for (String type in order) {
img =
list?.firstWhere((e) => e?.type == HistoryMessageDartConstant.V2_TIM_IMAGE_TYPES[type], orElse: () => null);
}
} catch (e) {
outputLogger.i('getImageFromImgList error ${e.toString()}');
}
return img;
}
static String getDisplayName(V2TimMessage message) {
final friendRemark = message.friendRemark ?? "";
final nameCard = message.nameCard ?? "";
final nickName = message.nickName ?? "";
final sender = message.sender ?? "";
final displayName = friendRemark.isNotEmpty
? friendRemark
: nameCard.isNotEmpty
? nameCard
: nickName.isNotEmpty
? nickName
: sender;
return displayName.toString();
}
static Future<V2TimValueCallback<V2TimMessage>?> handleMessageError(
Future<V2TimValueCallback<V2TimMessage>?> fun, BuildContext context) async {
final res = await fun;
return handleMessageErrorCode(res, context);
}
static V2TimValueCallback<V2TimMessage>? handleMessageErrorCode(
V2TimValueCallback<V2TimMessage>? sendMsgRes, BuildContext context) {
if (sendMsgRes == null) return null;
return sendMsgRes;
}
}
+45
View File
@@ -0,0 +1,45 @@
import 'dart:async';
class OptimizeUtils {
static throttle(Function func, int milliseconds) {
bool enable = true;
return (val) {
if (enable == true) {
enable = false;
Future.delayed(Duration(milliseconds: milliseconds), () {
enable = true;
func(val);
});
}
};
}
static multiThrottle(Function func, int milliseconds) {
bool enable = true;
return (val1, val2) {
if (enable == true) {
enable = false;
Future.delayed(Duration(milliseconds: milliseconds), () {
enable = true;
func(val1, val2);
});
}
};
}
static debounce(
Function func, [
Duration delay = const Duration(milliseconds: 100),
]) {
Timer? timer;
return (val) {
if (timer != null) {
timer?.cancel();
}
timer = Timer(delay, () {
func(val);
});
};
}
}
+366
View File
@@ -0,0 +1,366 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
class PermissionRequestInfo extends StatefulWidget {
final Function removeOverLay;
final int permissionType;
final String appName;
const PermissionRequestInfo(
{Key? key,
required this.removeOverLay,
required this.permissionType,
required this.appName})
: super(key: key);
@override
State<StatefulWidget> createState() => _PermissionRequestInfo();
}
class _PermissionRequestInfo extends TIMUIKitState<PermissionRequestInfo>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
widget.removeOverLay();
super.dispose();
WidgetsBinding.instance.removeObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
widget.removeOverLay();
}
}
@override
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
final theme = value.theme;
final permission = {
1: {
"name": TIM_t("相机"),
"icon": "images/chat_permission_icon_camera.png",
"text": TIM_t("为方便您将所拍摄的照片或视频发送给朋友,以及进行视频通话,请允许我们访问摄像头进行拍摄照片和视频。")
},
7: {
"name": TIM_t("麦克风"),
"icon": "images/chat_permission_icon_mic.png",
"text": TIM_t("为方便您发送语音消息、拍摄视频以及音视频通话,请允许我们使用麦克风进行录音。")
},
9: {
"name": TIM_t("相册"),
"icon": "images/chat_permission_icon_file.png",
"text": TIM_t("为方便您查看和选择相册里的图片视频发送给朋友,以及保存内容到设备,请允许我们访问您设备上的照片、媒体内容。")
},
15: {
"name": TIM_t("存储"),
"icon": "images/chat_permission_icon_file.png",
"text": TIM_t("为方便您查看和选择相册里的图片视频发送给朋友,以及保存内容到设备,请允许我们访问您设备上的照片、媒体内容。")
},
32: {
"name": TIM_t("相册"),
"icon": "images/chat_permission_icon_file.png",
"text": TIM_t("为方便您查看和选择相册里的图片视频发送给朋友,以及保存内容到设备,请允许我们访问您设备上的照片、媒体内容。")
},
}[widget.permissionType];
final option2 = permission?["name"] ?? "";
return Stack(
children: [
Positioned(
child: SafeArea(
child: Opacity(
opacity: 0.7,
child: Container(
color: theme.black,
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 50,
),
SizedBox(
height: 50,
width: 50,
child: Image.asset(
permission?["icon"] ?? "",
package: "tencent_cloud_chat_uikit",
),
),
const SizedBox(
height: 10,
),
Text(
TIM_t(widget.appName) +
TIM_t_para(" 申请获取{{option2}}", " 申请获取$option2")(
option2: option2) +
TIM_t("权限"),
style: TextStyle(color: theme.white, fontSize: 18),
),
const SizedBox(
height: 20,
),
Text(
permission?["text"] ?? "",
style: TextStyle(color: theme.white, fontSize: 16),
)
],
),
),
),
),
left: 0,
right: 0,
bottom: 0,
top: 0,
)
],
);
}
}
class Permissions {
static OverlayEntry? _entry;
static List<String> _names(BuildContext context) {
return <String>[
TIM_t("日历"),
TIM_t("相机"),
TIM_t("联系人"),
TIM_t("位置"),
'locationAlways',
'locationWhenInUse',
'mediaLibrary',
TIM_t("麦克风"),
'phone',
TIM_t("照片"),
TIM_t("相册写入"),
'reminders',
'sensors',
'sms',
'speech',
TIM_t("文件"),
'ignoreBatteryOptimizations',
'notification',
'access_media_location',
'activity_recognition',
'unknown',
'bluetooth',
'manageExternalStorage',
'systemAlertWindow',
'requestInstallPackages',
'appTrackingTransparency',
'criticalAlerts',
'accessNotificationPolicy',
'bluetoothScan',
'bluetoothAdvertise',
'bluetoothConnect',
'nearbyWifiDevices',
TIM_t("视频"),
'audio',
'scheduleExactAlarm'
];
}
static String _permissionText(
BuildContext context, String appName, int value) {
final _prefix = TIM_t("需要授予");
final _postfixList = <String>[
TIM_t("日历"),
TIM_t(" 相机权限,以正常使用拍摄图片视频、视频通话等功能。"),
TIM_t("联系人"),
TIM_t("位置"),
'locationAlways',
'locationWhenInUse',
'mediaLibrary',
TIM_t(" 麦克风权限,以正常使用发送语音消息、拍摄视频、音视频通话等功能。"),
'phone',
TIM_t(" 访问照片权限,以正常使用发送图片、视频等功能。"),
TIM_t(" 访问相册写入权限,以正常使用存储图片、视频等功能。"),
'reminders',
'sensors',
'sms',
'speech',
TIM_t(" 文件读写权限,以正常使用在聊天功能中的图片查看、选择能力和发送文件的能力。"),
'ignoreBatteryOptimizations',
'notification',
'access_media_location',
'activity_recognition',
'unknown',
'bluetooth',
'manageExternalStorage',
'systemAlertWindow',
'requestInstallPackages',
'appTrackingTransparency',
'criticalAlerts',
'accessNotificationPolicy',
'bluetoothScan',
'bluetoothAdvertise',
'bluetoothConnect',
'nearbyWifiDevices',
TIM_t(" 访问相册中视频权限,以正常使用发送视频等功能。"),
'audio',
'scheduleExactAlarm'
];
return _prefix + appName + _postfixList[value];
}
static Future<bool> checkPermission(
BuildContext context,
int value, [
TUITheme? theme,
bool isShowPermissionPage = true,
]) async {
final status = await Permission.byValue(value).status;
if (status.isGranted || status.isLimited) {
return true;
}
final bool? shouldRequestPermission = await showPermissionConfirmDialog(
context,
value,
theme,
isShowPermissionPage,
);
if (shouldRequestPermission != null && shouldRequestPermission) {
final isGranted = await Permission.byValue(value).request().isGranted;
_entry?.remove();
_entry = null;
return isGranted;
}
return shouldRequestPermission ?? false;
}
static Future<bool> checkPermissionSetBefore(int value) async {
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
SharedPreferences prefs = await _prefs;
final _hasPermissionSetBefore = prefs.getBool("permission$value");
return _hasPermissionSetBefore ?? false;
}
static Future<bool> setLocalPermission(int value) async {
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
SharedPreferences prefs = await _prefs;
return await prefs.setBool("permission$value", true);
}
static showPermissionRequestInfoDialog(BuildContext context, value) async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String appName = packageInfo.appName;
final entry = OverlayEntry(builder: (context) {
return PermissionRequestInfo(
appName: appName,
removeOverLay: () {
_entry?.remove();
_entry = null;
},
permissionType: value,
);
});
_entry = entry;
Overlay.of(context).insert(entry);
}
static Future<bool?> showPermissionConfirmDialog(BuildContext context, value,
[TUITheme? theme, bool isShowPermissionPage = true]) async {
final platformUtils = PlatformUtils();
// 第一次直接走系统文案
if (!await checkPermissionSetBefore(value)) {
await setLocalPermission(value);
if (platformUtils.isAndroid && isShowPermissionPage) {
showPermissionRequestInfoDialog(context, value);
}
return true;
}
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String appName = packageInfo.appName;
final option2 = _names(context)[value];
final permissionText = _permissionText(context, appName, value);
void closeDialog() {
Navigator.of(context).pop(false);
}
void getPermission() async {
Navigator.of(context).pop(false);
openAppSettings();
}
return showDialog<bool>(
context: context,
builder: (context) {
return platformUtils.isIOS
? CupertinoAlertDialog(
title: Text("“$appName”" +
TIM_t_para(" 想访问您的{{option2}}", " 想访问您的$option2")(
option2: option2)),
content: Text(permissionText),
actions: <Widget>[
CupertinoDialogAction(
child: Text(TIM_t("以后再说")),
onPressed: closeDialog, // 关闭对话框
),
CupertinoDialogAction(
child: Text(TIM_t("去开启")),
onPressed: getPermission,
),
],
)
: AlertDialog(
content: Text(permissionText),
actions: <Widget>[
const Divider(),
SizedBox(
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: TextButton(
child: Text(TIM_t("以后再说"),
style: TextStyle(
color: theme?.black ?? Colors.black,
)),
onPressed: closeDialog, // 关闭对话框
),
),
const VerticalDivider(),
Expanded(
child: TextButton(
child: Text(TIM_t("去开启"),
style: TextStyle(
color: theme?.black ?? Colors.black,
)),
onPressed: getPermission,
),
)
],
),
)
],
);
},
);
}
}
+61
View File
@@ -0,0 +1,61 @@
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
class PlatformUtils {
PlatformUtils._internal();
static late bool _isAndroid;
static late bool _isIos;
static late bool _isMobile;
static late bool _isWeb;
static late bool _isWindows;
static late bool _isMacOS;
static late bool _isLinux;
static late bool _isDesktop;
static bool _isInstantiation = false;
factory PlatformUtils() {
if (!_isInstantiation) {
_isAndroid = !kIsWeb && Platform.isAndroid;
_isIos = !kIsWeb && Platform.isIOS;
_isMobile = _isAndroid || _isIos;
_isWindows = !kIsWeb && Platform.isWindows;
_isMacOS = !kIsWeb && Platform.isMacOS;
_isLinux = !kIsWeb && Platform.isLinux;
_isDesktop = _isMacOS || _isWindows || _isLinux;
_isWeb = kIsWeb;
_isInstantiation = true;
}
return _instance;
}
static late final PlatformUtils _instance = PlatformUtils._internal();
get isAndroid {
return _isAndroid;
}
get isWeb {
return _isWeb;
}
get isIOS {
return _isIos;
}
get isWindows {
return _isWindows;
}
get isMacOS {
return _isMacOS;
}
get isMobile {
return _isMobile;
}
bool get isDesktop => _isDesktop;
bool get isLinux => _isLinux;
}
+24
View File
@@ -0,0 +1,24 @@
import 'package:flutter/cupertino.dart';
class FadeRoute extends PageRouteBuilder {
final Widget page;
FadeRoute({required this.page})
: super(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) =>
page,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) =>
FadeTransition(
opacity: animation,
child: child,
),
);
}
+105
View File
@@ -0,0 +1,105 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
import 'package:uuid/uuid.dart';
class ScreenshotHelper {
static Future<String?> captureScreen() async {
await requestScreenRecordingPermission();
String directory;
if(PlatformUtils().isWindows){
final String documentsDirectoryPath =
"${Platform.environment['USERPROFILE']}";
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String pkgName = packageInfo.packageName;
directory = p.join(documentsDirectoryPath, "Documents", ".TencentCloudChat",
pkgName, "screenshots");
}else{
final dic = await getApplicationSupportDirectory();
directory = dic.path;
}
const uuid = Uuid();
final fileName = 'screenshot_${uuid.v4()}.png';
final filePath = '$directory/$fileName';
if (Platform.isMacOS) {
// 在macOS平台上使用screencapture工具
final result = await Process.run(
'screencapture',
['-i', '-s', '-o', filePath],
);
if (result.exitCode == 0) {
return filePath;
} else {
return null;
}
} else if (Platform.isWindows) {
// 在Windows平台上使用snippingtool工具
final result = await Process.run(
'snippingtool',
['/clip', filePath],
);
if (result.exitCode == 0) {
return filePath;
} else {
return null;
}
} else {
// 不支持的平台
return null;
}
}
static Future<bool> requestScreenRecordingPermission() async {
if (Platform.isMacOS) {
final result = await Process.run(
'sh', ['-c', 'echo ${Platform.environment['USER']}']);
final username = result.stdout.toString().trim();
const script =
'tell application "System Events" to return exists (processes where name is "ControlCenter")';
final process = await Process.run('osascript', ['-e', script]);
final isControlCenterRunning = process.stdout.toString().trim() == 'true';
if (!isControlCenterRunning) {
await Process.run('open', ['-a', 'ControlCenter']);
await Future.delayed(const Duration(seconds: 1));
}
final script2 = 'tell application "ControlCenter" to activate\n'
'tell application "System Events"\n'
' tell process "ControlCenter"\n'
' click menu item "Screen Recording" of menu "File" of menu bar 1\n'
' delay 0.5\n'
' keystroke "$username" & return\n'
' end tell\n'
'end tell\n';
await Process.run('osascript', ['-e', script2]);
await Future.delayed(const Duration(seconds: 1));
final isScreenRecordingEnabled = await SystemChannels.platform
.invokeMethod<bool>('isScreenRecordingEnabled');
return isScreenRecordingEnabled ?? false;
} else {
return true;
}
}
static Future<Size> getImageSize(String imagePath) async {
final bytes = await File(imagePath).readAsBytes();
final completer = Completer<Size>();
final imageStream =
Image.memory(bytes).image.resolve(ImageConfiguration.empty);
imageStream.addListener(ImageStreamListener((imageInfo, _) {
completer.complete(Size(
imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble()));
}));
return completer.future;
}
}
+63
View File
@@ -0,0 +1,63 @@
// ignore_for_file: constant_identifier_names
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
enum DeviceType { Desktop, Mobile }
class FormFactor {
static double desktop = 900;
static double handset = 300;
}
class TUIKitScreenUtils {
static DeviceType? deviceType;
/// Although specifying the `BuildContext` is optional, providing it can prevent layout issues when this widget renders immediately after the app launch.
/// If this widget needs to be used at the moment the app launches, it's recommended to provide the `BuildContext` here.
static DeviceType getFormFactor([BuildContext? context]) {
if (deviceType != null) return deviceType!;
if (PlatformUtils().isWeb) {
final win = WidgetsBinding.instance.platformDispatcher.views.first;
final size = win.physicalSize;
final screenWidth = size.width / win.devicePixelRatio;
final screenHeight = size.height / win.devicePixelRatio;
final diagonalInInches =
sqrt(pow(screenWidth, 2) + pow(screenHeight, 2)) / 96.0;
deviceType = diagonalInInches < 11.0 ? DeviceType.Mobile : DeviceType.Desktop;
return deviceType ?? DeviceType.Mobile;
}else{
if(context != null){
double deviceWidth = MediaQuery.of(context).size.width;
double deviceHeight = MediaQuery.of(context).size.height;
if (deviceWidth > FormFactor.desktop || deviceWidth > deviceHeight * 1.1) {
deviceType = DeviceType.Desktop;
} else if (deviceWidth > FormFactor.handset) {
deviceType = DeviceType.Mobile;
}
return deviceType ?? DeviceType.Mobile;
}else{
return DeviceType.Mobile;
}
}
}
static Widget getDeviceWidget({
/// Although specifying the `BuildContext` is optional, providing it can prevent layout issues when this widget renders immediately after the app launch.
/// If this widget needs to be used at the moment the app launches, it's recommended to provide the `BuildContext` here.
BuildContext? context,
required Widget defaultWidget,
Widget? desktopWidget,
Widget? mobileWidget,
}) {
deviceType ??= getFormFactor(context);
if (deviceType == DeviceType.Desktop) return desktopWidget ?? defaultWidget;
return mobileWidget ?? defaultWidget;
}
}
+69
View File
@@ -0,0 +1,69 @@
import 'dart:async';
import 'package:flutter_plugin_record_plus/const/play_state.dart';
import 'package:flutter_plugin_record_plus/const/response.dart';
import 'package:flutter_plugin_record_plus/index.dart';
import 'package:just_audio/just_audio.dart';
import 'package:tencent_cloud_chat_uikit/import_proxy/import_proxy.dart';
typedef PlayStateListener = void Function(PlayState playState);
typedef SoundInterruptListener = void Function();
typedef ResponseListener = void Function(RecordResponse recordResponse);
class SoundPlayer {
final ImportProxy importProxy = ImportProxy();
static final FlutterPluginRecord _recorder = FlutterPluginRecord();
static SoundInterruptListener? _soundInterruptListener;
static bool isInit = false;
static final AudioPlayer _audioPlayer = AudioPlayer();
static initSoundPlayer() {
if (!isInit) {
_recorder.init();
// AudioPlayer.global.setGlobalAudioContext(const AudioContext());
isInit = true;
}
}
static Future<void> play({required String url}) async {
_audioPlayer.stop();
if (_soundInterruptListener != null) {
_soundInterruptListener!();
}
await _audioPlayer.setUrl(url);
await _audioPlayer.play();
}
static stop() {
_audioPlayer.stop();
}
static dispose() {
_audioPlayer.dispose();
_recorder.dispose();
}
static StreamSubscription<PlayerState> playStateListener({required void Function(PlayerState)? listener}) => _audioPlayer.playerStateStream.listen(listener);
static setSoundInterruptListener(SoundInterruptListener listener) {
_soundInterruptListener = listener;
}
static removeSoundInterruptListener() {
_soundInterruptListener = null;
}
static StreamSubscription<RecordResponse> responseListener(ResponseListener listener) => _recorder.response.listen(listener);
static StreamSubscription<RecordResponse> responseFromAmplitudeListener(ResponseListener listener) => _recorder.responseFromAmplitude.listen(listener);
static startRecord() {
_recorder.start();
}
static stopRecord() {
_recorder.stop();
}
}
+111
View File
@@ -0,0 +1,111 @@
// ignore_for_file: prefer_typing_uninitialized_variables
import 'package:intl/intl.dart';
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
class TimeAgo {
List<String> dayMap() {
return [
TIM_t("昨天"),
TIM_t("前天"),
];
}
List<String> weekdayMap() {
return ['', TIM_t("星期一"), TIM_t("星期二"), TIM_t("星期三"), TIM_t("星期四"), TIM_t("星期五"), TIM_t("星期六"), TIM_t("星期天")];
}
String getYearMonthDate(DateTime dateTime) {
String month = dateTime.month.toString();
String date = dateTime.day.toString();
return dateTime.year.toString() + '/' + (month.length == 1 ? '0' : '') + month + '/' + (date.length == 1 ? '0' : '') + date;
}
String getMonthDate(DateTime dateTime) {
String month = dateTime.month.toString();
String date = dateTime.day.toString();
return (month.length == 1 ? '0' : '') + month + '/' + (date.length == 1 ? '0' : '') + date;
}
static String getMonth(DateTime dateTime) {
String month = dateTime.month.toString();
return (month.length == 1 ? '0' : '') + month;
}
static String getDay(DateTime dateTime) {
String day = dateTime.day.toString();
return (day.length == 1 ? '0' : '') + day;
}
String? getTimeStringForChat(int timeStamp) {
final DateTime date = DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000);
final DateTime epochLimit = DateTime.utc(1971);
if (date.isBefore(epochLimit)) {
return null;
}
final Duration duration = DateTime.now().difference(date);
final int diffDays = duration.inDays + (duration.inMinutes > DateTime.now().difference(DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day)).inMinutes ? 1 : 0);
final int diffMinutes = duration.inMinutes;
var res;
// 一个礼拜之内
if (diffDays > 0 && diffDays < 7) {
if (diffDays <= 2) {
res = dayMap()[diffDays - 1];
} else {
res = weekdayMap()[date.weekday];
}
} else if (diffDays >= 7) {
//当年内
if (date.year == DateTime.now().year) {
res = getMonthDate(date);
} else {
res = getYearMonthDate(date);
}
} else {
if (diffMinutes > 1) {
if (diffMinutes < 60) {
final String option2 = diffMinutes.toString();
res = TIM_t_para("{{option2}} 分钟前", "$option2 分钟前")(option2: option2);
} else {
res = "${date.hour}:${date.minute < 10 ? "0" + date.minute.toString() : date.minute}";
// res = "$prefix $timeStr";
}
} else {
res = TIM_t("现在");
}
}
return res;
}
String getTimeForMessage(int timeStamp) {
var nowTime = DateTime.now();
nowTime = DateTime(nowTime.year, nowTime.month, nowTime.day);
var ftime = DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000);
// var preFix = ftime.hour >= 12 ? TIM_t("下午") : TIM_t("上午");
final timeStr = DateFormat('HH:mm').format(ftime); // Use 'HH:mm' for 24-hour format
// 一年外 年月日 + 时间 (24小时制)
if (nowTime.year != ftime.year) {
return '${DateFormat('yyyy-MM-dd').format(ftime)} $timeStr';
}
// 一年内一周外 月日 + 时间 (24小时制)
if (ftime.isBefore(nowTime.subtract(const Duration(days: 6)))) {
return '${DateFormat('MM-dd').format(ftime)} $timeStr';
}
// 一周内一天外 星期 + 时间 (24小时制)
if (ftime.isBefore(nowTime.subtract(const Duration(days: 1)))) {
return '${weekdayMap()[ftime.weekday]} $timeStr';
}
// 昨日 昨天 + 时间 (24小时制)
if (nowTime.day != ftime.day) {
String option2 = timeStr;
return TIM_t_para("昨天 {{option2}}", "昨天 $option2")(option2: option2);
}
// 同年月日 时间 (24小时制)
return timeStr;
}
}