init
This commit is contained in:
@@ -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 "*/*";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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] ?? "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user