feat(mobile-next): 导入官方 openim-flutter-demo 固定基线 3.8.3-patch.3 并完成双构建
- 上游: github.com/OpenIMSDK/openim-flutter-demo tag 3.8.3-patch.3 commit b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f - 并行目录 mobile-next/ 原样导入(含 LICENSE),不覆盖现网 mobile/ - 锁定 Flutter 3.24.5 / Dart 3.5.4 / JDK 17 / Gradle 7.6.3 / AGP 7.3.1 - 实测 Android debug 与 release 构建均成功(见 docs/mobile-next-baseline-import.md) - 本提交可单独回退:git revert 1db1229(重写前) Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
library openim_common;
|
||||
|
||||
export 'src/apis.dart';
|
||||
export 'src/bridge/package_bridge.dart';
|
||||
export 'src/config.dart';
|
||||
export 'src/controller/cache_controller.dart';
|
||||
export 'src/controller/download_controller.dart';
|
||||
export 'src/extension/custom_ext.dart';
|
||||
export 'src/extension/message_manager.dart';
|
||||
export 'src/models/api_resp.dart';
|
||||
export 'src/models/call_records.dart';
|
||||
export 'src/models/emoji_info.dart';
|
||||
export 'src/models/group_info.dart';
|
||||
export 'src/models/group_member_info.dart';
|
||||
export 'src/models/login_certificate.dart';
|
||||
export 'src/models/online_status.dart';
|
||||
export 'src/models/uni_mp_info.dart';
|
||||
export 'src/models/upgrade_info.dart';
|
||||
export 'src/models/user_full_info.dart';
|
||||
export 'src/models/user_info.dart';
|
||||
export 'src/models/define.dart';
|
||||
export 'src/models/signaling_info.dart';
|
||||
export 'src/res/images.dart';
|
||||
export 'src/res/strings.dart';
|
||||
export 'src/res/styles.dart';
|
||||
export 'src/urls.dart';
|
||||
export 'src/utils/data_sp.dart';
|
||||
export 'src/utils/http_util.dart';
|
||||
export 'src/utils/image_util.dart';
|
||||
export 'src/utils/logger.dart';
|
||||
export 'src/utils/permissions.dart';
|
||||
export 'src/utils/sp_util.dart';
|
||||
export 'src/utils/utils.dart';
|
||||
export 'src/utils/voice_record.dart';
|
||||
export 'src/utils/api_service.dart';
|
||||
export 'src/widgets/avatar_view.dart';
|
||||
export 'src/widgets/azlist_view.dart';
|
||||
export 'src/widgets/bottom_bar.dart';
|
||||
export 'src/widgets/bottom_sheet_view.dart';
|
||||
export 'src/widgets/button.dart';
|
||||
export 'src/widgets/chat/chat_bubble.dart';
|
||||
export 'src/widgets/chat/chat_call_item_view.dart';
|
||||
export 'src/widgets/chat/chat_carte_view.dart';
|
||||
export 'src/widgets/chat/chat_custom_emoji_view.dart';
|
||||
export 'src/widgets/chat/chat_delayed_status_view.dart';
|
||||
export 'src/widgets/chat/chat_disable_input_box.dart';
|
||||
export 'src/widgets/chat/chat_emoji_view.dart';
|
||||
export 'src/widgets/chat/chat_face_preview.dart';
|
||||
export 'src/widgets/chat/chat_file_icon_view.dart';
|
||||
export 'src/widgets/chat/chat_file_view.dart';
|
||||
export 'src/widgets/chat/chat_friend_relationship_hint_view.dart';
|
||||
export 'src/widgets/chat/chat_hint_text_view.dart';
|
||||
export 'src/widgets/chat/chat_image_view.dart';
|
||||
export 'src/widgets/chat/chat_input_box.dart';
|
||||
export 'src/widgets/chat/chat_item_container.dart';
|
||||
export 'src/widgets/chat/chat_item_view.dart';
|
||||
export 'src/widgets/chat/chat_listview.dart';
|
||||
export 'src/widgets/chat/chat_location_view.dart';
|
||||
export 'src/widgets/chat/chat_nickname_view.dart';
|
||||
export 'src/widgets/chat/chat_picture_preview.dart';
|
||||
export 'src/widgets/chat/chat_picture_view.dart';
|
||||
export 'src/widgets/chat/chat_pop_menu.dart';
|
||||
export 'src/widgets/chat/chat_radio.dart';
|
||||
export 'src/widgets/chat/chat_read_tag.dart';
|
||||
export 'src/widgets/chat/chat_revoke_view.dart';
|
||||
export 'src/widgets/chat/chat_send_failed_view.dart';
|
||||
export 'src/widgets/chat/chat_text.dart';
|
||||
export 'src/widgets/chat/chat_text_field.dart';
|
||||
export 'src/widgets/chat/chat_timeline_view.dart';
|
||||
export 'src/widgets/chat/chat_toolbox.dart';
|
||||
export 'src/widgets/chat/chat_video_player_view.dart';
|
||||
export 'src/widgets/chat/chat_video_view.dart';
|
||||
export 'src/widgets/chat/chat_voice_read_status_view.dart';
|
||||
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';
|
||||
export 'src/widgets/dialog.dart';
|
||||
export 'src/widgets/expanded_text.dart';
|
||||
export 'src/widgets/gesture_x_detector.dart';
|
||||
export 'src/widgets/gesture_zoom_box.dart';
|
||||
export 'src/widgets/h5_container.dart';
|
||||
export 'src/widgets/input_box.dart';
|
||||
export 'src/widgets/loading_view.dart';
|
||||
export 'src/widgets/map_view.dart';
|
||||
export 'src/widgets/material_controls.dart';
|
||||
export 'src/widgets/photo_browser_bottom_bar.dart';
|
||||
export 'src/widgets/pop_button.dart';
|
||||
export 'src/widgets/qr_view.dart';
|
||||
export 'src/widgets/rich_text_input_box.dart';
|
||||
export 'src/widgets/search_box.dart';
|
||||
export 'src/widgets/slide_hero.dart';
|
||||
export 'src/widgets/sync_status_view.dart';
|
||||
export 'src/widgets/tab_bar.dart';
|
||||
export 'src/widgets/text_ellipsis_middle_view.dart';
|
||||
export 'src/widgets/text_view.dart';
|
||||
export 'src/widgets/timing_view.dart';
|
||||
export 'src/widgets/title_bar.dart';
|
||||
export 'src/widgets/touch_close_keyboard.dart';
|
||||
export 'src/widgets/transparent_route.dart';
|
||||
export 'src/widgets/unread_count_view.dart';
|
||||
export 'src/widgets/verify_code_send_button.dart';
|
||||
export 'src/widgets/views.dart';
|
||||
export 'src/widgets/overlay_widget.dart';
|
||||
export 'src/widgets/photo_browser.dart';
|
||||
export 'src/controller/push_controller.dart';
|
||||
@@ -0,0 +1,435 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:sprintf/sprintf.dart';
|
||||
|
||||
import 'utils/api_service.dart';
|
||||
|
||||
class Apis {
|
||||
static Options get imTokenOptions => Options(headers: {'token': DataSp.imToken});
|
||||
|
||||
static Options get chatTokenOptions => Options(headers: {'token': DataSp.chatToken});
|
||||
|
||||
static StreamController kickoffController = StreamController<int>.broadcast();
|
||||
|
||||
static void _kickoff(int? errCode) {
|
||||
if (errCode == 1501 || errCode == 1503 || errCode == 1504 || errCode == 1505) {
|
||||
kickoffController.sink.add(errCode);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoginCertificate> login({
|
||||
String? areaCode,
|
||||
String? phoneNumber,
|
||||
String? account,
|
||||
String? email,
|
||||
String? password,
|
||||
String? verificationCode,
|
||||
}) async {
|
||||
try {
|
||||
var data = await HttpUtil.post(Urls.login, data: {
|
||||
"areaCode": areaCode,
|
||||
'account': account,
|
||||
'phoneNumber': phoneNumber,
|
||||
'email': email,
|
||||
'password': null != password ? IMUtils.generateMD5(password) : null,
|
||||
'platform': IMUtils.getPlatform(),
|
||||
'verifyCode': verificationCode,
|
||||
});
|
||||
final cert = LoginCertificate.fromJson(data!);
|
||||
ApiService().setToken(cert.imToken);
|
||||
|
||||
return cert;
|
||||
} catch (e, _) {
|
||||
final t = e as (int, String?)?;
|
||||
|
||||
if (t == null) {
|
||||
Logger.print('e:$e');
|
||||
|
||||
return Future.error(e);
|
||||
}
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoginCertificate> register({
|
||||
required String nickname,
|
||||
required String password,
|
||||
String? faceURL,
|
||||
String? areaCode,
|
||||
String? phoneNumber,
|
||||
String? email,
|
||||
String? account,
|
||||
int birth = 0,
|
||||
int gender = 1,
|
||||
required String verificationCode,
|
||||
String? invitationCode,
|
||||
}) async {
|
||||
try {
|
||||
var data = await HttpUtil.post(Urls.register, data: {
|
||||
'deviceID': DataSp.getDeviceID(),
|
||||
'verifyCode': verificationCode,
|
||||
'platform': IMUtils.getPlatform(),
|
||||
'invitationCode': invitationCode,
|
||||
'autoLogin': true,
|
||||
'user': {
|
||||
"nickname": nickname,
|
||||
"faceURL": faceURL,
|
||||
'birth': birth,
|
||||
'gender': gender,
|
||||
'email': email,
|
||||
"areaCode": areaCode,
|
||||
'phoneNumber': phoneNumber,
|
||||
'account': account,
|
||||
'password': IMUtils.generateMD5(password),
|
||||
},
|
||||
});
|
||||
|
||||
final cert = LoginCertificate.fromJson(data!);
|
||||
ApiService().setToken(cert.imToken);
|
||||
|
||||
return cert;
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<dynamic> resetPassword({
|
||||
String? areaCode,
|
||||
String? phoneNumber,
|
||||
String? email,
|
||||
required String password,
|
||||
required String verificationCode,
|
||||
}) async {
|
||||
try {
|
||||
return HttpUtil.post(
|
||||
Urls.resetPwd,
|
||||
data: {
|
||||
"areaCode": areaCode,
|
||||
'phoneNumber': phoneNumber,
|
||||
'email': email,
|
||||
'password': IMUtils.generateMD5(password),
|
||||
'verifyCode': verificationCode,
|
||||
'platform': IMUtils.getPlatform(),
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> changePassword({
|
||||
required String userID,
|
||||
required String currentPassword,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
try {
|
||||
await HttpUtil.post(
|
||||
Urls.changePwd,
|
||||
data: {
|
||||
"userID": userID,
|
||||
'currentPassword': IMUtils.generateMD5(currentPassword),
|
||||
'newPassword': IMUtils.generateMD5(newPassword),
|
||||
'platform': IMUtils.getPlatform(),
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> changePasswordOfB({
|
||||
required String newPassword,
|
||||
}) async {
|
||||
try {
|
||||
await HttpUtil.post(
|
||||
Urls.resetPwd,
|
||||
data: {
|
||||
'password': IMUtils.generateMD5(newPassword),
|
||||
'platform': IMUtils.getPlatform(),
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<dynamic> updateUserInfo({
|
||||
required String userID,
|
||||
String? account,
|
||||
String? phoneNumber,
|
||||
String? areaCode,
|
||||
String? email,
|
||||
String? nickname,
|
||||
String? faceURL,
|
||||
int? gender,
|
||||
int? birth,
|
||||
int? level,
|
||||
int? allowAddFriend,
|
||||
int? allowBeep,
|
||||
int? allowVibration,
|
||||
}) async {
|
||||
try {
|
||||
Map<String, dynamic> param = {'userID': userID};
|
||||
void put(String key, dynamic value) {
|
||||
if (null != value) {
|
||||
param[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
put('account', account);
|
||||
put('phoneNumber', phoneNumber);
|
||||
put('areaCode', areaCode);
|
||||
put('email', email);
|
||||
put('nickname', nickname);
|
||||
put('faceURL', faceURL);
|
||||
put('gender', gender);
|
||||
put('gender', gender);
|
||||
put('level', level);
|
||||
put('birth', birth);
|
||||
put('allowAddFriend', allowAddFriend);
|
||||
put('allowBeep', allowBeep);
|
||||
put('allowVibration', allowVibration);
|
||||
|
||||
return HttpUtil.post(
|
||||
Urls.updateUserInfo,
|
||||
data: {
|
||||
...param,
|
||||
'platform': IMUtils.getPlatform(),
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<FriendInfo>> searchFriendInfo(
|
||||
String keyword, {
|
||||
int pageNumber = 1,
|
||||
int showNumber = 10,
|
||||
}) async {
|
||||
try {
|
||||
final data = await HttpUtil.post(
|
||||
Urls.searchFriendInfo,
|
||||
data: {
|
||||
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
|
||||
'keyword': keyword,
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
if (data['users'] is List) {
|
||||
return (data['users'] as List).map((e) => FriendInfo.fromJson(e)).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<UserFullInfo>?> getUserFullInfo({
|
||||
int pageNumber = 0,
|
||||
int showNumber = 10,
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
try {
|
||||
final data = await HttpUtil.post(
|
||||
Urls.getUsersFullInfo,
|
||||
data: {
|
||||
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
|
||||
'userIDs': userIDList,
|
||||
'platform': IMUtils.getPlatform(),
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
if (data['users'] is List) {
|
||||
return (data['users'] as List).map((e) => UserFullInfo.fromJson(e)).toList();
|
||||
}
|
||||
return null;
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<UserFullInfo>?> searchUserFullInfo({
|
||||
required String content,
|
||||
int pageNumber = 1,
|
||||
int showNumber = 10,
|
||||
}) async {
|
||||
try {
|
||||
final data = await HttpUtil.post(
|
||||
Urls.searchUserFullInfo,
|
||||
data: {
|
||||
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
|
||||
'keyword': content,
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
if (data['users'] is List) {
|
||||
return (data['users'] as List).map((e) => UserFullInfo.fromJson(e)).toList();
|
||||
}
|
||||
return null;
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static Future<UserFullInfo?> queryMyFullInfo() async {
|
||||
final list = await Apis.getUserFullInfo(
|
||||
userIDList: [OpenIM.iMManager.userID],
|
||||
);
|
||||
return list?.firstOrNull;
|
||||
}
|
||||
|
||||
static Future<bool> requestVerificationCode({
|
||||
String? areaCode,
|
||||
String? phoneNumber,
|
||||
String? email,
|
||||
required int usedFor,
|
||||
String? invitationCode,
|
||||
}) async {
|
||||
return HttpUtil.post(
|
||||
Urls.getVerificationCode,
|
||||
data: {
|
||||
"areaCode": areaCode,
|
||||
"phoneNumber": phoneNumber,
|
||||
"email": email,
|
||||
'usedFor': usedFor,
|
||||
'invitationCode': invitationCode
|
||||
},
|
||||
).then((value) {
|
||||
IMViews.showToast(StrRes.sentSuccessfully);
|
||||
return true;
|
||||
}).catchError((e, s) {
|
||||
Logger.print('e:$e s:$s');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<SignalingCertificate> getTokenForRTC(String roomID, String userID) async {
|
||||
return HttpUtil.post(
|
||||
Urls.getTokenForRTC,
|
||||
data: {
|
||||
"room": roomID,
|
||||
"identity": userID,
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
).then((value) {
|
||||
final signaling = SignalingCertificate.fromJson(value)..roomID = roomID;
|
||||
return signaling;
|
||||
}).catchError((e, s) {
|
||||
Logger.print('e:$e s:$s');
|
||||
});
|
||||
}
|
||||
|
||||
static Future<dynamic> checkVerificationCode({
|
||||
String? areaCode,
|
||||
String? phoneNumber,
|
||||
String? email,
|
||||
required String verificationCode,
|
||||
required int usedFor,
|
||||
String? invitationCode,
|
||||
}) {
|
||||
return HttpUtil.post(
|
||||
Urls.checkVerificationCode,
|
||||
data: {
|
||||
"phoneNumber": phoneNumber,
|
||||
"areaCode": areaCode,
|
||||
"email": email,
|
||||
"verifyCode": verificationCode,
|
||||
"usedFor": usedFor,
|
||||
'invitationCode': invitationCode
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<UpgradeInfoV2> checkUpgradeV2() {
|
||||
return dio.post<Map<String, dynamic>>(
|
||||
'https://www.pgyer.com/apiv2/app/check',
|
||||
options: Options(
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
),
|
||||
data: {
|
||||
'_api_key': '',
|
||||
'appKey': '',
|
||||
},
|
||||
).then((resp) {
|
||||
Map<String, dynamic> map = resp.data!;
|
||||
if (map['code'] == 0) {
|
||||
return UpgradeInfoV2.fromJson(map['data']);
|
||||
}
|
||||
return Future.error(map);
|
||||
});
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getClientConfig() async {
|
||||
return {'discoverPageURL': Config.discoverPageURL, 'allowSendMsgNotFriend': Config.allowSendMsgNotFriend};
|
||||
}
|
||||
|
||||
static void _catchError(Object e, StackTrace s, {bool forceBack = true}) {
|
||||
if (e is ApiException) {
|
||||
var msg = '${e.code}'.tr;
|
||||
if (msg.isEmpty || e.code.toString() == msg) {
|
||||
msg = e.message ?? 'Unkonw error';
|
||||
} else if (e.code == 1004) {
|
||||
msg = sprintf(msg, [StrRes.meeting]);
|
||||
}
|
||||
|
||||
IMViews.showToast(msg);
|
||||
|
||||
if ((e.code == 10010 || e.code == 10002) && forceBack) {
|
||||
DataSp.removeLoginCertificate();
|
||||
Get.offAllNamed('/login');
|
||||
}
|
||||
} else {
|
||||
IMViews.showToast(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PackageBridge {
|
||||
PackageBridge._();
|
||||
|
||||
static SelectContactsBridge? selectContactsBridge;
|
||||
static ViewUserProfileBridge? viewUserProfileBridge;
|
||||
static ScanBridge? scanBridge;
|
||||
static RTCBridge? rtcBridge;
|
||||
}
|
||||
|
||||
abstract class ScanBridge {
|
||||
scanOutUserID(String userID);
|
||||
|
||||
scanOutGroupID(String groupID);
|
||||
}
|
||||
|
||||
abstract class OrganizationMultiSelBridge {
|
||||
Widget get checkedConfirmView;
|
||||
|
||||
bool get isMultiModel;
|
||||
|
||||
bool isChecked(dynamic info);
|
||||
|
||||
bool isDefaultChecked(dynamic info);
|
||||
|
||||
Function()? onTap(dynamic info);
|
||||
|
||||
toggleChecked(dynamic info);
|
||||
|
||||
removeItem(dynamic info);
|
||||
|
||||
updateDefaultCheckedList(List<String> userIDList);
|
||||
}
|
||||
|
||||
abstract class ViewUserProfileBridge {
|
||||
viewUserProfile(
|
||||
String userID,
|
||||
String? nickname,
|
||||
String? faceURL, [
|
||||
String? groupID,
|
||||
]);
|
||||
}
|
||||
|
||||
abstract class SelectContactsBridge {
|
||||
Future<T?>? selectContacts<T>(
|
||||
int type, {
|
||||
List<String>? defaultCheckedIDList,
|
||||
List<dynamic>? checkedList,
|
||||
List<String>? excludeIDList,
|
||||
bool openSelectedSheet = false,
|
||||
String? groupID,
|
||||
String? ex,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class RTCBridge {
|
||||
bool get hasConnection;
|
||||
void dismiss();
|
||||
}
|
||||
|
||||
class GetTags {
|
||||
static final List<String> _chatTags = <String>[];
|
||||
static final List<String> _userProfileTags = <String>[];
|
||||
|
||||
static void createChatTag() {
|
||||
_chatTags.add('_${DateTime.now().millisecondsSinceEpoch}');
|
||||
}
|
||||
|
||||
static void createUserProfileTag() {
|
||||
_userProfileTags.add('_${DateTime.now().millisecondsSinceEpoch}');
|
||||
}
|
||||
|
||||
static void destroyChatTag() {
|
||||
_chatTags.removeLast();
|
||||
}
|
||||
|
||||
static void destroyUserProfileTag() {
|
||||
_userProfileTags.removeLast();
|
||||
}
|
||||
|
||||
static String? get chat => _chatTags.isNotEmpty ? _chatTags.last : null;
|
||||
|
||||
static String get userProfile => _userProfileTags.last;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:openim_common/src/utils/api_service.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class Config {
|
||||
static Future init(Function() runApp) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
try {
|
||||
final path = (await getApplicationDocumentsDirectory()).path;
|
||||
cachePath = '$path/';
|
||||
await DataSp.init();
|
||||
await Hive.initFlutter(path);
|
||||
MediaKit.ensureInitialized();
|
||||
HttpUtil.init();
|
||||
ApiService().setBaseUrl(serverIp);
|
||||
} catch (_) {}
|
||||
|
||||
runApp();
|
||||
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
|
||||
var brightness = Platform.isAndroid ? Brightness.dark : Brightness.light;
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarBrightness: brightness,
|
||||
statusBarIconBrightness: brightness,
|
||||
));
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
_appName = packageInfo.appName;
|
||||
}
|
||||
|
||||
static late String _appName;
|
||||
|
||||
static late String cachePath;
|
||||
static const uiW = 375.0;
|
||||
static const uiH = 812.0;
|
||||
|
||||
static const double textScaleFactor = 1.0;
|
||||
|
||||
static const discoverPageURL = 'https://docs.openim.io/';
|
||||
static const allowSendMsgNotFriend = '1';
|
||||
// amap key
|
||||
static const webKey = 'webKey';
|
||||
static const webServerKey = 'webServerKey';
|
||||
static const locationHost = 'http://location.your-domain';
|
||||
|
||||
static OfflinePushInfo get offlinePushInfo => OfflinePushInfo(
|
||||
title: _appName,
|
||||
desc: StrRes.offlineMessage,
|
||||
iOSBadgeCount: true,
|
||||
);
|
||||
|
||||
static const friendScheme = "io.openim.app/addFriend/";
|
||||
static const groupScheme = "io.openim.app/joinGroup/";
|
||||
|
||||
static const _host = "your-server-ip or your-domain";
|
||||
|
||||
static const _ipRegex = '((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)';
|
||||
|
||||
static bool get _isIP => RegExp(_ipRegex).hasMatch(_host);
|
||||
|
||||
static String get serverIp {
|
||||
String? ip;
|
||||
var server = DataSp.getServerConfig();
|
||||
if (null != server) {
|
||||
ip = server['serverIP'];
|
||||
}
|
||||
return ip ?? _host;
|
||||
}
|
||||
|
||||
static String get chatTokenUrl {
|
||||
String? url;
|
||||
var server = DataSp.getServerConfig();
|
||||
if (null != server) {
|
||||
url = server['chatTokenUrl'];
|
||||
}
|
||||
return url ?? (_isIP ? "http://$_host:10009" : "https://$_host/chat");
|
||||
}
|
||||
|
||||
static String get appAuthUrl {
|
||||
String? url;
|
||||
var server = DataSp.getServerConfig();
|
||||
if (null != server) {
|
||||
url = server['authUrl'];
|
||||
}
|
||||
return url ?? (_isIP ? "http://$_host:10008" : "https://$_host/chat");
|
||||
}
|
||||
|
||||
static String get imApiUrl {
|
||||
String? url;
|
||||
var server = DataSp.getServerConfig();
|
||||
if (null != server) {
|
||||
url = server['apiUrl'];
|
||||
}
|
||||
return url ?? (_isIP ? 'http://$_host:10002' : "https://$_host/api");
|
||||
}
|
||||
|
||||
static String get imWsUrl {
|
||||
String? url;
|
||||
var server = DataSp.getServerConfig();
|
||||
if (null != server) {
|
||||
url = server['wsUrl'];
|
||||
}
|
||||
return url ?? (_isIP ? "ws://$_host:10001" : "wss://$_host/msg_gateway");
|
||||
}
|
||||
|
||||
static int get logLevel {
|
||||
String? level;
|
||||
var server = DataSp.getServerConfig();
|
||||
if (null != server) {
|
||||
level = server['logLevel'];
|
||||
}
|
||||
return level == null ? 5 : int.parse(level);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class CacheController extends GetxController {
|
||||
final favoriteList = <EmojiInfo>[].obs;
|
||||
final callRecordList = <CallRecords>[].obs;
|
||||
Box? favoriteBox;
|
||||
Box? callRecordBox;
|
||||
bool _isInitFavoriteList = false;
|
||||
bool _isInitCallRecords = false;
|
||||
|
||||
String get userID => DataSp.getLoginCertificate()!.userID;
|
||||
|
||||
void addFavoriteFromUrl(String? url, int? width, int? height) {
|
||||
var emoji = EmojiInfo(url: url, width: width, height: height);
|
||||
favoriteList.insert(0, emoji);
|
||||
final list = favoriteList.value;
|
||||
favoriteBox?.put(userID, list);
|
||||
}
|
||||
|
||||
void addFavoriteFromPath(String path, int width, int height) async {
|
||||
var result = await LoadingView.singleton.wrap(
|
||||
asyncFunction: () => OpenIM.iMManager.uploadFile(
|
||||
id: const Uuid().v4(),
|
||||
filePath: path,
|
||||
fileName: path,
|
||||
),
|
||||
);
|
||||
if (result is String) {
|
||||
final url = jsonDecode(result)['url'];
|
||||
Logger.print('url:$url');
|
||||
var emoji = EmojiInfo(url: url, width: width, height: height);
|
||||
Logger.print('addFavoriteFromPath :$url');
|
||||
favoriteList.insert(0, emoji);
|
||||
favoriteBox?.put(userID, favoriteList.value);
|
||||
}
|
||||
}
|
||||
|
||||
void delFavorite(String url) {
|
||||
favoriteList.removeWhere((element) => element.url == url);
|
||||
favoriteBox?.put(userID, favoriteList.value);
|
||||
}
|
||||
|
||||
void delFavoriteList(List<String> urlList) {
|
||||
for (final url in urlList) {
|
||||
favoriteList.removeWhere((element) => element.url == url);
|
||||
}
|
||||
favoriteBox?.put(userID, favoriteList.value);
|
||||
}
|
||||
|
||||
initFavoriteEmoji() {
|
||||
if (!_isInitFavoriteList) {
|
||||
_isInitFavoriteList = true;
|
||||
var list = favoriteBox?.get(userID, defaultValue: <EmojiInfo>[]);
|
||||
if (list != null) {
|
||||
favoriteList.assignAll((list as List).cast());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<String> get urlList => favoriteList.map((e) => e.url!).toList();
|
||||
|
||||
initCallRecords() {
|
||||
if (!_isInitCallRecords) {
|
||||
_isInitCallRecords = true;
|
||||
var list = callRecordBox?.get(userID, defaultValue: <CallRecords>[]);
|
||||
if (list != null) {
|
||||
callRecordList.assignAll((list as List).cast());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void resetCache() {
|
||||
if (_isInitCallRecords) {
|
||||
callRecordList.value = [];
|
||||
final list = callRecordBox?.get(userID, defaultValue: <CallRecords>[]);
|
||||
|
||||
if (list != null) {
|
||||
callRecordList.assignAll((list as List).cast());
|
||||
}
|
||||
}
|
||||
|
||||
if (_isInitFavoriteList) {
|
||||
favoriteList.value = [];
|
||||
final list = favoriteBox?.get(userID, defaultValue: <CallRecords>[]);
|
||||
|
||||
if (list != null) {
|
||||
favoriteList.assignAll((list as List).cast());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addCallRecords(CallRecords records) {
|
||||
callRecordList.insert(0, records);
|
||||
callRecordBox?.put(userID, callRecordList.value);
|
||||
}
|
||||
|
||||
deleteCallRecords(CallRecords records) async {
|
||||
callRecordList.removeWhere((element) => element.userID == records.userID && element.date == records.date);
|
||||
await callRecordBox?.put(userID, callRecordList.value);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_isInitFavoriteList = false;
|
||||
_isInitCallRecords = false;
|
||||
Hive.close();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
Hive.registerAdapter(EmojiInfoAdapter());
|
||||
Hive.registerAdapter(CallRecordsAdapter());
|
||||
|
||||
favoriteBox = await Hive.openBox<List>('favoriteEmoji');
|
||||
callRecordBox = await Hive.openBox<List>('callRecords');
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_download_manager/flutter_download_manager.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class DownloadController extends GetxController {
|
||||
final downloadManager = DownloadManager();
|
||||
String? savedDir;
|
||||
final downloadTaskList = <String?, DownloadTask>{}.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
_initDir();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
_initDir() async {
|
||||
savedDir ??= await IMUtils.getDownloadFileDir();
|
||||
}
|
||||
|
||||
DownloadTask? getTask(String url) {
|
||||
return downloadManager.getDownload(url);
|
||||
}
|
||||
|
||||
bool isExistTask(String url) {
|
||||
return null != downloadTaskList[url];
|
||||
}
|
||||
|
||||
bool isExistMessageTask(Message message) =>
|
||||
message.isFileType && null != message.fileElem?.sourceUrl && isExistTask(message.fileElem!.sourceUrl!);
|
||||
|
||||
ValueNotifier<DownloadStatus> getStatus(Message message) => downloadTaskList[message.fileElem!.sourceUrl!]!.status;
|
||||
|
||||
ValueNotifier<double> getProgress(Message message) => downloadTaskList[message.fileElem!.sourceUrl!]!.progress;
|
||||
|
||||
void addDownload(String url, {String? path}) {
|
||||
Permissions.storage(() async {
|
||||
await _initDir();
|
||||
path ??= "$savedDir/${downloadManager.getFileNameFromUrl(url)}";
|
||||
DownloadTask? task = await downloadManager.addDownload(url, path!);
|
||||
if (null != task) downloadTaskList[url] = task;
|
||||
});
|
||||
}
|
||||
|
||||
void addDownloadForMessage(Message message, {String? path}) {
|
||||
if (message.isFileType) {
|
||||
final url = message.fileElem?.sourceUrl;
|
||||
if (null != url) {
|
||||
Permissions.storage(() async {
|
||||
await _initDir();
|
||||
path ??= "$savedDir/${downloadManager.getFileNameFromUrl(url)}";
|
||||
DownloadTask? task = await downloadManager.addDownload(url, path!);
|
||||
if (null != task) downloadTaskList[url] = task;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clickFileMessage(String url, String path) async {
|
||||
var task = getTask(url);
|
||||
Logger.print(
|
||||
'clickFileMessage 当前状态: ${task?.status.value} 进度:${task?.progress.value} 完成:${task?.status.value.isCompleted} $url $path');
|
||||
if (task != null && !task.status.value.isCompleted) {
|
||||
switch (task.status.value) {
|
||||
case DownloadStatus.downloading:
|
||||
downloadManager.pauseDownload(url);
|
||||
break;
|
||||
case DownloadStatus.paused:
|
||||
downloadManager.resumeDownload(url);
|
||||
break;
|
||||
case DownloadStatus.queued:
|
||||
break;
|
||||
case DownloadStatus.completed:
|
||||
break;
|
||||
case DownloadStatus.failed:
|
||||
await downloadManager.removeDownload(url);
|
||||
addDownload(url, path: path);
|
||||
break;
|
||||
case DownloadStatus.canceled:
|
||||
addDownload(url, path: path);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
addDownload(url, path: path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||
|
||||
class DefaultFirebaseOptions {
|
||||
static FirebaseOptions get currentPlatform {
|
||||
if (kIsWeb) {
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for web - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
}
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return android;
|
||||
case TargetPlatform.iOS:
|
||||
return ios;
|
||||
case TargetPlatform.macOS:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for macos - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.windows:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for windows - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.linux:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for linux - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions are not supported for this platform.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static const FirebaseOptions android = FirebaseOptions(
|
||||
apiKey: 'AIzaSyCw-ohvxFisvm3Yrb4kCzPBuL0YfZkzI9Q',
|
||||
appId: '1:299075855003:android:292449dc34ad498109dc3e',
|
||||
messagingSenderId: '299075855003',
|
||||
projectId: 'im-fer-c3347',
|
||||
storageBucket: 'im-fer-c3347.firebasestorage.app',
|
||||
);
|
||||
|
||||
static const FirebaseOptions ios = FirebaseOptions(
|
||||
apiKey: 'AIzaSyC0BhV-mo84LvOuqWf2bS966jlf0f8mh74',
|
||||
appId: '1:299075855003:ios:23b06300e33d984309dc3e',
|
||||
messagingSenderId: '299075855003',
|
||||
projectId: 'im-fer-c3347',
|
||||
storageBucket: 'im-fer-c3347.firebasestorage.app',
|
||||
iosBundleId: 'io.openim.flutter.full.NotificationService',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:getuiflut/getuiflut.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:google_api_availability/google_api_availability.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
import 'firebase_options.dart';
|
||||
|
||||
enum PushType { getui, FCM }
|
||||
|
||||
const appID = 'your-app-id';
|
||||
const appKey = 'your-app-key';
|
||||
const appSecret = 'your-app-secret';
|
||||
|
||||
class PushController extends GetxService {
|
||||
PushType pushType = PushType.getui;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
if (PushController().pushType == PushType.getui) {
|
||||
GetuiPushController()._addEventHandler();
|
||||
GetuiPushController()._initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs in the user with the specified alias to the push notification service.
|
||||
///
|
||||
/// Depending on the push type configured, it either logs in using the Getui or
|
||||
/// FCM push service.
|
||||
///
|
||||
/// If using Getui, it binds the alias to the Getui service.
|
||||
///
|
||||
/// If using FCM, it listens for token refresh events and logs in, invoking the
|
||||
/// provided callback with the new token.
|
||||
///
|
||||
/// Throws an assertion error if the FCM push type is selected but the
|
||||
/// `onTokenRefresh` callback is not provided.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - alias: The alias to bind to the push notification service for getui.
|
||||
/// - onTokenRefresh: A callback function that is invoked with the refreshed
|
||||
/// token when using FCM. Required if the push type is FCM.
|
||||
static void login(String alias, {void Function(String token)? onTokenRefresh}) {
|
||||
assert((PushController().pushType == PushType.FCM && onTokenRefresh != null) ||
|
||||
(PushController().pushType == PushType.getui && alias.isNotEmpty));
|
||||
|
||||
if (PushController().pushType == PushType.getui) {
|
||||
GetuiPushController()._login(alias);
|
||||
} else {
|
||||
FCMPushController()._initialize().then((_) {
|
||||
FCMPushController()._getToken().then((token) => onTokenRefresh!(token));
|
||||
FCMPushController()._listenToTokenRefresh((token) => onTokenRefresh);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void logout() {
|
||||
if (PushController().pushType == PushType.getui) {
|
||||
GetuiPushController()._logout();
|
||||
} else {
|
||||
FCMPushController()._deleteToken();
|
||||
}
|
||||
}
|
||||
|
||||
static void setBadge(int badge) {
|
||||
if (PushController().pushType == PushType.getui) {
|
||||
GetuiPushController()._setBadge(badge);
|
||||
}
|
||||
}
|
||||
|
||||
static void resetBadge() {
|
||||
if (PushController().pushType == PushType.getui) {
|
||||
GetuiPushController()._resetBadge();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetuiPushController {
|
||||
static final GetuiPushController _instance = GetuiPushController._();
|
||||
factory GetuiPushController() => _instance;
|
||||
|
||||
GetuiPushController._();
|
||||
|
||||
Future<void> _initialize() async {
|
||||
Permissions.notification().then((isGranted) {
|
||||
if (isGranted) {
|
||||
try {
|
||||
Getuiflut.initGetuiSdk;
|
||||
} catch (e) {
|
||||
e.toString();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _addEventHandler() {
|
||||
if (Platform.isIOS) {
|
||||
Getuiflut().startSdk(
|
||||
appId: appID,
|
||||
appKey: appKey,
|
||||
appSecret: appSecret,
|
||||
);
|
||||
|
||||
Getuiflut().runBackgroundEnable(0);
|
||||
}
|
||||
|
||||
Getuiflut().addEventHandler(
|
||||
onReceiveClientId: (String message) async {
|
||||
print("flutter onReceiveClientId: $message");
|
||||
},
|
||||
onRegisterDeviceToken: (String message) async {
|
||||
print("flutter onRegisterDeviceToken: $message");
|
||||
},
|
||||
onReceivePayload: (Map<String, dynamic> message) async {},
|
||||
onReceiveNotificationResponse: (Map<String, dynamic> message) async {},
|
||||
onAppLinkPayload: (String message) async {},
|
||||
onReceiveOnlineState: (bool online) async {},
|
||||
onPushModeResult: (Map<String, dynamic> message) async {},
|
||||
onSetTagResult: (Map<String, dynamic> message) async {},
|
||||
onAliasResult: (Map<String, dynamic> message) async {},
|
||||
onQueryTagResult: (Map<String, dynamic> message) async {},
|
||||
onWillPresentNotification: (Map<String, dynamic> message) async {},
|
||||
onOpenSettingsForNotification: (Map<String, dynamic> message) async {},
|
||||
onGrantAuthorization: (String granted) async {},
|
||||
onReceiveMessageData: (Map<String, dynamic> event) async {
|
||||
print("flutter onReceiveMessageData: $event");
|
||||
},
|
||||
onNotificationMessageArrived: (Map<String, dynamic> event) async {},
|
||||
onNotificationMessageClicked: (Map<String, dynamic> event) async {},
|
||||
onTransmitUserMessageReceive: (Map<String, dynamic> event) async {},
|
||||
onLiveActivityResult: (Map<String, dynamic> event) async {},
|
||||
onRegisterPushToStartTokenResult: (Map<String, dynamic> event) async {},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _login(String uid) async {
|
||||
print('login user ID: $uid, client id: ${await Getuiflut.getClientId}');
|
||||
Getuiflut().bindAlias(uid, 'openim');
|
||||
}
|
||||
|
||||
void _logout() {
|
||||
Getuiflut().unbindAlias(OpenIM.iMManager.userID, 'openim', true);
|
||||
}
|
||||
|
||||
void _setBadge(int badge) {
|
||||
Getuiflut().setBadge(badge);
|
||||
}
|
||||
|
||||
void _resetBadge() {
|
||||
Getuiflut().resetBadge();
|
||||
}
|
||||
}
|
||||
|
||||
class FCMPushController {
|
||||
static final FCMPushController _instance = FCMPushController._internal();
|
||||
factory FCMPushController() => _instance;
|
||||
|
||||
FCMPushController._internal();
|
||||
|
||||
Future<void> _initialize() async {
|
||||
GooglePlayServicesAvailability? availability = GooglePlayServicesAvailability.success;
|
||||
if (Platform.isAndroid) {
|
||||
availability = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability();
|
||||
}
|
||||
if (availability != GooglePlayServicesAvailability.serviceInvalid) {
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
} else {
|
||||
Logger.print('Google Play Services are not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await _requestPermission();
|
||||
|
||||
_configureForegroundNotification();
|
||||
|
||||
_configureBackgroundNotification();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> _requestPermission() async {
|
||||
NotificationSettings settings = await FirebaseMessaging.instance.requestPermission();
|
||||
print('User granted permission: ${settings.authorizationStatus}');
|
||||
}
|
||||
|
||||
void _configureForegroundNotification() {
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
|
||||
print('Foreground notification received: ${message.notification?.title}');
|
||||
|
||||
if (message.notification != null) {}
|
||||
});
|
||||
}
|
||||
|
||||
void _configureBackgroundNotification() {
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||||
print('App opened from background: ${message.notification?.title}');
|
||||
});
|
||||
|
||||
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) {
|
||||
if (message != null) {
|
||||
print('App opened from terminated state: ${message.notification?.title}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<String> _getToken() async {
|
||||
final token = await FirebaseMessaging.instance.getToken();
|
||||
Logger.print("FCM Token: $token");
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('FCM Token is null');
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
Future<void> _deleteToken() {
|
||||
return FirebaseMessaging.instance.deleteToken();
|
||||
}
|
||||
|
||||
void _listenToTokenRefresh(void Function(String token) onTokenRefresh) {
|
||||
FirebaseMessaging.instance.onTokenRefresh.listen((String newToken) {
|
||||
print("FCM Token refreshed: $newToken");
|
||||
onTokenRefresh(newToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
extension SubjectExt<T> on Subject<T> {
|
||||
T addSafely(T data) {
|
||||
if (!isClosed) sink.add(data);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
extension TextEdCtrlExt on TextEditingController {
|
||||
void fixed63Length() {
|
||||
addListener(() {
|
||||
if (text.length == 63 && Platform.isAndroid) {
|
||||
text += " ";
|
||||
selection = TextSelection.fromPosition(
|
||||
TextPosition(
|
||||
affinity: TextAffinity.downstream,
|
||||
offset: text.length - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension StrExt on String {
|
||||
ImageView get toImage {
|
||||
return ImageView(name: this);
|
||||
}
|
||||
|
||||
TextView get toText {
|
||||
return TextView(data: this);
|
||||
}
|
||||
|
||||
LottieView get toLottie {
|
||||
return LottieView(name: this);
|
||||
}
|
||||
|
||||
String fixAutoLines() {
|
||||
return Characters(this).join('\u{200B}');
|
||||
}
|
||||
|
||||
String get thumbnailAbsoluteString {
|
||||
final host = split('?').first;
|
||||
final isGif = host.split('.').last.toLowerCase() == 'gif';
|
||||
if (isGif) {
|
||||
return host;
|
||||
}
|
||||
return '$host?height=640&width=360&type=image';
|
||||
}
|
||||
|
||||
String adjustThumbnailAbsoluteString(int size) {
|
||||
final host = split('?').first;
|
||||
final isGif = host.split('.').last.toLowerCase() == 'gif';
|
||||
if (isGif) {
|
||||
return host;
|
||||
}
|
||||
return '$host?height=$size&width=$size&type=image';
|
||||
}
|
||||
}
|
||||
|
||||
class LottieView extends StatelessWidget {
|
||||
LottieView({
|
||||
Key? key,
|
||||
required this.name,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit,
|
||||
}) : super(key: key);
|
||||
final String name;
|
||||
double? width;
|
||||
double? height;
|
||||
BoxFit? fit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Lottie.asset(
|
||||
name,
|
||||
height: height,
|
||||
width: width,
|
||||
package: 'openim_common',
|
||||
fit: fit,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextView extends StatelessWidget {
|
||||
TextView(
|
||||
{Key? key,
|
||||
required this.data,
|
||||
this.style,
|
||||
this.textAlign,
|
||||
this.overflow,
|
||||
this.textScaleFactor,
|
||||
this.maxLines,
|
||||
this.onTap,
|
||||
this.wordEllipsis = true})
|
||||
: super(key: key);
|
||||
final String data;
|
||||
TextStyle? style;
|
||||
TextAlign? textAlign;
|
||||
TextOverflow? overflow;
|
||||
double? textScaleFactor;
|
||||
int? maxLines;
|
||||
Function()? onTap;
|
||||
bool wordEllipsis;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Text(
|
||||
wordEllipsis ? data : data.replaceAll('', '\u200B'), //解决Flutter按单词省略
|
||||
style: style,
|
||||
textAlign: textAlign,
|
||||
overflow: overflow,
|
||||
textScaleFactor: textScaleFactor,
|
||||
maxLines: maxLines,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ImageView extends StatelessWidget {
|
||||
ImageView({
|
||||
Key? key,
|
||||
required this.name,
|
||||
this.width,
|
||||
this.height,
|
||||
this.color,
|
||||
this.opacity = 1,
|
||||
this.fit,
|
||||
this.onTap,
|
||||
this.onDoubleTap,
|
||||
}) : super(key: key);
|
||||
final String name;
|
||||
double? width;
|
||||
double? height;
|
||||
Color? color;
|
||||
double opacity;
|
||||
BoxFit? fit;
|
||||
Function()? onTap;
|
||||
Function()? onDoubleTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onTap,
|
||||
onDoubleTap: onDoubleTap,
|
||||
child: Opacity(
|
||||
opacity: opacity,
|
||||
child: Image.asset(
|
||||
name,
|
||||
package: 'openim_common',
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
fit: fit,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
extension IntExt on int {
|
||||
int ensureTenDigits() {
|
||||
String numberStr = toString();
|
||||
|
||||
if (numberStr.length == 10) {
|
||||
return this;
|
||||
} else if (numberStr.length > 10) {
|
||||
return int.parse(numberStr.substring(0, 10));
|
||||
} else {
|
||||
return int.parse(numberStr.padLeft(10, '0'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StringExt on String {
|
||||
String splitStringEveryNChars({int n = 3, String separator = ' '}) {
|
||||
RegExp regExp = RegExp('.{1,$n}');
|
||||
Iterable<Match> matches = regExp.allMatches(this);
|
||||
List<String> parts = matches.map((match) => match.group(0)!).toList();
|
||||
|
||||
return parts.join(separator);
|
||||
}
|
||||
}
|
||||
|
||||
extension DateTimeExt on DateTime {
|
||||
int get secondsSinceEpoch => millisecondsSinceEpoch ~/ 1000;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
extension MessageManagerExt on MessageManager {
|
||||
Future<Message> createCustomEmojiMessage({
|
||||
required String url,
|
||||
int? width,
|
||||
int? height,
|
||||
}) =>
|
||||
createCustomMessage(
|
||||
data: json.encode({
|
||||
"customType": CustomMessageType.emoji,
|
||||
"data": {
|
||||
'url': url,
|
||||
'width': width,
|
||||
'height': height,
|
||||
},
|
||||
}),
|
||||
extension: '',
|
||||
description: '',
|
||||
);
|
||||
|
||||
Future<Message> createFailedHintMessage({required int type}) => createCustomMessage(
|
||||
data: json.encode({
|
||||
"customType": type,
|
||||
"data": {},
|
||||
}),
|
||||
extension: '',
|
||||
description: '',
|
||||
);
|
||||
}
|
||||
|
||||
extension MessageExt on Message {
|
||||
|
||||
bool get isDeletedByFriendType {
|
||||
if (isCustomType) {
|
||||
try {
|
||||
var map = json.decode(customElem!.data!);
|
||||
var customType = map['customType'];
|
||||
return CustomMessageType.deletedByFriend == customType;
|
||||
} catch (e, s) {
|
||||
Logger.print('$e $s');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get isBlockedByFriendType {
|
||||
if (isCustomType) {
|
||||
try {
|
||||
var map = json.decode(customElem!.data!);
|
||||
var customType = map['customType'];
|
||||
return CustomMessageType.blockedByFriend == customType;
|
||||
} catch (e, s) {
|
||||
Logger.print('$e $s');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get isEmojiType {
|
||||
if (isCustomType) {
|
||||
try {
|
||||
var map = json.decode(customElem!.data!);
|
||||
var customType = map['customType'];
|
||||
return CustomMessageType.emoji == customType;
|
||||
} catch (e, s) {
|
||||
Logger.print('$e $s');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get isTextType => contentType == MessageType.text;
|
||||
|
||||
bool get isPictureType => contentType == MessageType.picture;
|
||||
|
||||
bool get isVoiceType => contentType == MessageType.voice;
|
||||
|
||||
bool get isVideoType => contentType == MessageType.video;
|
||||
|
||||
bool get isFileType => contentType == MessageType.file;
|
||||
|
||||
bool get isLocationType => contentType == MessageType.location;
|
||||
|
||||
bool get isCardType => contentType == MessageType.card;
|
||||
|
||||
bool get isCustomFaceType => contentType == MessageType.customFace;
|
||||
|
||||
bool get isCustomType => contentType == MessageType.custom;
|
||||
|
||||
bool get isRevokeType => contentType == MessageType.revokeMessageNotification;
|
||||
|
||||
bool get isNotificationType => contentType! >= 1000;
|
||||
}
|
||||
|
||||
class CustomMessageType {
|
||||
static const callingInvite = 200;
|
||||
static const callingAccept = 201;
|
||||
static const callingReject = 202;
|
||||
static const callingCancel = 203;
|
||||
static const callingHungup = 204;
|
||||
|
||||
static const call = 901;
|
||||
static const emoji = 902;
|
||||
static const tag = 903;
|
||||
static const moments = 904;
|
||||
static const meeting = 905;
|
||||
static const blockedByFriend = 910;
|
||||
static const deletedByFriend = 911;
|
||||
static const removedFromGroup = 912;
|
||||
static const groupDisbanded = 913;
|
||||
}
|
||||
|
||||
extension PublicUserInfoExt on PublicUserInfo {
|
||||
UserInfo get simpleUserInfo {
|
||||
return UserInfo(userID: userID, nickname: nickname, faceURL: faceURL);
|
||||
}
|
||||
}
|
||||
|
||||
extension FriendInfoExt on FriendInfo {
|
||||
UserInfo get simpleUserInfo {
|
||||
return UserInfo(userID: userID, nickname: nickname, faceURL: faceURL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class ApiResp {
|
||||
int errCode;
|
||||
String errMsg;
|
||||
String errDlt;
|
||||
dynamic data;
|
||||
|
||||
ApiResp.fromJson(Map<String, dynamic> map)
|
||||
: errCode = map["errCode"] ?? -1,
|
||||
errMsg = map["errMsg"] ?? '',
|
||||
errDlt = map["errDlt"] ?? '',
|
||||
data = map["data"];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['errCode'] = errCode;
|
||||
data['errMsg'] = errMsg;
|
||||
data['errDlt'] = errDlt;
|
||||
data['data'] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiError {
|
||||
static String getMsg(int errorCode) {
|
||||
return _errorZH['$errorCode'] ?? '';
|
||||
}
|
||||
|
||||
static const _errorZH = {
|
||||
'10001': '请求参数错误',
|
||||
'10002': '数据库错误',
|
||||
'10003': '服务器错误',
|
||||
'10006': '记录不存在',
|
||||
'20001': '账号已注册',
|
||||
'20002': '重复发送验证码',
|
||||
'20003': '邀请码错误',
|
||||
'20004': '注册IP受限',
|
||||
'30001': '验证码错误',
|
||||
'30002': '验证码已过期',
|
||||
'30003': '邀请码被使用',
|
||||
'30004': '邀请码不存在',
|
||||
'40001': '账号未注册',
|
||||
'40002': '密码错误',
|
||||
'40003': '登录受ip限制',
|
||||
'40004': 'ip禁止注册登录',
|
||||
'50001': '过期',
|
||||
'50002': '格式错误',
|
||||
'50003': '未生效',
|
||||
'50004': '未知错误',
|
||||
'50005': '创建错误',
|
||||
};
|
||||
static const _errorEN = {};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'define.dart';
|
||||
|
||||
class BookingConfig {
|
||||
String name;
|
||||
int beginTime;
|
||||
int duration;
|
||||
int timeZone;
|
||||
int endsIn;
|
||||
int repeatTimes;
|
||||
|
||||
int interval;
|
||||
List<int>? repeatDaysOfWeek;
|
||||
int monthUnit;
|
||||
List<int>? dates;
|
||||
|
||||
bool enableMeetingPassword;
|
||||
String? meetingPassword;
|
||||
bool enableEnterBeforeHost;
|
||||
bool enableMicrophone;
|
||||
bool enableCamera;
|
||||
|
||||
BookingConfig({
|
||||
required this.name,
|
||||
required this.beginTime,
|
||||
required this.duration,
|
||||
this.timeZone = 8,
|
||||
this.endsIn = 0,
|
||||
this.repeatTimes = 0,
|
||||
this.interval = 0,
|
||||
this.repeatDaysOfWeek,
|
||||
this.monthUnit = 0,
|
||||
this.dates,
|
||||
this.enableMeetingPassword = false,
|
||||
this.meetingPassword,
|
||||
this.enableEnterBeforeHost = true,
|
||||
this.enableMicrophone = true,
|
||||
this.enableCamera = true,
|
||||
});
|
||||
|
||||
factory BookingConfig.fromMap(Map<String, dynamic> json) => BookingConfig(
|
||||
name: json['name'],
|
||||
beginTime: json['beginTime'],
|
||||
duration: json['duration'],
|
||||
timeZone: json['timeZone'],
|
||||
endsIn: json['endsIn'],
|
||||
repeatTimes: json['repeatTimes'],
|
||||
interval: json['interval'],
|
||||
repeatDaysOfWeek: json['repeatDaysOfWeek'],
|
||||
monthUnit: json['monthUnit'],
|
||||
dates: json['dates'],
|
||||
enableMeetingPassword: json['enableMeetingPassword'],
|
||||
meetingPassword: json['meetingPassword'],
|
||||
enableEnterBeforeHost: json['enableEnterBeforeHost'],
|
||||
enableMicrophone: json['enableMicrophone'],
|
||||
enableCamera: json['enableCamera'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'name': name,
|
||||
'beginTime': beginTime,
|
||||
'duration': duration,
|
||||
'timeZone': timeZone,
|
||||
'endsIn': endsIn,
|
||||
'repeatTimes': repeatTimes,
|
||||
'interval': interval,
|
||||
'repeatDaysOfWeek': repeatDaysOfWeek,
|
||||
'monthUnit': monthUnit,
|
||||
'dates': dates,
|
||||
'enableMeetingPassword': enableMeetingPassword,
|
||||
'meetingPassword': meetingPassword,
|
||||
'enableEnterBeforeHost': enableEnterBeforeHost,
|
||||
'enableMicrophone': enableMicrophone,
|
||||
'enableCamera': enableCamera,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'call_records.g.dart';
|
||||
|
||||
@HiveType(typeId: 4)
|
||||
class CallRecords {
|
||||
@HiveField(1)
|
||||
String userID;
|
||||
@HiveField(2)
|
||||
String nickname;
|
||||
@HiveField(3)
|
||||
String? faceURL;
|
||||
@HiveField(4)
|
||||
String type;
|
||||
@HiveField(5)
|
||||
bool success;
|
||||
@HiveField(6)
|
||||
bool incomingCall;
|
||||
@HiveField(7)
|
||||
int date;
|
||||
@HiveField(8)
|
||||
int duration;
|
||||
|
||||
CallRecords({
|
||||
required this.userID,
|
||||
required this.nickname,
|
||||
this.faceURL,
|
||||
required this.type,
|
||||
required this.success,
|
||||
required this.incomingCall,
|
||||
required this.date,
|
||||
required this.duration,
|
||||
});
|
||||
|
||||
CallRecords.fromJson(Map<String, dynamic> json)
|
||||
: userID = json['userID'],
|
||||
nickname = json['nickname'],
|
||||
faceURL = json['faceURL'],
|
||||
type = json['type'],
|
||||
success = json['success'],
|
||||
incomingCall = json['incomingCall'],
|
||||
date = json['date'],
|
||||
duration = json['duration'];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = Map<String, dynamic>();
|
||||
data['userID'] = this.userID;
|
||||
data['nickname'] = this.nickname;
|
||||
data['faceURL'] = this.faceURL;
|
||||
data['type'] = this.type;
|
||||
data['success'] = this.success;
|
||||
data['incomingCall'] = this.incomingCall;
|
||||
data['date'] = this.date;
|
||||
data['duration'] = this.duration;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
part of 'call_records.dart';
|
||||
|
||||
class CallRecordsAdapter extends TypeAdapter<CallRecords> {
|
||||
@override
|
||||
final int typeId = 4;
|
||||
|
||||
@override
|
||||
CallRecords read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return CallRecords(
|
||||
userID: fields[1] as String,
|
||||
nickname: fields[2] as String,
|
||||
faceURL: fields[3] as String?,
|
||||
type: fields[4] as String,
|
||||
success: fields[5] as bool,
|
||||
incomingCall: fields[6] as bool,
|
||||
date: fields[7] as int,
|
||||
duration: fields[8] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CallRecords obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(1)
|
||||
..write(obj.userID)
|
||||
..writeByte(2)
|
||||
..write(obj.nickname)
|
||||
..writeByte(3)
|
||||
..write(obj.faceURL)
|
||||
..writeByte(4)
|
||||
..write(obj.type)
|
||||
..writeByte(5)
|
||||
..write(obj.success)
|
||||
..writeByte(6)
|
||||
..write(obj.incomingCall)
|
||||
..writeByte(7)
|
||||
..write(obj.date)
|
||||
..writeByte(8)
|
||||
..write(obj.duration);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CallRecordsAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
enum OperationType { participants, roomSettings, leave, end, setting, onlyClose, kickOff } // case setting for app setting
|
||||
|
||||
enum OperationParticipantType {
|
||||
pined,
|
||||
focus,
|
||||
camera,
|
||||
microphone,
|
||||
nickname,
|
||||
setHost,
|
||||
kickoff,
|
||||
muteAll,
|
||||
}
|
||||
|
||||
enum RoomSetting { allowParticipantUnMute, allowParticipantVideo, onlyHostCanShareScreen, defaultMuted, lockMeeting, audioEncouragement }
|
||||
|
||||
enum MeetingStatus {
|
||||
scheduled,
|
||||
inProgress,
|
||||
completed,
|
||||
}
|
||||
|
||||
extension MeetingStatusExt on MeetingStatus {
|
||||
String get rawValue {
|
||||
switch (this) {
|
||||
case MeetingStatus.scheduled:
|
||||
return 'Scheduled';
|
||||
case MeetingStatus.inProgress:
|
||||
return 'In-Progress';
|
||||
case MeetingStatus.completed:
|
||||
return 'Completed';
|
||||
}
|
||||
}
|
||||
|
||||
static MeetingStatus fromString(String str) {
|
||||
switch (str) {
|
||||
case 'Scheduled':
|
||||
return MeetingStatus.scheduled;
|
||||
case 'In-Progress':
|
||||
return MeetingStatus.inProgress;
|
||||
case 'Completed':
|
||||
return MeetingStatus.completed;
|
||||
default:
|
||||
throw Exception('Invalid MeetingStatus: $str');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Weekdays {
|
||||
monday,
|
||||
tuesday,
|
||||
wednesday,
|
||||
thursday,
|
||||
friday,
|
||||
saturday,
|
||||
sunday,
|
||||
}
|
||||
|
||||
extension WeekdaysExt on Weekdays {
|
||||
String get title {
|
||||
switch (this) {
|
||||
case Weekdays.monday:
|
||||
return StrRes.monday;
|
||||
case Weekdays.tuesday:
|
||||
return StrRes.tuesday;
|
||||
case Weekdays.wednesday:
|
||||
return StrRes.wednesday;
|
||||
case Weekdays.thursday:
|
||||
return StrRes.thursday;
|
||||
case Weekdays.friday:
|
||||
return StrRes.friday;
|
||||
case Weekdays.saturday:
|
||||
return StrRes.saturday;
|
||||
case Weekdays.sunday:
|
||||
return StrRes.sunday;
|
||||
}
|
||||
}
|
||||
|
||||
String get rawValue {
|
||||
switch (this) {
|
||||
case Weekdays.monday:
|
||||
return 'Monday';
|
||||
case Weekdays.tuesday:
|
||||
return 'Tuesday';
|
||||
case Weekdays.wednesday:
|
||||
return 'Wednesday';
|
||||
case Weekdays.thursday:
|
||||
return 'Thursday';
|
||||
case Weekdays.friday:
|
||||
return 'Friday';
|
||||
case Weekdays.saturday:
|
||||
return 'Saturday';
|
||||
case Weekdays.sunday:
|
||||
return 'Sunday';
|
||||
}
|
||||
}
|
||||
|
||||
static Weekdays fromString(String str) {
|
||||
switch (str) {
|
||||
case 'Monday':
|
||||
return Weekdays.monday;
|
||||
case 'Tuesday':
|
||||
return Weekdays.tuesday;
|
||||
case 'Wednesday':
|
||||
return Weekdays.wednesday;
|
||||
case 'Thursday':
|
||||
return Weekdays.thursday;
|
||||
case 'Friday':
|
||||
return Weekdays.friday;
|
||||
case 'Saturday':
|
||||
return Weekdays.saturday;
|
||||
case 'Sunday':
|
||||
return Weekdays.sunday;
|
||||
default:
|
||||
throw Exception('Invalid Weekdays: $str');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum HostType {
|
||||
host,
|
||||
coHost,
|
||||
}
|
||||
|
||||
extension HostTypeExt on HostType {
|
||||
static HostType fromString(String str) {
|
||||
switch (str) {
|
||||
case 'Host':
|
||||
return HostType.host;
|
||||
case 'CoHost':
|
||||
return HostType.coHost;
|
||||
default:
|
||||
throw Exception('Invalid HostType: $str');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MxNLayoutViewType { oneXn, twoXtwo, threeXthree }
|
||||
|
||||
extension MxNLayoutViewTypeExt on MxNLayoutViewType {
|
||||
int get rawValue {
|
||||
switch (this) {
|
||||
case MxNLayoutViewType.oneXn:
|
||||
return 1;
|
||||
case MxNLayoutViewType.twoXtwo:
|
||||
return 2;
|
||||
case MxNLayoutViewType.threeXthree:
|
||||
return 3;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'emoji_info.g.dart';
|
||||
|
||||
@HiveType(typeId: 1)
|
||||
class EmojiInfo {
|
||||
@HiveField(1)
|
||||
String? url;
|
||||
@HiveField(2)
|
||||
int? width;
|
||||
@HiveField(3)
|
||||
int? height;
|
||||
|
||||
EmojiInfo({this.url, this.width, this.height});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
part of 'emoji_info.dart';
|
||||
|
||||
class EmojiInfoAdapter extends TypeAdapter<EmojiInfo> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
EmojiInfo read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return EmojiInfo(
|
||||
url: fields[1] as String?,
|
||||
width: fields[2] as int?,
|
||||
height: fields[3] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, EmojiInfo obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(1)
|
||||
..write(obj.url)
|
||||
..writeByte(2)
|
||||
..write(obj.width)
|
||||
..writeByte(3)
|
||||
..write(obj.height);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || other is EmojiInfoAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:azlistview/azlistview.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
|
||||
class ISGroupInfo extends GroupInfo with ISuspensionBean {
|
||||
String? tagIndex;
|
||||
String? pinyin;
|
||||
String? shortPinyin;
|
||||
String? namePinyin;
|
||||
|
||||
ISGroupInfo.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
||||
tagIndex = json['tagIndex'];
|
||||
pinyin = json['pinyin'];
|
||||
shortPinyin = json['shortPinyin'];
|
||||
namePinyin = json['namePinyin'];
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
var map = super.toJson();
|
||||
map['tagIndex'] = tagIndex;
|
||||
map['pinyin'] = pinyin;
|
||||
map['shortPinyin'] = shortPinyin;
|
||||
map['namePinyin'] = namePinyin;
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String getSuspensionTag() {
|
||||
return tagIndex!;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return json.encode(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:azlistview/azlistview.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
|
||||
class ISGroupMembersInfo extends GroupMembersInfo with ISuspensionBean {
|
||||
String? tagIndex;
|
||||
String? pinyin;
|
||||
String? shortPinyin;
|
||||
String? namePinyin;
|
||||
|
||||
ISGroupMembersInfo.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json) {
|
||||
tagIndex = json['tagIndex'];
|
||||
pinyin = json['pinyin'];
|
||||
shortPinyin = json['shortPinyin'];
|
||||
namePinyin = json['namePinyin'];
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
var map = super.toJson();
|
||||
map['tagIndex'] = tagIndex;
|
||||
map['pinyin'] = pinyin;
|
||||
map['shortPinyin'] = shortPinyin;
|
||||
map['namePinyin'] = namePinyin;
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String getSuspensionTag() {
|
||||
return tagIndex!;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return json.encode(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class LoginCertificate {
|
||||
String userID;
|
||||
String imToken; // im的
|
||||
String chatToken; // 业务服务器的
|
||||
|
||||
LoginCertificate.fromJson(Map<String, dynamic> map)
|
||||
: userID = map["userID"] ?? '',
|
||||
imToken = map["imToken"] ?? '',
|
||||
chatToken = map['chatToken'] ?? '';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['userID'] = userID;
|
||||
data['imToken'] = imToken;
|
||||
data['chatToken'] = chatToken;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class OnlineStatus {
|
||||
String? userID;
|
||||
String? status;
|
||||
List<DetailPlatformStatus>? detailPlatformStatus;
|
||||
|
||||
OnlineStatus({this.userID, this.status, this.detailPlatformStatus});
|
||||
|
||||
OnlineStatus.fromJson(Map<String, dynamic> json) {
|
||||
userID = json['userID'];
|
||||
status = json['status'];
|
||||
if (json['detailPlatformStatus'] != null) {
|
||||
detailPlatformStatus = <DetailPlatformStatus>[];
|
||||
json['detailPlatformStatus'].forEach((v) {
|
||||
detailPlatformStatus!.add(DetailPlatformStatus.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['userID'] = userID;
|
||||
data['status'] = status;
|
||||
if (detailPlatformStatus != null) {
|
||||
data['detailPlatformStatus'] =
|
||||
detailPlatformStatus!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class DetailPlatformStatus {
|
||||
String? platform;
|
||||
String? status;
|
||||
|
||||
DetailPlatformStatus({this.platform, this.status});
|
||||
|
||||
DetailPlatformStatus.fromJson(Map<String, dynamic> json) {
|
||||
platform = json['platform'];
|
||||
status = json['status'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['platform'] = platform;
|
||||
data['status'] = status;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
|
||||
class SignalingInfo {
|
||||
String? userID;
|
||||
|
||||
InvitationInfo? invitation;
|
||||
|
||||
OfflinePushInfo? offlinePushInfo;
|
||||
|
||||
SignalingInfo({
|
||||
this.userID,
|
||||
this.invitation,
|
||||
this.offlinePushInfo,
|
||||
});
|
||||
|
||||
SignalingInfo.fromJson(Map<String, dynamic> json) {
|
||||
invitation = json['invitation'] == null ? null : InvitationInfo.fromJson(json['invitation']);
|
||||
offlinePushInfo = json['offlinePushInfo'] == null ? null : OfflinePushInfo.fromJson(json['offlinePushInfo']);
|
||||
userID = json['userID'] ?? invitation?.inviterUserID;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['userID'] = this.userID;
|
||||
data['invitation'] = this.invitation?.toJson();
|
||||
data['offlinePushInfo'] = this.offlinePushInfo?.toJson();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class InvitationInfo {
|
||||
String? inviterUserID;
|
||||
|
||||
List<String>? inviteeUserIDList;
|
||||
|
||||
String? groupID;
|
||||
|
||||
String? roomID;
|
||||
|
||||
int? timeout;
|
||||
|
||||
String? mediaType;
|
||||
|
||||
int? sessionType;
|
||||
|
||||
int? platformID;
|
||||
|
||||
InvitationInfo(
|
||||
{this.inviterUserID, this.inviteeUserIDList, this.groupID, this.roomID, this.timeout, this.mediaType, this.sessionType, this.platformID});
|
||||
|
||||
InvitationInfo.fromJson(Map<String, dynamic> json) {
|
||||
inviterUserID = json['inviterUserID'];
|
||||
inviteeUserIDList = json['inviteeUserIDList']?.cast<String>();
|
||||
groupID = json['groupID'];
|
||||
roomID = json['roomID'];
|
||||
timeout = json['timeout'];
|
||||
mediaType = json['mediaType'];
|
||||
sessionType = json['sessionType'];
|
||||
platformID = json['platformID'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['inviterUserID'] = this.inviterUserID;
|
||||
data['inviteeUserIDList'] = this.inviteeUserIDList;
|
||||
data['groupID'] = this.groupID;
|
||||
data['roomID'] = this.roomID;
|
||||
data['timeout'] = this.timeout;
|
||||
data['mediaType'] = this.mediaType;
|
||||
data['sessionType'] = this.sessionType;
|
||||
data['platformID'] = this.platformID;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class SignalingCertificate {
|
||||
String? token;
|
||||
|
||||
String? roomID;
|
||||
|
||||
String? liveURL;
|
||||
|
||||
List<String>? busyLineUserIDList;
|
||||
|
||||
SignalingCertificate({
|
||||
this.token,
|
||||
this.roomID,
|
||||
this.liveURL,
|
||||
this.busyLineUserIDList,
|
||||
});
|
||||
|
||||
SignalingCertificate.fromJson(Map<String, dynamic> json) {
|
||||
token = json['token'];
|
||||
roomID = json['roomID'];
|
||||
liveURL = json['serverUrl'];
|
||||
busyLineUserIDList = json['busyLineUserIDList']?.cast<String>();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = Map<String, dynamic>();
|
||||
data['token'] = this.token;
|
||||
data['roomID'] = this.roomID;
|
||||
data['serverUrl'] = this.liveURL;
|
||||
data['busyLineUserIDList'] = this.busyLineUserIDList;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomCallingInfo {
|
||||
InvitationInfo? invitation;
|
||||
List<Participant>? participant;
|
||||
String? roomID;
|
||||
String? token;
|
||||
String? liveURL;
|
||||
String? groupID;
|
||||
|
||||
RoomCallingInfo({
|
||||
this.invitation,
|
||||
this.participant,
|
||||
this.roomID,
|
||||
this.token,
|
||||
this.liveURL,
|
||||
this.groupID,
|
||||
});
|
||||
|
||||
RoomCallingInfo.fromJson(Map<String, dynamic> json) {
|
||||
invitation = json['invitation'] != null ? InvitationInfo.fromJson(json['invitation']) : null;
|
||||
if (json['participant'] != null) {
|
||||
participant = <Participant>[];
|
||||
json['participant'].forEach((v) {
|
||||
participant!.add(Participant.fromJson(v));
|
||||
});
|
||||
}
|
||||
roomID = json['roomID'] ?? invitation?.roomID;
|
||||
token = json['token'];
|
||||
liveURL = json['liveURL'];
|
||||
groupID = json['groupID'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = Map<String, dynamic>();
|
||||
if (this.invitation != null) {
|
||||
data['invitation'] = this.invitation!.toJson();
|
||||
}
|
||||
if (this.participant != null) {
|
||||
data['participant'] = this.participant!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['roomID'] = this.roomID;
|
||||
data['token'] = this.token;
|
||||
data['liveURL'] = this.liveURL;
|
||||
data['groupID'] = this.groupID;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Participant {
|
||||
GroupInfo? groupInfo;
|
||||
GroupMembersInfo? groupMemberInfo;
|
||||
UserInfo? userInfo;
|
||||
|
||||
Participant({this.groupInfo, this.groupMemberInfo, this.userInfo});
|
||||
|
||||
Participant.fromJson(Map<String, dynamic> json) {
|
||||
groupInfo = json['groupInfo'] != null ? GroupInfo.fromJson(json['groupInfo']) : null;
|
||||
groupMemberInfo = json['groupMemberInfo'] != null ? GroupMembersInfo.fromJson(json['groupMemberInfo']) : null;
|
||||
userInfo = json['userInfo'] != null ? UserInfo.fromJson(json['userInfo']) : null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = Map<String, dynamic>();
|
||||
if (this.groupInfo != null) {
|
||||
data['groupInfo'] = this.groupInfo!.toJson();
|
||||
}
|
||||
if (this.groupMemberInfo != null) {
|
||||
data['groupMemberInfo'] = this.groupMemberInfo!.toJson();
|
||||
}
|
||||
if (this.userInfo != null) {
|
||||
data['userInfo'] = this.userInfo!.toJson();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSignaling {
|
||||
String? roomID;
|
||||
String? customInfo;
|
||||
|
||||
CustomSignaling({this.roomID, this.customInfo});
|
||||
|
||||
CustomSignaling.fromJson(Map<String, dynamic> json) {
|
||||
roomID = json['roomID'];
|
||||
customInfo = json['customInfo'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['roomID'] = roomID;
|
||||
data['customInfo'] = customInfo;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class UniMPInfo {
|
||||
String? id;
|
||||
String? appID;
|
||||
String? name;
|
||||
String? icon;
|
||||
String? url;
|
||||
String? md5;
|
||||
int? size;
|
||||
String? version;
|
||||
int? progress;
|
||||
|
||||
UniMPInfo({
|
||||
this.id,
|
||||
this.appID,
|
||||
this.name,
|
||||
this.icon,
|
||||
this.url,
|
||||
this.md5,
|
||||
this.size,
|
||||
this.version,
|
||||
this.progress,
|
||||
});
|
||||
|
||||
UniMPInfo.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
appID = json['appID'];
|
||||
name = json['name'];
|
||||
icon = json['icon'];
|
||||
url = json['url'];
|
||||
md5 = json['md5'];
|
||||
size = json['size'];
|
||||
version = json['version'];
|
||||
progress = json['progress'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['appID'] = appID;
|
||||
data['name'] = name;
|
||||
data['icon'] = icon;
|
||||
data['url'] = url;
|
||||
data['progress'] = progress;
|
||||
data['md5'] = md5;
|
||||
data['size'] = size;
|
||||
data['version'] = version;
|
||||
data['progress'] = progress;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class UpgradeInfoV2 {
|
||||
String? buildBuildVersion;
|
||||
String? forceUpdateVersion;
|
||||
String? forceUpdateVersionNo;
|
||||
bool? needForceUpdate;
|
||||
String? downloadURL;
|
||||
bool? buildHaveNewVersion;
|
||||
String? buildVersionNo;
|
||||
String? buildVersion;
|
||||
String? buildUpdateDescription;
|
||||
String? appKey;
|
||||
String? buildKey;
|
||||
String? buildName;
|
||||
String? buildIcon;
|
||||
String? buildFileKey;
|
||||
String? buildFileSize;
|
||||
String? appURl;
|
||||
|
||||
UpgradeInfoV2({
|
||||
this.buildBuildVersion,
|
||||
this.forceUpdateVersion,
|
||||
this.forceUpdateVersionNo,
|
||||
this.needForceUpdate,
|
||||
this.downloadURL,
|
||||
this.buildHaveNewVersion,
|
||||
this.buildVersionNo,
|
||||
this.buildVersion,
|
||||
this.buildUpdateDescription,
|
||||
this.appKey,
|
||||
this.buildKey,
|
||||
this.buildName,
|
||||
this.buildIcon,
|
||||
this.buildFileKey,
|
||||
this.buildFileSize,
|
||||
this.appURl,
|
||||
});
|
||||
|
||||
UpgradeInfoV2.fromJson(Map<String, dynamic> json) {
|
||||
buildBuildVersion = json['buildBuildVersion'];
|
||||
forceUpdateVersion = json['forceUpdateVersion'];
|
||||
forceUpdateVersionNo = json['forceUpdateVersionNo'];
|
||||
needForceUpdate = json['needForceUpdate'];
|
||||
downloadURL = json['downloadURL'];
|
||||
buildHaveNewVersion = json['buildHaveNewVersion'];
|
||||
buildVersionNo = json['buildVersionNo'];
|
||||
buildVersion = json['buildVersion'];
|
||||
buildUpdateDescription = json['buildUpdateDescription'];
|
||||
appKey = json['appKey'];
|
||||
buildKey = json['buildKey'];
|
||||
buildName = json['buildName'];
|
||||
buildIcon = json['buildIcon'];
|
||||
buildFileKey = json['buildFileKey'];
|
||||
buildFileSize = json['buildFileSize'];
|
||||
appURl = json['appURl'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['buildBuildVersion'] = buildBuildVersion;
|
||||
data['forceUpdateVersion'] = forceUpdateVersion;
|
||||
data['forceUpdateVersionNo'] = forceUpdateVersionNo;
|
||||
data['needForceUpdate'] = needForceUpdate;
|
||||
data['downloadURL'] = downloadURL;
|
||||
data['buildHaveNewVersion'] = buildHaveNewVersion;
|
||||
data['buildVersionNo'] = buildVersionNo;
|
||||
data['buildVersion'] = buildVersion;
|
||||
data['buildUpdateDescription'] = buildUpdateDescription;
|
||||
data['appKey'] = appKey;
|
||||
data['buildKey'] = buildKey;
|
||||
data['buildName'] = buildName;
|
||||
data['buildIcon'] = buildIcon;
|
||||
data['buildFileKey'] = buildFileKey;
|
||||
data['buildFileSize'] = buildFileSize;
|
||||
data['appURl'] = appURl;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
class UserFullInfo {
|
||||
String? userID;
|
||||
String? password;
|
||||
String? account;
|
||||
String? phoneNumber;
|
||||
String? areaCode;
|
||||
String? nickname;
|
||||
String? remark;
|
||||
String? englishName;
|
||||
String? faceURL;
|
||||
int? gender;
|
||||
String? mobileAreaCode;
|
||||
String? mobile;
|
||||
String? telephone;
|
||||
int? level;
|
||||
int? birth;
|
||||
String? email;
|
||||
int? order;
|
||||
int? status;
|
||||
int? allowAddFriend;
|
||||
int? allowBeep;
|
||||
int? allowVibration;
|
||||
int? forbidden;
|
||||
String? ex;
|
||||
String? station;
|
||||
int? globalRecvMsgOpt;
|
||||
bool isFriendship = false;
|
||||
bool isBlacklist = false;
|
||||
List<DepartmentInfo>? departmentList;
|
||||
|
||||
bool get isMale => gender == 1;
|
||||
|
||||
String get showName => remark?.isNotEmpty == true ? remark! : (nickname?.isNotEmpty == true ? nickname! : userID!);
|
||||
|
||||
UserFullInfo({
|
||||
this.userID,
|
||||
this.password,
|
||||
this.account,
|
||||
this.phoneNumber,
|
||||
this.areaCode,
|
||||
this.nickname,
|
||||
this.remark,
|
||||
this.englishName,
|
||||
this.faceURL,
|
||||
this.gender,
|
||||
this.mobileAreaCode,
|
||||
this.mobile,
|
||||
this.telephone,
|
||||
this.level,
|
||||
this.birth,
|
||||
this.email,
|
||||
this.order,
|
||||
this.status,
|
||||
this.allowAddFriend,
|
||||
this.allowBeep,
|
||||
this.allowVibration,
|
||||
this.forbidden,
|
||||
this.station,
|
||||
this.ex,
|
||||
this.globalRecvMsgOpt,
|
||||
this.isFriendship = false,
|
||||
this.isBlacklist = false,
|
||||
this.departmentList,
|
||||
});
|
||||
|
||||
UserFullInfo.fromJson(Map<String, dynamic> json) {
|
||||
userID = json['userID'];
|
||||
password = json['password'];
|
||||
account = json['account'];
|
||||
phoneNumber = json['phoneNumber'];
|
||||
areaCode = json['areaCode'];
|
||||
nickname = json['nickname'];
|
||||
remark = json['remark'];
|
||||
englishName = json['englishName'];
|
||||
faceURL = json['faceURL'];
|
||||
gender = json['gender'];
|
||||
mobileAreaCode = json['mobileAreaCode'];
|
||||
mobile = json['mobile'];
|
||||
telephone = json['telephone'];
|
||||
level = json['level'];
|
||||
birth = json['birth'];
|
||||
email = json['email'];
|
||||
order = json['order'];
|
||||
status = json['status'];
|
||||
allowAddFriend = json['allowAddFriend'];
|
||||
allowBeep = json['allowBeep'];
|
||||
allowVibration = json['allowVibration'];
|
||||
forbidden = json['forbidden'];
|
||||
station = json['station'];
|
||||
ex = json['ex'];
|
||||
globalRecvMsgOpt = json['globalRecvMsgOpt'];
|
||||
isFriendship = json['isFriendship'] ?? false;
|
||||
isBlacklist = json['isBlacklist'] ?? false;
|
||||
departmentList = json['departmentList'] == null
|
||||
? null
|
||||
: (json['departmentList'] as List).map((e) => DepartmentInfo.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['userID'] = userID;
|
||||
data['password'] = password;
|
||||
data['account'] = account;
|
||||
data['phoneNumber'] = phoneNumber;
|
||||
data['areaCode'] = areaCode;
|
||||
data['nickname'] = nickname;
|
||||
data['remark'] = remark;
|
||||
data['englishName'] = englishName;
|
||||
data['faceURL'] = faceURL;
|
||||
data['gender'] = gender;
|
||||
data['mobileAreaCode'] = mobileAreaCode;
|
||||
data['telephone'] = telephone;
|
||||
data['level'] = level;
|
||||
data['birth'] = birth;
|
||||
data['email'] = email;
|
||||
data['order'] = order;
|
||||
data['status'] = status;
|
||||
data['allowAddFriend'] = allowAddFriend;
|
||||
data['allowBeep'] = allowBeep;
|
||||
data['allowVibration'] = allowVibration;
|
||||
data['forbidden'] = forbidden;
|
||||
data['station'] = station;
|
||||
data['ex'] = ex;
|
||||
data['globalRecvMsgOpt'] = globalRecvMsgOpt;
|
||||
data['isFriendship'] = isFriendship;
|
||||
data['isBlacklist'] = isBlacklist;
|
||||
data['departmentList'] = departmentList?.map((e) => e.toJson()).toList();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class DepartmentInfo {
|
||||
String? departmentID;
|
||||
String? departmentFaceURL;
|
||||
String? departmentName;
|
||||
String? departmentParentID;
|
||||
int? departmentOrder;
|
||||
int? departmentDepartmentType;
|
||||
String? departmentRelatedGroupID;
|
||||
int? departmentCreateTime;
|
||||
int? memberOrder;
|
||||
String? memberPosition;
|
||||
int? memberLeader;
|
||||
int? memberStatus;
|
||||
int? memberEntryTime;
|
||||
int? memberTerminationTime;
|
||||
int? memberCreateTime;
|
||||
|
||||
DepartmentInfo(
|
||||
{this.departmentID,
|
||||
this.departmentFaceURL,
|
||||
this.departmentName,
|
||||
this.departmentParentID,
|
||||
this.departmentOrder,
|
||||
this.departmentDepartmentType,
|
||||
this.departmentRelatedGroupID,
|
||||
this.departmentCreateTime,
|
||||
this.memberOrder,
|
||||
this.memberPosition,
|
||||
this.memberLeader,
|
||||
this.memberStatus,
|
||||
this.memberEntryTime,
|
||||
this.memberTerminationTime,
|
||||
this.memberCreateTime});
|
||||
|
||||
DepartmentInfo.fromJson(Map<String, dynamic> json) {
|
||||
departmentID = json['departmentID'];
|
||||
departmentFaceURL = json['departmentFaceURL'];
|
||||
departmentName = json['departmentName'];
|
||||
departmentParentID = json['departmentParentID'];
|
||||
departmentOrder = json['departmentOrder'];
|
||||
departmentDepartmentType = json['departmentDepartmentType'];
|
||||
departmentRelatedGroupID = json['departmentRelatedGroupID'];
|
||||
departmentCreateTime = json['departmentCreateTime'];
|
||||
memberOrder = json['memberOrder'];
|
||||
memberPosition = json['memberPosition'];
|
||||
memberLeader = json['memberLeader'];
|
||||
memberStatus = json['memberStatus'];
|
||||
memberEntryTime = json['memberEntryTime'];
|
||||
memberTerminationTime = json['memberTerminationTime'];
|
||||
memberCreateTime = json['memberCreateTime'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['departmentID'] = departmentID;
|
||||
data['departmentFaceURL'] = departmentFaceURL;
|
||||
data['departmentName'] = departmentName;
|
||||
data['departmentParentID'] = departmentParentID;
|
||||
data['departmentOrder'] = departmentOrder;
|
||||
data['departmentDepartmentType'] = departmentDepartmentType;
|
||||
data['departmentRelatedGroupID'] = departmentRelatedGroupID;
|
||||
data['departmentCreateTime'] = departmentCreateTime;
|
||||
data['memberOrder'] = memberOrder;
|
||||
data['memberPosition'] = memberPosition;
|
||||
data['memberLeader'] = memberLeader;
|
||||
data['memberStatus'] = memberStatus;
|
||||
data['memberEntryTime'] = memberEntryTime;
|
||||
data['memberTerminationTime'] = memberTerminationTime;
|
||||
data['memberCreateTime'] = memberCreateTime;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:azlistview/azlistview.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ISUserInfo extends UserFullInfo with ISuspensionBean {
|
||||
String? tagIndex;
|
||||
String? pinyin;
|
||||
String? shortPinyin;
|
||||
String? namePinyin;
|
||||
|
||||
ISUserInfo.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
||||
tagIndex = json['tagIndex'];
|
||||
pinyin = json['pinyin'];
|
||||
shortPinyin = json['shortPinyin'];
|
||||
namePinyin = json['namePinyin'];
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
var map = super.toJson();
|
||||
map['tagIndex'] = tagIndex;
|
||||
map['pinyin'] = pinyin;
|
||||
map['shortPinyin'] = shortPinyin;
|
||||
map['namePinyin'] = namePinyin;
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String getSuspensionTag() {
|
||||
return tagIndex!;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return json.encode(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
class ImageRes {
|
||||
ImageRes._();
|
||||
|
||||
static const _dir = "assets/images";
|
||||
|
||||
static const voiceWhiteAnim = "assets/anim/voice_white.json";
|
||||
static const voiceBlueAnim = "assets/anim/voice_blue.json";
|
||||
static const voiceBlackAnim = "assets/anim/voice_black.json";
|
||||
|
||||
static const splashLogo = "$_dir/ic_splash_logo.webp";
|
||||
static const loginLogo = "$_dir/ic_login_logo.webp";
|
||||
static const eyeOpen = "$_dir/ic_eye_open.webp";
|
||||
static const eyeClose = "$_dir/ic_eye_close.webp";
|
||||
static const clearText = "$_dir/ic_clear_text.webp";
|
||||
static const homeTab1Nor = "$_dir/ic_home_tab1_nor.webp";
|
||||
static const homeTab2Nor = "$_dir/ic_home_tab2_nor.webp";
|
||||
static const homeTab3Nor = "$_dir/ic_home_tab3_nor.webp";
|
||||
static const homeTab4Nor = "$_dir/ic_home_tab4_nor.webp";
|
||||
static const homeTab1Sel = "$_dir/ic_home_tab1_sel.webp";
|
||||
static const homeTab2Sel = "$_dir/ic_home_tab2_sel.webp";
|
||||
static const homeTab3Sel = "$_dir/ic_home_tab3_sel.webp";
|
||||
static const homeTab4Sel = "$_dir/ic_home_tab4_sel.webp";
|
||||
static const searchGrey = "$_dir/ic_search_gery.webp";
|
||||
static const callBack = "$_dir/ic_call_black.webp";
|
||||
static const addBlack = "$_dir/ic_add_black.webp";
|
||||
static const syncFailed = "$_dir/ic_sync_failed.webp";
|
||||
static const notDisturb = "$_dir/ic_not_disturb.webp";
|
||||
static const backBlack = "$_dir/ic_back_black.webp";
|
||||
static const moreBlack = "$_dir/ic_more_black.webp";
|
||||
static const openEmoji = "$_dir/ic_open_emoji.webp";
|
||||
static const openKeyboard = "$_dir/ic_open_keyboard.webp";
|
||||
static const openToolbox = "$_dir/ic_open_toolbox.webp";
|
||||
static const openVoice = "$_dir/ic_open_voice.webp";
|
||||
static const toolboxAlbum = "$_dir/ic_toolbox_album.webp";
|
||||
static const toolboxCall = "$_dir/ic_toolbox_call.webp";
|
||||
static const toolboxCamera = "$_dir/ic_toolbox_camera.webp";
|
||||
static const toolboxCard = "$_dir/ic_toolbox_card.webp";
|
||||
static const toolboxFile = "$_dir/ic_toolbox_file.webp";
|
||||
static const toolboxLocation = "$_dir/ic_toolbox_location.webp";
|
||||
static const delQuote = "$_dir/ic_del_quote.webp";
|
||||
static const favoriteTab = "$_dir/ic_favorite_tab.webp";
|
||||
static const emojiTab = "$_dir/ic_emoji_tab.webp";
|
||||
static const addFavorite = "$_dir/ic_add_favorite.webp";
|
||||
static const videoCallMsg = "$_dir/ic_video_call_msg.webp";
|
||||
static const voiceCallMsg = "$_dir/ic_voice_call_msg.webp";
|
||||
static const fileExcel = "$_dir/ic_file_excel.webp";
|
||||
static const filePdf = "$_dir/ic_file_pdf.webp";
|
||||
static const filePpt = "$_dir/ic_file_ppt.webp";
|
||||
static const fileUnknown = "$_dir/ic_file_unknown.webp";
|
||||
static const fileWord = "$_dir/ic_file_word.webp";
|
||||
static const fileZip = "$_dir/ic_file_zip.webp";
|
||||
static const fileMask = "$_dir/ic_file_mask.webp";
|
||||
static const progressGoing = "$_dir/ic_progress_going.webp";
|
||||
static const progressPause = "$_dir/ic_progress_pause.webp";
|
||||
static const progressPlay = "$_dir/ic_progress_play.webp";
|
||||
static const videoPause = "$_dir/ic_video_pause.webp";
|
||||
static const voiceWhite = "$_dir/ic_voice_white.webp";
|
||||
static const voiceBlue = "$_dir/ic_voice_blue.webp";
|
||||
static const voiceBlack = "$_dir/ic_voice_black.webp";
|
||||
static const notice = "$_dir/ic_notice.webp";
|
||||
static const radioNor = "$_dir/ic_radio_nor.webp";
|
||||
static const radioSel = "$_dir/ic_radio_sel.webp";
|
||||
static const failedToResend = "$_dir/ic_failed_resend.webp";
|
||||
static const menuAddFace = "$_dir/ic_menu_add_face.webp";
|
||||
static const menuCopy = "$_dir/ic_menu_copy.webp";
|
||||
static const menuDel = "$_dir/ic_menu_del.webp";
|
||||
static const menuForward = "$_dir/ic_menu_forward.webp";
|
||||
static const menuMulti = "$_dir/ic_menu_multi.webp";
|
||||
static const menuReply = "$_dir/ic_menu_reply.webp";
|
||||
static const menuRevoke = "$_dir/ic_menu_revoke.webp";
|
||||
static const scrollDown = "$_dir/ic_scroll_down.webp";
|
||||
static const sendMessage = "$_dir/ic_send_message.webp";
|
||||
static const multiBoxDel = "$_dir/ic_multi_box_del.webp";
|
||||
static const multiBoxForward = "$_dir/ic_multi_box_forward.webp";
|
||||
static const groupCallHitArrow = "$_dir/ic_group_call_hint_arrow.webp";
|
||||
static const moreCallMember = "$_dir/ic_more_call_member.webp";
|
||||
static const closeGroupNotice = "$_dir/ic_close_notice.webp";
|
||||
static const videoMeeting = "$_dir/ic_video_meeting.webp";
|
||||
static const nextBlue = "$_dir/ic_next_blue.webp";
|
||||
static const callVoice = "$_dir/ic_call_voice.webp";
|
||||
static const callVideo = "$_dir/ic_call_video.webp";
|
||||
static const mineHeaderBg = "$_dir/ic_mine_header_bg.webp";
|
||||
static const mineCopy = "$_dir/ic_mine_copy.webp";
|
||||
static const mineQr = "$_dir/ic_mine_qr.webp";
|
||||
static const rightArrow = "$_dir/ic_right_arrow.webp";
|
||||
static const myInfo = "$_dir/ic_mine_my_info.webp";
|
||||
static const workingCircle = "$_dir/ic_working_circle.webp";
|
||||
static const accountSetup = "$_dir/ic_mine_account_setup.webp";
|
||||
static const aboutUs = "$_dir/ic_mine_about_us.webp";
|
||||
static const logout = "$_dir/ic_mine_logout.webp";
|
||||
static const myFriend = "$_dir/ic_my_friend.webp";
|
||||
static const myGroup = "$_dir/ic_my_group.webp";
|
||||
static const newFriend = "$_dir/ic_new_friend.webp";
|
||||
static const newGroup = "$_dir/ic_new_group.webp";
|
||||
static const searchBlack = "$_dir/ic_search_black.webp";
|
||||
static const addContacts = "$_dir/ic_add_contacts.webp";
|
||||
static const tree = "$_dir/ic_tree.webp";
|
||||
static const scanBlue = "$_dir/ic_scan_blue.webp";
|
||||
static const addFriendBlue = "$_dir/ic_add_friend_blue.webp";
|
||||
static const createGroupBlue = "$_dir/ic_create_group_blue.webp";
|
||||
static const addGroupBLue = "$_dir/ic_add_group_blue.webp";
|
||||
static const createGroupTime = "$_dir/ic_create_group_time.webp";
|
||||
static const moreMembers = "$_dir/ic_more_members.webp";
|
||||
static const searchPersonIcon = "$_dir/ic_search_person.webp";
|
||||
static const searchGroupIcon = "$_dir/ic_search_group.webp";
|
||||
static const audioAndVideoCall = "$_dir/ic_voice_video_call.webp";
|
||||
static const message = "$_dir/ic_message.webp";
|
||||
static const checked = "$_dir/ic_checked.webp";
|
||||
static const blacklistEmpty = "$_dir/ic_blacklist_empty.webp";
|
||||
static const chatSearch = "$_dir/ic_chat_search.webp";
|
||||
static const chatSearchPic = "$_dir/ic_chat_pic.webp";
|
||||
static const chatSearchVideo = "$_dir/ic_chat_video.webp";
|
||||
static const chatSearchFile = "$_dir/ic_chat_file.webp";
|
||||
static const addFriendTobeGroup = "$_dir/ic_add_friend_tobe_group.webp";
|
||||
static const editAvatar = "$_dir/ic_edit_avatar.webp";
|
||||
static const editName = "$_dir/ic_edit_name.webp";
|
||||
static const addMember = "$_dir/ic_member_add.webp";
|
||||
static const delMember = "$_dir/ic_member_del.webp";
|
||||
static const expandUpArrow = "$_dir/ic_expand.webp";
|
||||
static const indexBarBg = "$_dir/ic_index_bar_bubble_white.png";
|
||||
static const sendRequests = "$_dir/ic_send_requests.webp";
|
||||
static const cameraGray = "$_dir/ic_camera_gray.webp";
|
||||
static const searchNotFound = "$_dir/ic_search_not_found.webp";
|
||||
static const popMenuAddFriend = "$_dir/ic_popmenu_add_friend.webp";
|
||||
static const popMenuAddGroup = "$_dir/ic_popmenu_add_group.webp";
|
||||
static const popMenuCreateGroup = "$_dir/ic_popmenu_create_group.webp";
|
||||
static const popMenuScan = "$_dir/ic_popmenu_scan.webp";
|
||||
static const popMenuVideoMeeting = "$_dir/ic_popmenu_video_meeting.webp";
|
||||
static const workingCircleHeaderBg = "$_dir/ic_working_circle_header_bg.webp";
|
||||
static const workingCircleMessage = "$_dir/ic_working_circle_message.webp";
|
||||
static const workingCirclePublish = "$_dir/ic_working_circle_publish.webp";
|
||||
static const workingCircleEmpty = "$_dir/ic_working_circle_empty.webp";
|
||||
static const visibleUser = "$_dir/ic_visible_user.webp";
|
||||
static const moreOp = "$_dir/ic_more_op.webp";
|
||||
static const whoCanWatch = "$_dir/ic_who_can_watch.webp";
|
||||
static const remindWhoToWatch = "$_dir/ic_remind_who_to_watch.webp";
|
||||
static const circle = "$_dir/ic_circle.webp";
|
||||
static const liveClose = "$_dir/ic_live_close.webp";
|
||||
static const liveHangUp = "$_dir/ic_live_hang_up.webp";
|
||||
static const liveMicOff = "$_dir/ic_live_mic_off.webp";
|
||||
static const liveMicOn = "$_dir/ic_live_mic_on.webp";
|
||||
static const livePicUp = "$_dir/ic_live_pick_up.webp";
|
||||
static const liveSpeakerOff = "$_dir/ic_live_speaker_off.webp";
|
||||
static const liveSpeakerOn = "$_dir/ic_live_speaker_on.webp";
|
||||
static const liveCameraOn = "$_dir/ic_live_camera_on.webp";
|
||||
static const liveCameraOff = "$_dir/ic_live_camera_off.webp";
|
||||
static const liveSwitchCamera = "$_dir/ic_live_switch_camera.webp";
|
||||
static const liveBg = "$_dir/ic_live_bg.webp";
|
||||
static const meetingBook = "$_dir/ic_meeting_book.webp";
|
||||
static const meetingJoin = "$_dir/ic_meeting_join.webp";
|
||||
static const meetingQuickStart = "$_dir/ic_meeting_quick_start.webp";
|
||||
static const meetingForward = "$_dir/ic_meeting_forward.webp";
|
||||
static const meetingPerson = "$_dir/ic_meeting_person.webp";
|
||||
static const meetingTrumpet = "$_dir/ic_meeting_trumpet.webp";
|
||||
static const meetingScreenShareOn = "$_dir/ic_meeting_screen_share_on.webp";
|
||||
static const meetingScreenShareOff = "$_dir/ic_meeting_screen_share_off.webp";
|
||||
static const meetingSetting = "$_dir/ic_meeting_setting.webp";
|
||||
static const meetingMore = "$_dir/ic_meeting_more.webp";
|
||||
static const meetingMembers = "$_dir/ic_meeting_members.webp";
|
||||
static const meetingExpandArrow = "$_dir/ic_meeting_expand_arrow.webp";
|
||||
static const meetingEar = "$_dir/ic_meeting_ear.webp";
|
||||
static const meetingCameraOffGray = "$_dir/ic_meeting_camera_off_gray.webp";
|
||||
static const meetingCameraOffWhite = "$_dir/ic_meeting_camera_off_white.webp";
|
||||
static const meetingCameraOnGray = "$_dir/ic_meeting_camera_on_gray.webp";
|
||||
static const meetingCameraOnWhite = "$_dir/ic_meeting_camera_on_white.webp";
|
||||
static const meetingMicOffGray = "$_dir/ic_meeting_mic_off_gray.webp";
|
||||
static const meetingMicOffWhite = "$_dir/ic_meeting_mic_off_white.webp";
|
||||
static const meetingMicOnGray = "$_dir/ic_meeting_mic_on_gray.webp";
|
||||
static const meetingMicOnWhite = "$_dir/ic_meeting_mic_on_white.webp";
|
||||
static const meetingMembersPin = "$_dir/ic_meeting_members_pin.webp";
|
||||
static const meetingVideo = "$_dir/ic_meeting_video.webp";
|
||||
static const meetingAllSeeHim = "$_dir/ic_meeting_see_him.webp";
|
||||
static const meetingRotateScreen = "$_dir/ic_meeting_rotate_screen.webp";
|
||||
static const pictureError = "$_dir/ic_picture_error.webp";
|
||||
static const downArrow = "$_dir/ic_down_arrow.webp";
|
||||
static const warn = "$_dir/ic_warn.webp";
|
||||
static const issueNotice = "$_dir/ic_issue_notice.webp";
|
||||
static const tagGroup = "$_dir/ic_tag_group.webp";
|
||||
static const addTagMember = "$_dir/ic_add_tag_member.webp";
|
||||
static const toolboxAlbum1 = "$_dir/ic_toolbox_album1.webp";
|
||||
static const toolboxCamera1 = "$_dir/ic_toolbox_camera1.webp";
|
||||
static const toolboxCard1 = "$_dir/ic_toolbox_card1.webp";
|
||||
static const toolboxFile1 = "$_dir/ic_toolbox_file1.webp";
|
||||
static const toolboxLocation1 = "$_dir/ic_toolbox_location1.webp";
|
||||
static const toolboxDirectionalMessage = "$_dir/ic_toolbox_directional_message.webp";
|
||||
static const tagIcon = "$_dir/ic_tag_icon.webp";
|
||||
static const momentsIcon = "$_dir/ic_moments.webp";
|
||||
static const forwardIcon = '$_dir/ic_forward_icon.webp';
|
||||
static const saveIcon = '$_dir/ic_save_icon.webp';
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
const Map<String, String> en_US = {
|
||||
"welcome": "Welcome to OpenIM",
|
||||
"userID": "user id",
|
||||
"phoneNumber": "Phone Number",
|
||||
"plsEnterPhoneNumber": "Please enter your phone number",
|
||||
"plsEnterEmail": "Please enter your email",
|
||||
"password": "Password",
|
||||
"plsEnterPassword": "Please enter your password",
|
||||
"account": "Account",
|
||||
"plsEnterAccount": "Please enter your account",
|
||||
"forgetPassword": "Forgot Password",
|
||||
"verificationCodeLogin": "Verification Code Login",
|
||||
"login": "Login",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
"loginNow": "Login now",
|
||||
"registerNow": "Register now",
|
||||
'lockPwdErrorHint': 'Error %s times',
|
||||
"newUserRegister": "New User Registration",
|
||||
"verificationCode": "Verification Code",
|
||||
"sendVerificationCode": "Send Verification Code",
|
||||
'resendVerificationCode': 'Resend Verification Code',
|
||||
'verificationCodeTimingReminder': 'Get Verification Code Again in %s seconds',
|
||||
"defaultVerificationCode": "(Default Verification Code: %s)",
|
||||
"plsEnterVerificationCode": "Please enter your verification code",
|
||||
"invitationCode": "Invitation Code",
|
||||
"plsEnterInvitationCode": "Please enter your invitation code %s",
|
||||
"optional": "Optional",
|
||||
"nextStep": "Next Step",
|
||||
"plsEnterRightPhone": "Please enter a valid phone number",
|
||||
'plsEnterRightEmail': "Please enter a valid email address",
|
||||
'plsEnterRightAccount': 'Please enter a valid account',
|
||||
'plsEnterRightPhoneOrEmail': 'Please enter a valid phone or email address',
|
||||
"enterVerificationCode": "Enter %s verification code",
|
||||
"setPassword": "Set Password",
|
||||
"plsConfirmPasswordAgain": "Please confirm your password again",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"wrongPasswordFormat": "Invalid password format",
|
||||
"plsCompleteInfo": "Please complete your personal information",
|
||||
"plsEnterYourNickname": "Please enter your nickname",
|
||||
"setInfo": "Set Information",
|
||||
"loginPwdFormat": "6 to 20 characters, at least including numbers and letters",
|
||||
"passwordLogin": "Password Login",
|
||||
'through': 'Through %s',
|
||||
"home": "OpenIM",
|
||||
"contacts": "Contacts",
|
||||
"workbench": "Discover",
|
||||
"mine": "Mine",
|
||||
"draftText": "Draft",
|
||||
'everyone': 'Everyone',
|
||||
"you": "You",
|
||||
'someoneMentionYou': 'Someone @ you',
|
||||
"groupAc": "GA",
|
||||
"createGroupNtf": "%s created a group",
|
||||
"editGroupInfoNtf": "%s modified group information",
|
||||
"quitGroupNtf": "%s quit the group",
|
||||
"invitedJoinGroupNtf": "%s invited %s to join the group",
|
||||
"kickedGroupNtf": "%s was removed from the group by %s",
|
||||
"joinGroupNtf": "%s joined the group",
|
||||
"dismissGroupNtf": "%s dismissed the group",
|
||||
"transferredGroupNtf": "%s transferred group management authority to %s",
|
||||
"muteMemberNtf": "%s was muted by %s for %s",
|
||||
"muteCancelMemberNtf": "%s had their mute canceled by %s",
|
||||
"muteGroupNtf": "%s enabled group mute",
|
||||
"muteCancelGroupNtf": "%s disabled group mute",
|
||||
"friendAddedNtf": "You are now friends and can start chatting",
|
||||
"openPrivateChatNtf": "Enable self-destructing messages",
|
||||
"closePrivateChatNtf": "Disable self-destructing messages",
|
||||
"memberInfoChangedNtf": "%s edited their group member profile",
|
||||
"unsupportedMessage": "Unsupported message type",
|
||||
"picture": "Picture",
|
||||
"video": "Video",
|
||||
"voice": "Voice",
|
||||
"location": "Location",
|
||||
"file": "File",
|
||||
"carte": "Card",
|
||||
"emoji": "Custom Emoji",
|
||||
"chatRecord": "Chat Records",
|
||||
"revokeMsg": "withdrew a message",
|
||||
"aRevokeBMsg": "%s withdrew %s's message",
|
||||
'blockedByFriendHint': 'The message has been sent but rejected by the recipient',
|
||||
'deletedByFriendHint': '%s enabled friend verification. You are not their friend yet.',
|
||||
'sendFriendVerification': 'Send Friend Verification',
|
||||
'removedFromGroupHint': 'You have been removed from the group',
|
||||
'groupDisbanded': 'The group has been disbanded',
|
||||
'search': 'Search',
|
||||
'synchronizing': 'Synchronizing',
|
||||
'syncFailed': 'Sync Failed',
|
||||
'connecting': 'Connecting',
|
||||
'connectionFailed': 'Connection Failed',
|
||||
'top': 'Top',
|
||||
'cancelTop': 'Cancel Top',
|
||||
'markHasRead': 'Mark as Read',
|
||||
'delete': 'Delete',
|
||||
'nPieces': '%s pieces',
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"phoneOnline": "Phone",
|
||||
"pcOnline": "Pc",
|
||||
"webOnline": "Web",
|
||||
"webMiniOnline": "Mini Program",
|
||||
"upgradeFind": "Discover New Version",
|
||||
"upgradeVersion": "A new version %s is available. Your current version is %s.",
|
||||
"upgradeDescription": "Update Description:",
|
||||
"upgradeIgnore": "Ignore",
|
||||
"upgradeLater": "Later",
|
||||
"upgradeNow": "Update Now",
|
||||
'upgradePermissionTips': 'Please allow notification permission',
|
||||
'inviteYouCall': '%s invited you to %s',
|
||||
'rejectCall': 'Reject',
|
||||
'acceptCall': 'Accept',
|
||||
"callVoice": "Voice Call",
|
||||
"callVideo": "Video Call",
|
||||
"sentSuccessfully": "Sent Successfully",
|
||||
"copySuccessfully": "Copied Successfully",
|
||||
"day": "day",
|
||||
"hour": "hour",
|
||||
"hours": "hours",
|
||||
"minute": "minute",
|
||||
"seconds": "seconds",
|
||||
"cancel": "Cancel",
|
||||
"determine": "OK",
|
||||
"toolboxAlbum": "Album",
|
||||
"toolboxCall": "Video Call",
|
||||
"toolboxCamera": "Camera",
|
||||
"toolboxCard": "Card",
|
||||
"toolboxFile": "File",
|
||||
"toolboxLocation": "Location",
|
||||
"toolboxDirectionalMessage": "Dire-Message",
|
||||
"send": "Send",
|
||||
"holdTalk": "Hold to Talk",
|
||||
"releaseToSend": "Release to Send",
|
||||
"releaseToSendSwipeUpToCancel": "Release to Send, Swipe Up to Cancel",
|
||||
"liftFingerToCancelSend": "Lift Finger to Cancel Send",
|
||||
"callDuration": "Call Duration: %s",
|
||||
"cancelled": "Cancelled",
|
||||
"cancelledByCaller": "Cancelled by Caller",
|
||||
"rejectedByCaller": "Rejected by Caller",
|
||||
"callTimeout": "Call Timeout",
|
||||
"rejected": "Rejected",
|
||||
"networkAnomaly": "Network Anomaly",
|
||||
'forwardMaxCountHint': 'you can only forward up to 20 messages.',
|
||||
"typing": "Typing...",
|
||||
"addSuccessfully": "Added Successfully",
|
||||
"addFailed": "Add Failed",
|
||||
"setSuccessfully": "Set Successfully",
|
||||
'callingBusy': 'You are already on a call or in a meeting and cannot perform this operation!',
|
||||
'groupCallHint': 'The group is currently in a call. Are you sure you want to join the ongoing call?',
|
||||
'joinIn': 'Join',
|
||||
'menuCopy': 'Copy',
|
||||
'menuDel': 'Delete',
|
||||
'menuForward': 'Forward',
|
||||
'menuReply': 'Reply',
|
||||
'menuMulti': 'Select Multiple',
|
||||
'menuRevoke': 'Revoke',
|
||||
'menuAdd': 'Add',
|
||||
'nMessage': '%s New Messages',
|
||||
'plsSelectLocation': 'Please select a location',
|
||||
'groupAudioCallHint': '%s people in a voice call',
|
||||
'groupVideoCallHint': '%s people in a video call',
|
||||
'reEdit': 'Re-edit',
|
||||
'playSpeed': 'Playback Speed',
|
||||
'download': 'Download',
|
||||
"googleMap": "Google Maps",
|
||||
"appleMap": "Apple Maps",
|
||||
"baiduMap": "Baidu Maps",
|
||||
"amapMap": "AMap",
|
||||
"tencentMap": "Tencent Maps",
|
||||
"offlineMeetingMessage": "You received a meeting invitation message",
|
||||
"offlineMessage": "You received a new message",
|
||||
"offlineCallMessage": "You received a call invitation message",
|
||||
"logoutHint": "Are you sure you want to log out?",
|
||||
"myInfo": "My Info",
|
||||
"workingCircle": "Work Circle",
|
||||
"accountSetup": "Account Setup",
|
||||
"aboutUs": "About Us",
|
||||
"logout": "Log Out",
|
||||
"qrcode": "QR Code",
|
||||
"qrcodeHint": "Scan the QR code below to add me as a friend",
|
||||
"favoriteFace": "Favorite Emojis",
|
||||
"favoriteManage": "Manage",
|
||||
"favoriteCount": "%s Favorites",
|
||||
"favoriteDel": "Delete (%s)",
|
||||
"hasRead": "Read",
|
||||
"unread": "Unread",
|
||||
"nPersonUnRead": "%s unread",
|
||||
"allRead": "All Read",
|
||||
"messageRecipientList": "Message Recipient List",
|
||||
"hasReadCount": "Read (%s)",
|
||||
"unreadCount": "Unread (%s)",
|
||||
"newFriend": "New Friend",
|
||||
"newGroup": "New Group",
|
||||
"newGroupRequest": "Group notification",
|
||||
"myFriend": "My Friends",
|
||||
"myGroup": "My Groups",
|
||||
"add": "Add",
|
||||
"scan": "Scan",
|
||||
"scanHint": "Scan business card QR code",
|
||||
"addFriend": "Add Friend",
|
||||
"addFriendHint": "Add by ID",
|
||||
"createGroup": "Create Group",
|
||||
"createGroupHint": "Create a group, fully utilize OpenIM",
|
||||
"addGroup": "Add Group",
|
||||
"addGroupHint": "Ask the admin or members for the ID",
|
||||
"searchIDAddFriend": "Search ID to Add Friend",
|
||||
"searchIDAddGroup": "Search ID to Add Group",
|
||||
"searchIDIs": "ID: %s",
|
||||
"searchPhoneIs": "Phone: %s",
|
||||
"searchEmailIs": "Email: %s",
|
||||
"searchNicknameIs": "Nickname: %s",
|
||||
"searchGroupNicknameIs": "Group Nickname: %s",
|
||||
"noFoundUser": "User not found",
|
||||
"noFoundGroup": "Group not found",
|
||||
'joinGroupDate': 'Join Group Date',
|
||||
'joinGroupMethod': 'Join Group Method',
|
||||
'byInviteJoinGroup': 'Joined by invitation from %s',
|
||||
'byIDJoinGroup': 'Joined by searching group ID',
|
||||
'byQrcodeJoinGroup': 'Joined via group QR code',
|
||||
'groupID': 'Group ID',
|
||||
'setAsAdmin': 'Set as Admin',
|
||||
'setMute': 'Set Mute',
|
||||
'organizationInfo': 'Organization Info',
|
||||
'organization': 'Organization',
|
||||
'department': 'Department',
|
||||
'position': 'Position',
|
||||
'personalInfo': 'Personal Info',
|
||||
'viewDynamics': 'View Dynamics',
|
||||
'audioAndVideoCall': 'Call',
|
||||
'sendMessage': 'Message',
|
||||
'avatar': 'Avatar',
|
||||
'name': 'Name',
|
||||
'nickname': 'Nickname',
|
||||
'gender': 'Gender',
|
||||
'englishName': 'English Name',
|
||||
'birthDay': 'Birthday',
|
||||
'tel': 'Tel',
|
||||
'mobile': 'Mobile',
|
||||
'email': 'Email',
|
||||
'man': 'Male',
|
||||
'woman': 'Female',
|
||||
'friendSetup': 'Friend Setup',
|
||||
'setupRemark': 'Set Remark',
|
||||
'recommendToFriend': 'Recommend to Friend',
|
||||
'addToBlacklist': 'Add to Blacklist',
|
||||
'unfriend': 'Unfriend',
|
||||
"areYouSureDelFriend": "Are you sure you want to delete this friend?",
|
||||
"areYouSureAddBlacklist": "Are you sure you want to add this friend to the blacklist?",
|
||||
"remark": "Remark",
|
||||
"save": "Save",
|
||||
"saveSuccessfully": "Saved Successfully",
|
||||
"saveFailed": "Save Failed",
|
||||
"groupVerification": "Group Verification",
|
||||
"friendVerification": "Friend Verification",
|
||||
"sendEnterGroupApplication": "Send Enter Group Application",
|
||||
"sendToBeFriendApplication": "Send Friend Request",
|
||||
"sendSuccessfully": "Sent Successfully",
|
||||
"sendFailed": "Send Failed",
|
||||
'canNotAddFriends': 'This user has set not to be added!',
|
||||
"mutedAll": "Mute All",
|
||||
"tenMinutes": "10 Minutes",
|
||||
"oneHour": "1 Hour",
|
||||
"twelveHours": "12 Hours",
|
||||
"oneDay": "1 Day",
|
||||
"custom": "Custom",
|
||||
'unmute': 'Unmute',
|
||||
"youMuted": 'You have been muted',
|
||||
"groupMuted": 'Group Muted',
|
||||
"notDisturbMode": 'Do Not Disturb Mode',
|
||||
"allowRing": 'New Message Sound',
|
||||
"allowVibrate": 'New Message Vibration',
|
||||
'forbidAddMeToFriend': 'Disallow Add Me as a Friend',
|
||||
"blacklist": 'Contacts Blacklist',
|
||||
"unlockSettings": 'Unlock Settings',
|
||||
"changePassword": 'Change Password',
|
||||
"clearChatHistory": 'Clear Chat History',
|
||||
"confirmClearChatHistory": "Confirm Clearing All Chat History?",
|
||||
"languageSetup": "Language Setup",
|
||||
"language": "Language",
|
||||
"english": "English",
|
||||
"chinese": "简体中文",
|
||||
"followSystem": "Follow System",
|
||||
"blacklistEmpty": "No Blacklisted Contacts",
|
||||
"remove": "Remove",
|
||||
'fingerprint': 'Fingerprint',
|
||||
'gesture': 'Gesture',
|
||||
'biometrics': 'Biometrics',
|
||||
'plsEnterPwd': 'Please Enter Password',
|
||||
'plsEnterOldPwd': 'Please Enter Old Password',
|
||||
'plsEnterNewPwd': 'Please Enter New Password',
|
||||
'plsConfirmNewPwd': 'Please Confirm New Password',
|
||||
'reset': 'Reset',
|
||||
'oldPwd': 'Old Password:',
|
||||
'newPwd': 'New Password:',
|
||||
'confirmNewPwd': 'Confirm Password:',
|
||||
'plsEnterConfirmPwd': 'Please Enter Confirmation Password',
|
||||
'twicePwdNoSame': 'The entered passwords do not match',
|
||||
'changedSuccessfully': 'Changed Successfully',
|
||||
'checkNewVersion': 'Check for New Version',
|
||||
'chatContent': 'Chat Content',
|
||||
'topContacts': 'Top Contacts',
|
||||
'messageNotDisturb': 'Message Do Not Disturb',
|
||||
'messageNotDisturbHint': 'Receive messages but no notification',
|
||||
"burnAfterReading": "Burn After Reading",
|
||||
"timeSet": "Time Settings",
|
||||
'setChatBackground': 'Set Chat Background',
|
||||
'setDefaultBackground': 'Set Default Background',
|
||||
'fontSize': 'Font Size',
|
||||
"little": "Small",
|
||||
"standard": "Standard",
|
||||
"big": "Large",
|
||||
'thirtySeconds': '30 Seconds',
|
||||
'fiveMinutes': '5 Minutes',
|
||||
"clearAll": "Clear All",
|
||||
"clearSuccessfully": "Cleared Successfully",
|
||||
"groupChatSetup": "Group Settings",
|
||||
"viewAllGroupMembers": "View All Group Members (%s)",
|
||||
"groupManage": "Management",
|
||||
"myGroupMemberNickname": "My Nickname",
|
||||
"topChat": "Pin Chat",
|
||||
"muteAllMember": "Mute All",
|
||||
"exitGroup": "Exit Group",
|
||||
"dismissGroup": "Dismiss Group",
|
||||
"dismissGroupHint": "Once the group is dismissed, you will lose contact with group members",
|
||||
"quitGroupHint": "After quitting the group, you will no longer receive messages from this group.",
|
||||
'joinGroupSet': 'Group Verification',
|
||||
'allowAnyoneJoinGroup': 'Allow Anyone',
|
||||
'inviteNotVerification': 'No Verification for Member Invitation',
|
||||
'needVerification': 'Verification Required',
|
||||
'addMember': 'Add',
|
||||
'delMember': 'Remove',
|
||||
'groupOwner': 'Group Owner',
|
||||
'groupAdmin': 'Group Admin',
|
||||
'notAllowSeeMemberProfile': "Disallow View Members' Profiles",
|
||||
'notAllAddMemberToBeFriend': 'Disallow Add Members as Friends',
|
||||
'transferGroupOwnerRight': 'Transfer Group Owner',
|
||||
'groupName': 'Group Name',
|
||||
'groupAcPermissionTips': 'Only owner and admin can edit',
|
||||
'plsEnterGroupAc': 'Please enter group announcement',
|
||||
'edit': 'Edit',
|
||||
'publish': 'Publish',
|
||||
'groupMember': 'Group Members',
|
||||
'selectedPeopleCount': 'Selected (%s)',
|
||||
'confirmSelectedPeople': 'Confirm (%s/%s)',
|
||||
'confirm': 'Confirm',
|
||||
"confirmTransferGroupToUser": "Are you sure you want to transfer group ownership to: %s?",
|
||||
"removeGroupMember": "Remove Group Member",
|
||||
"searchNotResult": "No results found",
|
||||
"groupQrcode": "Group QR Code",
|
||||
"groupQrcodeHint": "Scan the QR code below to join the group",
|
||||
"approved": "Approved",
|
||||
"accept": "Accept",
|
||||
"reject": "Reject",
|
||||
"waitingForVerification": "Waiting",
|
||||
"rejectSuccessfully": "Rejected successfully",
|
||||
"rejectFailed": "Rejection failed",
|
||||
"applyJoin": "Apply to join",
|
||||
"enterGroup": "Enter Group",
|
||||
"applyReason": "Reason: %s",
|
||||
'invite': 'Invite',
|
||||
'sourceFrom': 'Source: %s',
|
||||
'byMemberInvite': 'Invitation',
|
||||
'bySearch': 'By Search',
|
||||
'byScanQrcode': 'By QR Code',
|
||||
"iCreatedGroup": "I Created",
|
||||
"iJoinedGroup": "I Joined",
|
||||
"nPerson": "%s people",
|
||||
"searchNotFound": "No relevant results found",
|
||||
"organizationStructure": "Organization Structure",
|
||||
"recentConversations": "Recent Conversations",
|
||||
"selectAll": "Select All",
|
||||
"plsEnterGroupNameHint": "Choose a group name for easy search",
|
||||
"completeCreation": "Complete Creation",
|
||||
"sendCarteConfirmHint": "Are you sure you want to send this contact to the chat?",
|
||||
"sentSeparatelyTo": "Sent separately to:",
|
||||
"sentTo": "Sent to:",
|
||||
"leaveMessage": "Leave a message",
|
||||
"mergeForwardHint": "[Merge Forward] %s messages",
|
||||
"mergeForward": "Merge Forward",
|
||||
"quicklyFindChatHistory": "Quickly find chat history",
|
||||
"notFoundChatHistory": "No \"%s\" related content found",
|
||||
"globalSearchAll": "All",
|
||||
"globalSearchContacts": "Contacts",
|
||||
"globalSearchGroup": "Groups",
|
||||
"globalSearchChatHistory": "Chat History",
|
||||
"globalSearchChatFile": "Files",
|
||||
"relatedChatHistory": "%s related chat history",
|
||||
"seeMoreRelatedContacts": "See more related contacts",
|
||||
"seeMoreRelatedGroup": "See more related groups",
|
||||
"seeMoreRelatedChatHistory": "See more related chat history",
|
||||
"seeMoreRelatedFile": "See more related documents",
|
||||
"publishPicture": "Publish Picture",
|
||||
"publishVideo": "Publish Video",
|
||||
"mentioned": "Mentioned: %s",
|
||||
"comment": "Comment",
|
||||
"like": "Like",
|
||||
"reply": "Reply",
|
||||
"rollUp": "Roll Up",
|
||||
"fullText": "Full Text",
|
||||
"selectAssetsFromCamera": "Take Photo",
|
||||
"selectAssetsFromAlbum": "Choose from Album",
|
||||
'selectAssetsFirst': "Please select or take at least one picture.",
|
||||
"whoCanWatch": "Who can watch",
|
||||
"remindWhoToWatch": "Remind who to watch",
|
||||
"public": "Public",
|
||||
"everyoneCanSee": "Everyone can see",
|
||||
"partiallyVisible": "Partially visible",
|
||||
"visibleToTheSelected": "Visible to the selected",
|
||||
"partiallyInvisible": "Not visible to some",
|
||||
"invisibleToTheSelected": "Invisible to the selected",
|
||||
"private": "Private",
|
||||
"onlyVisibleToMe": "Only visible to me",
|
||||
"selectVideoLimit": "Duration cannot exceed %s",
|
||||
"selectContactsLimit": "Please select at least one contact",
|
||||
"message": "Message",
|
||||
"commentedYou": "Commented on your post: %s",
|
||||
"likedYou": "Liked your post",
|
||||
"mentionedYou": "Mentioned you",
|
||||
"replied": "Replied",
|
||||
"detail": "Detail",
|
||||
"totalNPicture": "Total %s pictures",
|
||||
"noDynamic": "No posts yet",
|
||||
"callRecords": "Call Records",
|
||||
"allCall": "All Calls",
|
||||
"missedCall": "Missed Calls",
|
||||
"incomingCall": "Incoming",
|
||||
"outgoingCall": "Outgoing",
|
||||
"microphone": "Microphone",
|
||||
"speaker": "Speaker",
|
||||
"hangUp": "Hang Up",
|
||||
"pickUp": "Pick Up",
|
||||
"waitingCallHint": "Calling...",
|
||||
"waitingVoiceCallHint": "Waiting for the other party to pick up...",
|
||||
"invitedVoiceCallHint": "Inviting you for a voice call...",
|
||||
"waitingVideoCallHint": "Waiting for the other party to accept the invitation",
|
||||
"invitedVideoCallHint": "Inviting you for a video call...",
|
||||
"waitingToAnswer": "Waiting to answer",
|
||||
"invitedYouToCall": "Invited you to a call",
|
||||
"calling": "Calling...",
|
||||
"nPeopleCalling": "%s people on the call",
|
||||
'busyVideoCallHint': 'A user is busy and cannot receive your invitation',
|
||||
'inviterBusyVideoCallHint': 'You are in another call and cannot send an invitation',
|
||||
"whoInvitedVoiceCallHint": "%s invited you for a voice call",
|
||||
"whoInvitedVideoCallHint": "%s invited you for a video call",
|
||||
'plsInputMeetingSubject': 'Please enter meeting subject',
|
||||
'meetingStartTime': 'Start Time',
|
||||
'meetingDuration': 'Meeting Duration',
|
||||
'enterMeeting': 'Enter Meeting',
|
||||
'meetingNo': 'Meeting Number',
|
||||
'yourMeetingName': 'Your Name',
|
||||
'plsInputMeetingNo': 'Please enter meeting number',
|
||||
'plsInputYouMeetingName': 'Please enter your name',
|
||||
'meetingSubjectIs': 'Subject: %s',
|
||||
'meetingStartTimeIs': 'Start: %s',
|
||||
'meetingDurationIs': 'Duration: %s',
|
||||
'meetingHostIs': 'Host: %s',
|
||||
'meetingNoIs': 'Number: %s',
|
||||
'meetingMessageClickHint': 'Click this message to join the meeting directly',
|
||||
'meetingMessage': 'Meeting Message',
|
||||
'openMeeting': 'Ongoing Meeting',
|
||||
'didNotStart': 'Did not start',
|
||||
'started': 'Started',
|
||||
'meetingInitiatorIs': 'Video meeting initiated by %s',
|
||||
'meetingDetail': 'Meeting Details',
|
||||
'meetingOrganizerIs': 'Organizer: %s',
|
||||
'updateMeetingInfo': 'Update Meeting Information',
|
||||
'cancelMeeting': 'Cancel Meeting',
|
||||
'videoMeeting': 'Video Meeting',
|
||||
"joinMeeting": "Join Meeting",
|
||||
'bookAMeeting': 'Book a Meeting',
|
||||
'quickMeeting': 'Quick Meeting',
|
||||
'confirmTheChanges': 'Confirm Changes',
|
||||
'invitesYouToVideoConference': '%s invites you to join a video conference',
|
||||
'over': 'End',
|
||||
'meetingMute': 'Mute',
|
||||
'meetingUnmute': 'Unmute',
|
||||
'meetingCloseVideo': 'Off',
|
||||
'meetingOpenVideo': 'On',
|
||||
'meetingEndSharing': 'End Sharing',
|
||||
'meetingShareScreen': 'Share Screen',
|
||||
'meetingMembers': 'Members (%s)',
|
||||
'settings': 'Settings',
|
||||
'leaveMeeting': 'Leave Meeting',
|
||||
'endMeeting': 'End Meeting',
|
||||
'leaveMeetingConfirmHint': 'Are you sure you want to leave the meeting?',
|
||||
'endMeetingConfirmHit': 'Are you sure you want to end the meeting?',
|
||||
'meetingSettings': 'Meeting Settings',
|
||||
'allowMembersOpenMic': 'Allow members to unmute themselves',
|
||||
'allowMembersOpenVideo': 'Allow members to turn on video',
|
||||
'onlyHostShareScreen': 'Only the host can share screen',
|
||||
'onlyHostInviteMember': 'Only the host can invite meeting members',
|
||||
'defaultMuteMembers': 'Mute members upon entry',
|
||||
'pinThisMember': 'Pin',
|
||||
'unpinThisMember': 'Unpin',
|
||||
'allSeeHim': 'See him',
|
||||
'cancelAllSeeHim': 'no see',
|
||||
'muteAll': 'Mute all',
|
||||
'unmuteAll': 'Unmute all',
|
||||
'members': 'Members',
|
||||
'screenShare': 'Screen Share',
|
||||
'screenShareHint': 'Sharing the screen...',
|
||||
'meetingClosedHint': 'The meeting has been closed or the connection has been lost. Are you sure you want to leave?',
|
||||
'meetingIsOver': 'The meeting has ended!',
|
||||
'networkError': 'Network error. Please try again later!',
|
||||
'shareSuccessfully': 'Shared successfully!',
|
||||
'notFoundMinP': 'No mini-programs published yet',
|
||||
'notSendMessageNotInGroup': 'Can\'t send messages in a group chat that\'s signed out',
|
||||
'whoModifyGroupName': '%s modified the group name %s',
|
||||
"accountWarn": "Warn!",
|
||||
"accountException": "Your account has been logged in to another device, please change your password in time.",
|
||||
"tagGroup": "TAG",
|
||||
"issueNotice": "Mass sending assistant",
|
||||
"createTagGroup": "Create tags",
|
||||
"plsEnterTagGroupName": "Please enter a tag name",
|
||||
"tagGroupMember": "Tag member",
|
||||
"completeEdit": "Complete Edit",
|
||||
"emptyTagGroup": "No tag group yet",
|
||||
"confirmDelTagGroupHint": "Are you sure to remove this tag group?",
|
||||
"editTagGroup": " Edit Tag",
|
||||
"newBuild": " New",
|
||||
"receiveMember": " Receive member",
|
||||
"emptyNotification": "No notice yet",
|
||||
"notificationReceiver": "%s recipients: %s",
|
||||
"sendAnother": "Send another",
|
||||
"confirmDelTagNotificationHint": "Are you sure to remove this notification record?",
|
||||
"contentNotBlank": "The content can not be blank",
|
||||
"plsEnterDescription": "Please enter a description",
|
||||
"gifNotSupported": "Gif images are not supported",
|
||||
"lookOver": "Look",
|
||||
"groupRequestHandled": "Has been processed",
|
||||
"burnAfterReadingDescription":
|
||||
"Messages sent after this time will be automatically deleted after the selected time after being read by friends",
|
||||
"periodicallyDeleteMessage": "Periodically delete message",
|
||||
"periodicallyDeleteMessageDescription":
|
||||
"Messages sent after this will be automatically deleted after the selected time",
|
||||
"nDay": "%s days",
|
||||
"nWeek": "%s weeks",
|
||||
"nMonth": "%s months",
|
||||
"talkTooShort": "Talk too short",
|
||||
'quoteContentBeRevoked': 'Quote content be revoked',
|
||||
'tapTooShort': 'Tap too short',
|
||||
'createGroupTips': 'The group name cannot exceed 16 characters',
|
||||
'likedWho': 'liked %s',
|
||||
"mentionedWho": "mentioned %s",
|
||||
"commentedWho": "commented%s:%s",
|
||||
'otherCallHandle': 'Already on other platforms %s call request',
|
||||
'uploadErrorLog': 'Upload all log',
|
||||
'uploaded': 'uploaded',
|
||||
'uploadLogWithLine': 'Set the number of rows to upload logs',
|
||||
'setLines': 'Set Lines',
|
||||
'sdkApiAddress': 'SDK API Address',
|
||||
'sdkWsAddress': 'SDK WS Address',
|
||||
'appAddress': 'Login/Register Address',
|
||||
'serverAddress': 'Server Address',
|
||||
'switchToIP': 'Switch to IP',
|
||||
'switchToDomain': 'Switch to Domain',
|
||||
'serverSettingTips': 'After modifying the configuration, you need to save and restart for the changes to take effect',
|
||||
'callFail': 'You cannot call this user',
|
||||
'search_by_phone_and_uid': 'search by phone and uid',
|
||||
'special_message': 'special message',
|
||||
'edit_group_name': 'Modify group chat name',
|
||||
'edit_group_tips': 'After modifying the group chat name, other members will be notified in the group.',
|
||||
'logLevel': 'Log Level',
|
||||
'tokenInvalid': 'Invalid token, please log in again.',
|
||||
'supportsTypeHint': 'Only png, jpg, jpeg, gif, bmp and webp formats are supported.',
|
||||
'permissionDeniedTitle': 'Permission denied',
|
||||
'permissionDeniedHint': 'Please allow %s permissions in settings.',
|
||||
'camera': 'Camera',
|
||||
'gallery': 'Gallery',
|
||||
'notification': 'Notification',
|
||||
'externalStorage': 'External Storage',
|
||||
'monday': 'Monday',
|
||||
'tuesday': 'Tuesday',
|
||||
'wednesday': 'Wednesday',
|
||||
'thursday': 'Thursday',
|
||||
'friday': 'Friday',
|
||||
'saturday': 'Saturday',
|
||||
'sunday': 'Sunday',
|
||||
'participantRemovedHit': 'You have been removed from the meeting.',
|
||||
'hasBeenSet': 'Has been set.',
|
||||
'lockMeeting': 'Lock Meeting',
|
||||
'lockMeetingHint': 'After the meeting is locked, new members cannot enter the meeting',
|
||||
'voiceMotivation': 'Voice Motivation',
|
||||
'voiceMotivationHint': 'When enabled, the participants who are speaking will be displayed first.',
|
||||
'meetingIsLocked': 'The meeting is locked.',
|
||||
'today': 'Today',
|
||||
'meetingIsEnded': 'The meeting has ended.',
|
||||
'oneXnViews': 'Primary and secondary',
|
||||
'twoXtwoViews': 'Quarter grid',
|
||||
'threeXthreeViews': 'Nine points',
|
||||
'appointNewHost': 'Appoint a new host',
|
||||
'appointNewHostHint': 'Are you sure you want to set it as host?',
|
||||
'gridView': 'Grid View',
|
||||
'gridViewHint': 'Use after opening the video, only effective for yourself',
|
||||
'requestXDoHint': 'Moderator requests you to %s',
|
||||
'keepClose': 'Keep Close',
|
||||
'cancelMeetingConfirmHit': 'Are you sure to cancel this meeting?',
|
||||
'iKnew': 'I know',
|
||||
'assignAndLeave': 'Assign and leave',
|
||||
'muteAllHint': 'All participants will be muted',
|
||||
'meetingEnableVideo': 'Enable Video',
|
||||
'inProgressByTerminalHint':
|
||||
'You were forced to leave the meeting due to an exception. Do you want to rejoin the meeting?',
|
||||
'restore': 'Restore',
|
||||
'done': 'Done',
|
||||
'networkNotStable': 'Network not stable',
|
||||
'otherNetworkNotStableHint': 'The other party\'s network is unstable',
|
||||
'callingInterruption': 'Calling interruption',
|
||||
'meeting': 'Meeting',
|
||||
'directedTo': 'Directed to',
|
||||
'100000': 'User not found',
|
||||
'100001': 'User has already registered',
|
||||
'100002': 'Incorrect account or password',
|
||||
'100003': 'Not found user account',
|
||||
'100004': 'Not found user token',
|
||||
'200001': 'One user joins more than one meeting',
|
||||
'200002': 'Password not match error',
|
||||
'200003': 'Meeting auth check permission error',
|
||||
'200004': 'The meeting has ended',
|
||||
'100010': 'The login status has expired and you need to log in again',
|
||||
'1004': '%s does not exist',
|
||||
};
|
||||
@@ -0,0 +1,616 @@
|
||||
const Map<String, String> zh_CN = {
|
||||
"welcome": "欢迎使用OpenCorp(Flutter版)",
|
||||
"userID": "用户ID",
|
||||
"phoneNumber": "手机号",
|
||||
"plsEnterPhoneNumber": "请输入您的手机号",
|
||||
"plsEnterEmail": "请输入您的邮箱",
|
||||
"password": "密码",
|
||||
"plsEnterPassword": "请输入您的密码",
|
||||
"account": "账号",
|
||||
"plsEnterAccount": "请输入您的账号",
|
||||
"forgetPassword": "忘记密码",
|
||||
"verificationCodeLogin": "验证码登录",
|
||||
"login": "登录",
|
||||
"noAccountYet": "还没有账号?",
|
||||
"loginNow": "立即登录",
|
||||
"registerNow": "立即注册",
|
||||
'lockPwdErrorHint': '错误%s次',
|
||||
"newUserRegister": "新用户注册",
|
||||
"verificationCode": "验证码",
|
||||
"sendVerificationCode": "发送验证码",
|
||||
'resendVerificationCode': '重发验证码',
|
||||
'verificationCodeTimingReminder': '%sS后重新获取验证码',
|
||||
"defaultVerificationCode": "(默认验证码:%s)",
|
||||
"plsEnterVerificationCode": "请输入您的验证码",
|
||||
"invitationCode": "邀请码",
|
||||
"plsEnterInvitationCode": "请输入您的邀请码%s",
|
||||
"optional": "选填",
|
||||
"nextStep": "下一步",
|
||||
"plsEnterRightPhone": "请输入正确的手机号",
|
||||
'plsEnterRightEmail': '请输入正确的邮箱',
|
||||
'plsEnterRightAccount': '请输入正确的账号',
|
||||
'plsEnterRightPhoneOrEmail': '请输入正确的邮箱或手机号',
|
||||
"enterVerificationCode": "输入%s验证码",
|
||||
"setPassword": "设置密码",
|
||||
"plsConfirmPasswordAgain": "请再次确认您的密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"wrongPasswordFormat": "密码格式错误",
|
||||
"plsCompleteInfo": "请完善个人信息",
|
||||
"plsEnterYourNickname": "请输入您的昵称",
|
||||
"setInfo": "设置信息",
|
||||
"loginPwdFormat": "6~20位,至少包含数字、字母",
|
||||
"passwordLogin": "密码登录",
|
||||
'through': '通过%s',
|
||||
"home": "OpenIM",
|
||||
"contacts": "通讯录",
|
||||
"workbench": "发现",
|
||||
"mine": "我的",
|
||||
"draftText": "草稿",
|
||||
'everyone': '所有人',
|
||||
"you": "你",
|
||||
'someoneMentionYou': '有人@你',
|
||||
"groupAc": "群公告",
|
||||
"createGroupNtf": "%s 创建了群聊",
|
||||
"editGroupInfoNtf": "%s 修改了群资料",
|
||||
"quitGroupNtf": "%s 退出了群聊",
|
||||
"invitedJoinGroupNtf": "%s 邀请 %s 加入群聊",
|
||||
"kickedGroupNtf": "%s 被 %s 移出群聊",
|
||||
"joinGroupNtf": "%s 加入群聊",
|
||||
"dismissGroupNtf": "%s 解散了群聊",
|
||||
"transferredGroupNtf": "%s 将群管理权限转让给了 %s",
|
||||
"muteMemberNtf": "%s 被 %s 禁言%s",
|
||||
"muteCancelMemberNtf": "%s 被 %s 取消了禁言",
|
||||
"muteGroupNtf": "%s 开起了群禁言",
|
||||
"muteCancelGroupNtf": "%s 关闭了群禁言",
|
||||
"friendAddedNtf": "你们已成为好友,可以开始聊天了",
|
||||
"openPrivateChatNtf": "已开启阅后即焚",
|
||||
"closePrivateChatNtf": "已关闭阅后即焚",
|
||||
"memberInfoChangedNtf": "%s 编辑了自己的群成员资料",
|
||||
"unsupportedMessage": "暂不支持的消息类型",
|
||||
"picture": "图片",
|
||||
"video": "视频",
|
||||
"voice": "语音",
|
||||
"location": "位置",
|
||||
"file": "文件",
|
||||
"carte": "名片",
|
||||
"emoji": "自定义表情",
|
||||
"chatRecord": "聊天记录",
|
||||
"revokeMsg": "撤回了一条消息",
|
||||
"aRevokeBMsg": "%s 撤回了 %s 的消息",
|
||||
'blockedByFriendHint': '消息已发出,但被对方拒收了',
|
||||
'deletedByFriendHint': '%s开启了好友验证,你还不是他的好友 ',
|
||||
'sendFriendVerification': '验证添加',
|
||||
'removedFromGroupHint': '你已被移出群聊',
|
||||
'groupDisbanded': '群已经解散',
|
||||
'search': '搜索',
|
||||
'synchronizing': '同步中',
|
||||
'syncFailed': '同步失败',
|
||||
'connecting': '连接中',
|
||||
'connectionFailed': '连接失败',
|
||||
'top': '置顶',
|
||||
'cancelTop': '取消置顶',
|
||||
'markHasRead': '标记已读',
|
||||
'delete': '删除',
|
||||
'nPieces': '%s条',
|
||||
"online": "在线",
|
||||
"offline": "离线",
|
||||
"phoneOnline": "手机",
|
||||
"pcOnline": "电脑",
|
||||
"webOnline": "Web",
|
||||
"webMiniOnline": "小程序",
|
||||
"upgradeFind": "发现新版本",
|
||||
"upgradeVersion": "检测到一个新的可用版本%s,你当前版本是%s",
|
||||
"upgradeDescription": "更新说明:",
|
||||
"upgradeIgnore": "忽略",
|
||||
"upgradeLater": "稍后更新",
|
||||
"upgradeNow": "立即更新",
|
||||
'upgradePermissionTips': '请打开通知权限,否则无法显示升级进度。',
|
||||
'inviteYouCall': '%s邀请你进行%s',
|
||||
'rejectCall': '拒绝',
|
||||
'acceptCall': '同意',
|
||||
"callVoice": "语音通话",
|
||||
"callVideo": "视频通话",
|
||||
"sentSuccessfully": "发送成功",
|
||||
"copySuccessfully": "复制成功",
|
||||
"day": "天",
|
||||
"hour": "时",
|
||||
"hours": "小时",
|
||||
"minute": "分钟",
|
||||
"seconds": "秒",
|
||||
"cancel": "取消",
|
||||
"determine": "确定",
|
||||
"toolboxAlbum": "相册",
|
||||
"toolboxCall": "视频通话",
|
||||
"toolboxCamera": "拍摄",
|
||||
"toolboxCard": "名片",
|
||||
"toolboxFile": "文件",
|
||||
"toolboxLocation": "位置",
|
||||
"toolboxDirectionalMessage": "定向消息",
|
||||
"send": "发送",
|
||||
"holdTalk": "按住 说话",
|
||||
"releaseToSend": "松开 发送",
|
||||
"releaseToSendSwipeUpToCancel": "松开发送,上滑取消",
|
||||
"liftFingerToCancelSend": "松开手指,取消发送",
|
||||
"callDuration": "通话时长 %s",
|
||||
"cancelled": "已取消",
|
||||
"cancelledByCaller": "对方已取消",
|
||||
"rejectedByCaller": "对方已拒绝",
|
||||
"callTimeout": "超时无响应",
|
||||
"rejected": "已拒绝",
|
||||
"networkAnomaly": "网络异常",
|
||||
'forwardMaxCountHint': '当前仅支持转发最多二十条消息~',
|
||||
"typing": "正在输入...",
|
||||
"addSuccessfully": "添加成功",
|
||||
"addFailed": "添加失败",
|
||||
"setSuccessfully": "设置成功",
|
||||
'callingBusy': '你已在通话中或正在会议中,不能进行此操作!',
|
||||
'groupCallHint': '当前群正在通话中,你确定加入当前通话吗?',
|
||||
'joinIn': '加入',
|
||||
'menuCopy': '复制',
|
||||
'menuDel': '删除',
|
||||
'menuForward': '转发',
|
||||
'menuReply': '回复',
|
||||
'menuMulti': '多选',
|
||||
'menuRevoke': '撤回',
|
||||
'menuAdd': '添加',
|
||||
'nMessage': '%s条新消息',
|
||||
'plsSelectLocation': '请选择一个位置',
|
||||
'groupAudioCallHint': '%s人正在语音通话',
|
||||
'groupVideoCallHint': '%s人正在视频通话',
|
||||
'reEdit': '重新编辑',
|
||||
'playSpeed': '播放速度',
|
||||
'download': '下载',
|
||||
"googleMap": "谷歌地图",
|
||||
"appleMap": "Apple地图",
|
||||
"baiduMap": "百度地图",
|
||||
"amapMap": "高德地图",
|
||||
"tencentMap": "腾讯地图",
|
||||
"offlineMeetingMessage": "你收到了一条会议邀请消息",
|
||||
"offlineMessage": "你收到了一条新消息",
|
||||
"offlineCallMessage": "你收到了一条通话邀请消息",
|
||||
"logoutHint": "您确定要退出登录吗?",
|
||||
"myInfo": "我的信息",
|
||||
"workingCircle": "工作圈",
|
||||
"accountSetup": "账号设置",
|
||||
"aboutUs": "关于我们",
|
||||
"logout": "退出登录",
|
||||
"qrcode": "二维码",
|
||||
"qrcodeHint": "扫一扫下面的二维码,添加我为好友",
|
||||
"favoriteFace": "收藏表情",
|
||||
"favoriteManage": "管理",
|
||||
"favoriteCount": "共%s个表情",
|
||||
"favoriteDel": "删除(%s)",
|
||||
"hasRead": "已读",
|
||||
"unread": "未读",
|
||||
"nPersonUnRead": "%s人未读",
|
||||
"allRead": "全部已读",
|
||||
"messageRecipientList": "消息接收人列表",
|
||||
"hasReadCount": "已读(%s)",
|
||||
"unreadCount": "未读(%s)",
|
||||
"newFriend": "新的好友",
|
||||
"newGroup": "新的群聊",
|
||||
"newGroupRequest": "群通知",
|
||||
"myFriend": "我的好友",
|
||||
"myGroup": "我的群组",
|
||||
"add": "添加",
|
||||
"scan": "扫一扫",
|
||||
"scanHint": "扫描二维码名片",
|
||||
"addFriend": "添加好友",
|
||||
"addFriendHint": "通过ID号搜索添加",
|
||||
"createGroup": "创建群聊",
|
||||
"createGroupHint": "创建群聊,全面使用OpenIM",
|
||||
"addGroup": "添加群聊",
|
||||
"addGroupHint": "向管理员或群聊成员询问ID",
|
||||
"searchIDAddFriend": "搜索ID添加好友",
|
||||
"searchIDAddGroup": "搜索ID添加群聊",
|
||||
"searchIDIs": "ID:%s",
|
||||
"searchPhoneIs": "手机号:%s",
|
||||
"searchEmailIs": "邮箱:%s",
|
||||
"searchNicknameIs": "昵称:%s",
|
||||
"searchGroupNicknameIs": "群昵称:%s",
|
||||
"noFoundUser": "无法找到该用户",
|
||||
"noFoundGroup": "无法找到该群聊",
|
||||
'joinGroupDate': '入群时间',
|
||||
'joinGroupMethod': '入群方式',
|
||||
'byInviteJoinGroup': '%s邀请进群',
|
||||
'byIDJoinGroup': '搜索群ID',
|
||||
'byQrcodeJoinGroup': '群二维码',
|
||||
'groupID': '群ID号',
|
||||
'setAsAdmin': '设为管理员',
|
||||
'setMute': '设置禁言',
|
||||
'organizationInfo': '组织信息',
|
||||
'organization': '组织',
|
||||
'department': '部门',
|
||||
'position': '职位',
|
||||
'personalInfo': '个人资料',
|
||||
'viewDynamics': '查看动态',
|
||||
'audioAndVideoCall': '音视频通话',
|
||||
'sendMessage': '发消息',
|
||||
'avatar': '头像',
|
||||
'name': '姓名',
|
||||
'nickname': '昵称',
|
||||
'gender': '性别',
|
||||
'englishName': '英文名',
|
||||
'birthDay': '生日',
|
||||
'tel': '座机',
|
||||
'mobile': '手机号码',
|
||||
'email': '邮箱',
|
||||
'man': '男',
|
||||
'woman': '女',
|
||||
'friendSetup': '好友设置',
|
||||
'setupRemark': '设置备注',
|
||||
'recommendToFriend': '把他推荐给朋友',
|
||||
'addToBlacklist': '加入黑名单',
|
||||
'unfriend': '解除好友关系',
|
||||
"areYouSureDelFriend": "确定删除好友吗?",
|
||||
"areYouSureAddBlacklist": "确定将好友加入黑名单吗?",
|
||||
"remark": "备注",
|
||||
"save": "保存",
|
||||
"saveSuccessfully": "保存成功",
|
||||
"saveFailed": "保存失败",
|
||||
"groupVerification": "群聊验证",
|
||||
"friendVerification": "好友验证",
|
||||
"sendEnterGroupApplication": "发送入群申请",
|
||||
"sendToBeFriendApplication": "发送好友申请",
|
||||
"sendSuccessfully": "发送成功",
|
||||
"sendFailed": "发送失败",
|
||||
'canNotAddFriends': '该用户已设置不可添加!',
|
||||
"mutedAll": "全体禁言",
|
||||
"tenMinutes": "10分钟",
|
||||
"oneHour": "1小时",
|
||||
"twelveHours": "12小时",
|
||||
"oneDay": "1天",
|
||||
"custom": "自定义",
|
||||
'unmute': '取消禁言',
|
||||
"youMuted": '你已被禁言',
|
||||
"groupMuted": '已开启群禁言',
|
||||
"notDisturbMode": '勿扰模式',
|
||||
"allowRing": '新消息提示音',
|
||||
"allowVibrate": '新消息震动',
|
||||
'forbidAddMeToFriend': '禁止加我为好友',
|
||||
"blacklist": '通讯录黑名单',
|
||||
"unlockSettings": '解锁设置',
|
||||
"changePassword": '修改密码',
|
||||
"clearChatHistory": '清空聊天记录',
|
||||
"confirmClearChatHistory": "确认清空所有聊天记录吗?",
|
||||
"languageSetup": "语言设置",
|
||||
"language": "语言",
|
||||
"english": "English",
|
||||
"chinese": "简体中文",
|
||||
"followSystem": "跟随系统",
|
||||
"blacklistEmpty": "暂无黑名单",
|
||||
"remove": "移除",
|
||||
'fingerprint': '指纹',
|
||||
'gesture': '手势',
|
||||
'biometrics': '生物识别',
|
||||
'plsEnterPwd': '请输入密码',
|
||||
'plsEnterOldPwd': '请输入原始密码',
|
||||
'plsEnterNewPwd': '请输入新密码',
|
||||
'plsConfirmNewPwd': '请确认新密码',
|
||||
'reset': '重置',
|
||||
'oldPwd': '原密码:',
|
||||
'newPwd': '新密码:',
|
||||
'confirmNewPwd': '确认新密码:',
|
||||
'plsEnterConfirmPwd': '请输入确认密码',
|
||||
'twicePwdNoSame': '两次输入的密码不一致',
|
||||
'changedSuccessfully': '修改成功',
|
||||
'checkNewVersion': '检查新版本',
|
||||
'chatContent': '聊天内容',
|
||||
'topContacts': '置顶联系人',
|
||||
'messageNotDisturb': '消息免打扰',
|
||||
'messageNotDisturbHint': '接收消息但不提醒',
|
||||
"burnAfterReading": "阅后即焚",
|
||||
"timeSet": "时间设置",
|
||||
'setChatBackground': '设置聊天背景',
|
||||
'setDefaultBackground': '使用默认背景',
|
||||
'fontSize': '字体大小',
|
||||
"little": "小",
|
||||
"standard": "标准",
|
||||
"big": "大",
|
||||
'thirtySeconds': '30秒',
|
||||
'fiveMinutes': '5分钟',
|
||||
"clearAll": "清空",
|
||||
"clearSuccessfully": "清除成功",
|
||||
"groupChatSetup": "群聊设置",
|
||||
"viewAllGroupMembers": "查看全部群成员(%s)",
|
||||
"groupManage": "群管理",
|
||||
"myGroupMemberNickname": "我在本群的名称",
|
||||
"topChat": "聊天置顶",
|
||||
"muteAllMember": "全员禁言",
|
||||
"exitGroup": "退出群聊",
|
||||
"dismissGroup": "解散群聊",
|
||||
"dismissGroupHint": "解散群聊后将失去和群成员的联系。",
|
||||
"quitGroupHint": "退出群聊后将不再接收此群聊信息。",
|
||||
'joinGroupSet': '进群验证',
|
||||
'allowAnyoneJoinGroup': '允许任何人加群',
|
||||
'inviteNotVerification': '群成员邀请无需验证',
|
||||
'needVerification': '需要发送验证信息',
|
||||
'addMember': '添加',
|
||||
'delMember': '移除',
|
||||
'groupOwner': '群主',
|
||||
'groupAdmin': '管理员',
|
||||
'notAllowSeeMemberProfile': '不允许查看其他群成员资料',
|
||||
'notAllAddMemberToBeFriend': '不允许添加群成员为好友',
|
||||
'transferGroupOwnerRight': '群主管理权转让',
|
||||
'groupName': '群名称',
|
||||
'groupAcPermissionTips': '只有群主及管理员可以编辑',
|
||||
'plsEnterGroupAc': '请输入群公告',
|
||||
'edit': '编辑',
|
||||
'publish': '发布',
|
||||
'groupMember': '群成员',
|
||||
'selectedPeopleCount': '已选择(%s)',
|
||||
'confirmSelectedPeople': '确定(%s/%s)',
|
||||
'confirm': '确认',
|
||||
"confirmTransferGroupToUser": "确定将群主转让给:%s?",
|
||||
"removeGroupMember": "移除群成员",
|
||||
"searchNotResult": "搜索无结果",
|
||||
"groupQrcode": "群二维码",
|
||||
"groupQrcodeHint": "扫一扫下面的二维码,立即加入群聊",
|
||||
"approved": "已同意",
|
||||
"accept": "接受",
|
||||
"reject": "拒绝",
|
||||
"waitingForVerification": "等待验证",
|
||||
"rejectSuccessfully": "拒绝成功",
|
||||
"rejectFailed": "拒绝失败",
|
||||
"applyJoin": "申请加入",
|
||||
"enterGroup": "进入群聊",
|
||||
"applyReason": "申请理由:%s",
|
||||
'invite': '邀请',
|
||||
'sourceFrom': '来源:%s',
|
||||
'byMemberInvite': '邀请',
|
||||
'bySearch': '通过搜索',
|
||||
'byScanQrcode': '扫描二维码',
|
||||
"iCreatedGroup": "我创建的",
|
||||
"iJoinedGroup": "我加入的",
|
||||
"nPerson": "%s人",
|
||||
"searchNotFound": "未搜索到相关结果",
|
||||
"organizationStructure": "组织架构",
|
||||
"recentConversations": "最近会话",
|
||||
"selectAll": "全选",
|
||||
"plsEnterGroupNameHint": "取个群名称方便后续搜索",
|
||||
"completeCreation": "完成创建",
|
||||
"sendCarteConfirmHint": "确定发送该联系人到本聊天吗?",
|
||||
"sentSeparatelyTo": "分别发送给:",
|
||||
"sentTo": "发送给:",
|
||||
"leaveMessage": "留言",
|
||||
"mergeForwardHint": "[合并转发]共%s条消息",
|
||||
"mergeForward": "合并转发",
|
||||
"quicklyFindChatHistory": "快速查找聊天记录",
|
||||
"notFoundChatHistory": "没有找到\"%s\"相关内容",
|
||||
"globalSearchAll": "综合",
|
||||
"globalSearchContacts": "联系人",
|
||||
"globalSearchGroup": "群组",
|
||||
"globalSearchChatHistory": "聊天记录",
|
||||
"globalSearchChatFile": "文件",
|
||||
"relatedChatHistory": "%s条相关的聊天记录",
|
||||
"seeMoreRelatedContacts": "查看更多相关联系人",
|
||||
"seeMoreRelatedGroup": "查看更多相关群组",
|
||||
"seeMoreRelatedChatHistory": "查看更多相关聊天记录",
|
||||
"seeMoreRelatedFile": "查看更多相关文档",
|
||||
"publishPicture": "发布图片",
|
||||
"publishVideo": "发布视频",
|
||||
"mentioned": "提到了:%s",
|
||||
"comment": "评论",
|
||||
"like": "赞",
|
||||
"reply": "回复",
|
||||
"rollUp": "收起",
|
||||
"fullText": "全文",
|
||||
"selectAssetsFromCamera": "拍摄",
|
||||
"selectAssetsFromAlbum": "从相册选择",
|
||||
'selectAssetsFirst': "请至少选择或者拍摄一张图片。",
|
||||
"whoCanWatch": "谁可以看",
|
||||
"remindWhoToWatch": "提醒谁看",
|
||||
"public": "公开",
|
||||
"everyoneCanSee": "所有人可见",
|
||||
"partiallyVisible": "部分可见",
|
||||
"visibleToTheSelected": "选中的人可见",
|
||||
"partiallyInvisible": "不给谁看",
|
||||
"invisibleToTheSelected": "选中的人不可见",
|
||||
"private": "私密",
|
||||
"onlyVisibleToMe": "仅自己可见",
|
||||
"selectVideoLimit": "时长不能超过%s",
|
||||
"selectContactsLimit": "至少选择一个联系人",
|
||||
"message": "消息",
|
||||
"commentedYou": "评论了你:%s",
|
||||
"commentedWho": "评论了%s:%s",
|
||||
"likedYou": "为你点了赞",
|
||||
"mentionedYou": "提到了你",
|
||||
"mentionedWho": "提到了%s",
|
||||
"replied": "回复了",
|
||||
"detail": "详情",
|
||||
"totalNPicture": "共%s张",
|
||||
"noDynamic": "暂无动态",
|
||||
"callRecords": "通话记录",
|
||||
"allCall": "所有通话",
|
||||
"missedCall": "未接来电",
|
||||
"incomingCall": "呼入",
|
||||
"outgoingCall": "呼出",
|
||||
"microphone": "麦克风",
|
||||
"speaker": "扬声器",
|
||||
"hangUp": "挂断",
|
||||
"pickUp": "接听",
|
||||
"waitingCallHint": "正在呼叫中…",
|
||||
"waitingVoiceCallHint": "正在等待对方接听…",
|
||||
"invitedVoiceCallHint": "邀请你进行语音通话…",
|
||||
"waitingVideoCallHint": "正在等待对方接受邀请",
|
||||
"invitedVideoCallHint": "邀请你进行视频通话…",
|
||||
"waitingToAnswer": "等待接听",
|
||||
"invitedYouToCall": "邀请你通话",
|
||||
"calling": "通话中",
|
||||
"nPeopleCalling": "%s人正在通话中",
|
||||
'busyVideoCallHint': '有用户正忙,无法接收到你的邀请',
|
||||
'inviterBusyVideoCallHint': '你在其它通话中,无法发起邀请',
|
||||
"whoInvitedVoiceCallHint": "%s邀请你进行语音通话",
|
||||
"whoInvitedVideoCallHint": "%s邀请你进行视频通话",
|
||||
'plsInputMeetingSubject': '请输入会议主题',
|
||||
'meetingStartTime': '开始时间',
|
||||
'meetingDuration': '会议时长',
|
||||
'enterMeeting': '进入会议',
|
||||
'meetingNo': '会议号',
|
||||
'yourMeetingName': '您的名称',
|
||||
'plsInputMeetingNo': '请输入会议号',
|
||||
'plsInputYouMeetingName': '请输入您的名称',
|
||||
'meetingSubjectIs': '会议主题:%s',
|
||||
'meetingStartTimeIs': '开始时间:%s',
|
||||
'meetingDurationIs': '会议时长:%s',
|
||||
'meetingHostIs': '主持人:%s',
|
||||
'meetingNoIs': '会议号:%s',
|
||||
'meetingMessageClickHint': '点击此消息可直接加入会议',
|
||||
'meetingMessage': '会议消息',
|
||||
'openMeeting': '未结束会议',
|
||||
'didNotStart': '未开始',
|
||||
'started': '已开始',
|
||||
'meetingInitiatorIs': '%s发起的视频会议',
|
||||
'meetingDetail': '会议详情',
|
||||
'meetingOrganizerIs': '发起人:%s',
|
||||
'updateMeetingInfo': '修改会议信息',
|
||||
'cancelMeeting': '取消会议',
|
||||
'videoMeeting': '视频会议',
|
||||
"joinMeeting": "加入会议",
|
||||
'bookAMeeting': '预约会议',
|
||||
'quickMeeting': '快速会议',
|
||||
'confirmTheChanges': '确认修改',
|
||||
'invitesYouToVideoConference': '%s邀请你加入视频会议',
|
||||
'over': '结束',
|
||||
'meetingMute': '静音',
|
||||
'meetingUnmute': '取消静音',
|
||||
'meetingCloseVideo': '关闭视频',
|
||||
'meetingOpenVideo': '开启视频',
|
||||
'meetingEndSharing': '结束共享',
|
||||
'meetingShareScreen': '共享屏幕',
|
||||
'meetingMembers': '成员(%s)',
|
||||
'settings': '设置',
|
||||
'leaveMeeting': '离开会议',
|
||||
'endMeeting': '结束会议',
|
||||
'leaveMeetingConfirmHint': '确定离开会议吗?',
|
||||
'endMeetingConfirmHit': '确定结束会议吗?',
|
||||
'meetingSettings': '会议设置',
|
||||
'allowMembersOpenMic': '允许成员自我解除静音',
|
||||
'allowMembersOpenVideo': '允许成员开启视频',
|
||||
'onlyHostShareScreen': '仅主持人可以共享屏幕',
|
||||
'onlyHostInviteMember': '仅主持人可邀请会议成员',
|
||||
'defaultMuteMembers': '成员入会禁音',
|
||||
'pinThisMember': '置顶该成员',
|
||||
'unpinThisMember': '取消置顶',
|
||||
'allSeeHim': '全部看他',
|
||||
'cancelAllSeeHim': '取消看他',
|
||||
'muteAll': '全员静音',
|
||||
'unmuteAll': '解除全员静音',
|
||||
'members': '成员',
|
||||
'screenShare': '屏幕共享',
|
||||
'screenShareHint': '共享屏幕中...',
|
||||
'meetingClosedHint': '会议已关闭或已断开链接,确定离开吗?',
|
||||
'meetingIsOver': '会议已经结束!',
|
||||
'networkError': '网络异常请稍后再试!',
|
||||
'shareSuccessfully': '分享成功!',
|
||||
'notFoundMinP': '暂未发布小程序',
|
||||
'notSendMessageNotInGroup': '无法在已退出的群聊中发送消息',
|
||||
'whoModifyGroupName': '%s 修改了群名称为 %s',
|
||||
"accountWarn": "警告!",
|
||||
"accountException": "你的账号已在其他设备登录,请及时修改密码。",
|
||||
"tagGroup": "标签",
|
||||
"issueNotice": "群发助手",
|
||||
"createTagGroup": "创建标签",
|
||||
"plsEnterTagGroupName": "请输入标签名",
|
||||
"tagGroupMember": "标签成员",
|
||||
"completeEdit": "完成编辑",
|
||||
"emptyTagGroup": "暂无标签分组",
|
||||
"confirmDelTagGroupHint": "确认移除该标签分组吗?",
|
||||
"editTagGroup": " 编辑标签",
|
||||
"newBuild": " 新建",
|
||||
"receiveMember": " 接收成员",
|
||||
"emptyNotification": "暂无通知",
|
||||
"notificationReceiver": "%s个接收成员:%s",
|
||||
"sendAnother": "再发一条",
|
||||
"confirmDelTagNotificationHint": "确认移除该通知记录吗?",
|
||||
"contentNotBlank": "内容不能为空",
|
||||
"plsEnterDescription": "请输入描述文字",
|
||||
"gifNotSupported": "不支持gif图片",
|
||||
"lookOver": "查看",
|
||||
"groupRequestHandled": "入群申请已经被群主或其他管理员处理",
|
||||
"burnAfterReadingDescription": "在此之后发送的消息,好友已读后会在选择的时间之后自动删除",
|
||||
"periodicallyDeleteMessage": "定期删除消息记录",
|
||||
"periodicallyDeleteMessageDescription": "在此之后发送的消息会在选择的时间之后自动删除",
|
||||
"nDay": "%s天",
|
||||
"nWeek": "%s周",
|
||||
"nMonth": "%s个月",
|
||||
"talkTooShort": "说话时间太短",
|
||||
'quoteContentBeRevoked': '引用内容已被删除',
|
||||
'tapTooShort': '点击时间太短',
|
||||
'createGroupTips': '群名称不能超过16个字符',
|
||||
'likedWho': '点赞了%s',
|
||||
'otherCallHandle': '已在其他平台%s呼叫请求',
|
||||
'uploadErrorLog': '上传全部日志',
|
||||
'uploadLogWithLine': '设置行数上传日志',
|
||||
'setLines': '设置行数',
|
||||
'uploaded': '已上传',
|
||||
'sdkApiAddress': 'IM API服务器地址',
|
||||
'sdkWsAddress': 'IM WS地址',
|
||||
'appAddress': '登录注册服务器地址',
|
||||
'serverAddress': '服务器地址',
|
||||
'switchToIP': '切换到IP',
|
||||
'switchToDomain': '切换到域名',
|
||||
'serverSettingTips': '修改配置后,保存并重启才能生效',
|
||||
'callFail': '你无法呼叫该用户',
|
||||
'search_by_phone_and_uid': '通过手机号/ID号/搜索添加',
|
||||
'special_message': '特殊消息',
|
||||
'edit_group_name': '修改群聊名称',
|
||||
'edit_group_tips': '修改群聊名称后,将在群内通知其他成员。',
|
||||
'logLevel': '日志级别',
|
||||
'tokenInvalid': '无效的token,请重新登录。',
|
||||
'supportsTypeHint': '仅支持png、jpg、jpeg、gif、bmp及webp格式。',
|
||||
'permissionDeniedTitle': '权限被拒绝',
|
||||
'permissionDeniedHint': '请在设置中允许使用%s权限。',
|
||||
'camera': '相机',
|
||||
'gallery': '相册',
|
||||
'notification': '通知',
|
||||
'externalStorage': '外部存储',
|
||||
'monday': '星期一',
|
||||
'tuesday': '星期二',
|
||||
'wednesday': '星期三',
|
||||
'thursday': '星期四',
|
||||
'friday': '星期五',
|
||||
'saturday': '星期六',
|
||||
'sunday': '星期日',
|
||||
'participantRemovedHit': '你已被请出会议。',
|
||||
'hasBeenSet': '已被设置',
|
||||
'lockMeeting': '锁定会议',
|
||||
'lockMeetingHint': '锁定会议后,新成员无法进入会议',
|
||||
'voiceMotivation': '语音激励',
|
||||
'voiceMotivationHint': '开启后,会优先显示正在说话的参会成员',
|
||||
'meetingIsLocked': '会议已被锁定',
|
||||
'today': '今天',
|
||||
'meetingIsEnded': '会议已结束',
|
||||
'oneXnViews': '主次视图',
|
||||
'twoXtwoViews': '四分格视图',
|
||||
'threeXthreeViews': '九分格视图',
|
||||
'appointNewHost': '指定一名新的主持人',
|
||||
'appointNewHostHint': '确定将其设置为主持人吗?',
|
||||
'gridView': '网格视图',
|
||||
'gridViewHint': '开启视频后使用,仅对自己生效',
|
||||
'requestXDoHint': '主持人请求%s',
|
||||
'keepClose': '保持关闭',
|
||||
'cancelMeetingConfirmHit': '确定取消该会议吗?',
|
||||
'iKnew': '知道了',
|
||||
'assignAndLeave': '设置并离开',
|
||||
'muteAllHint': '所有参会人员即将静音',
|
||||
'meetingEnableVideo': '开启视频',
|
||||
'inProgressByTerminalHint': '由于出现异常,您被迫离开会议。您想重新加入会议吗?',
|
||||
'restore': '恢复',
|
||||
'done': '完成',
|
||||
'networkNotStableHint': '你的网络不稳定',
|
||||
'otherNetworkNotStableHint': '对方网络不稳定',
|
||||
'callingInterruption': '通话中断',
|
||||
'meeting': '会议',
|
||||
'directedTo': '定向发送给',
|
||||
'100001': '账号已注册',
|
||||
'100002': '账号或者密码错误',
|
||||
'100003': '未发现账号',
|
||||
'100004': '未发现账号token',
|
||||
'200001': '你已在其它会议室,不能重复进入',
|
||||
'200002': '密码不匹配',
|
||||
'200003': '没有权限',
|
||||
'200004': '会议已结束',
|
||||
'100010': '登录状态已失效,需要重新登录',
|
||||
'1004': '%s不存在',
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class Styles {
|
||||
Styles._();
|
||||
|
||||
static Color c_0089FF = const Color(0xFF0089FF); // 主题色
|
||||
static Color c_0C1C33 = const Color(0xFF0C1C33); // 黑色字体
|
||||
static Color c_8E9AB0 = const Color(0xFF8E9AB0); // 说明文字
|
||||
static Color c_E8EAEF = const Color(0xFFE8EAEF); // 分割线
|
||||
static Color c_FF381F = const Color(0xFFFF381F); // 警告色
|
||||
static Color c_FFFFFF = const Color(0xFFFFFFFF); // 警告色
|
||||
static Color c_18E875 = const Color(0xFF18E875); // 在线
|
||||
static Color c_F0F2F6 = const Color(0xFFF0F2F6); // 聊天页底部
|
||||
static Color c_000000 = const Color(0xFF000000); //
|
||||
static Color c_92B3E0 = const Color(0xFF92B3E0);
|
||||
static Color c_F2F8FF = const Color(0xFFF2F8FF); // 同步成功背景色
|
||||
static Color c_F8F9FA = const Color(0xFFF8F9FA); // 默认背景
|
||||
static Color c_6085B1 = const Color(0xFF6085B1);
|
||||
static Color c_FFB300 = const Color(0xFFFFB300); // 会议状态
|
||||
static Color c_FFE1DD = const Color(0xFFFFE1DD); // 同步失败背景色
|
||||
static Color c_707070 = const Color(0xFF707070);
|
||||
|
||||
static Color c_92B3E0_opacity50 = c_92B3E0.withOpacity(.5); // 气泡背景
|
||||
static Color c_E8EAEF_opacity50 = c_E8EAEF.withOpacity(.5);
|
||||
static Color c_F4F5F7 = const Color(0xFFF4F5F7);
|
||||
static Color c_CCE7FE = const Color(0xFFCCE7FE);
|
||||
|
||||
static Color c_FFFFFF_opacity0 = c_FFFFFF.withOpacity(.0);
|
||||
static Color c_FFFFFF_opacity70 = c_FFFFFF.withOpacity(.7);
|
||||
static Color c_FFFFFF_opacity50 = c_FFFFFF.withOpacity(.5);
|
||||
static Color c_0089FF_opacity10 = c_0089FF.withOpacity(.1);
|
||||
static Color c_0089FF_opacity20 = c_0089FF.withOpacity(.2);
|
||||
static Color c_0089FF_opacity50 = c_0089FF.withOpacity(.5);
|
||||
static Color c_FF381F_opacity10 = c_FF381F.withOpacity(.1);
|
||||
static Color c_8E9AB0_opacity13 = c_8E9AB0.withOpacity(.13);
|
||||
static Color c_8E9AB0_opacity15 = c_8E9AB0.withOpacity(.15);
|
||||
static Color c_8E9AB0_opacity16 = c_8E9AB0.withOpacity(.16);
|
||||
static Color c_8E9AB0_opacity30 = c_8E9AB0.withOpacity(.3);
|
||||
static Color c_8E9AB0_opacity50 = c_8E9AB0.withOpacity(.5);
|
||||
static Color c_0C1C33_opacity30 = c_0C1C33.withOpacity(.3);
|
||||
static Color c_0C1C33_opacity60 = c_0C1C33.withOpacity(.6);
|
||||
static Color c_0C1C33_opacity85 = c_0C1C33.withOpacity(.85);
|
||||
static Color c_0C1C33_opacity80 = c_0C1C33.withOpacity(.8);
|
||||
static Color c_FF381F_opacity70 = c_FF381F.withOpacity(.7);
|
||||
static Color c_000000_opacity70 = c_000000.withOpacity(.7);
|
||||
static Color c_000000_opacity15 = c_000000.withOpacity(.15);
|
||||
static Color c_000000_opacity12 = c_000000.withOpacity(.12);
|
||||
static Color c_000000_opacity4 = c_000000.withOpacity(.04);
|
||||
|
||||
static TextStyle ts_FFFFFF_21sp = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 21.sp,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_20sp_medium = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_18sp_medium = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_17sp = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 17.sp,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_opacity70_17sp = TextStyle(
|
||||
color: c_FFFFFF_opacity70,
|
||||
fontSize: 17.sp,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_17sp_semibold = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_17sp_medium = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_16sp = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 16.sp,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_14sp = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 14.sp,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_opacity70_14sp = TextStyle(
|
||||
color: c_FFFFFF_opacity70,
|
||||
fontSize: 14.sp,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_14sp_medium = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_12sp = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 12.sp,
|
||||
);
|
||||
static TextStyle ts_FFFFFF_10sp = TextStyle(
|
||||
color: c_FFFFFF,
|
||||
fontSize: 10.sp,
|
||||
);
|
||||
|
||||
static TextStyle ts_8E9AB0_10sp_semibold = TextStyle(
|
||||
color: c_8E9AB0,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
static TextStyle ts_8E9AB0_10sp = TextStyle(
|
||||
color: c_8E9AB0,
|
||||
fontSize: 10.sp,
|
||||
);
|
||||
static TextStyle ts_8E9AB0_12sp = TextStyle(
|
||||
color: c_8E9AB0,
|
||||
fontSize: 12.sp,
|
||||
);
|
||||
static TextStyle ts_8E9AB0_13sp = TextStyle(
|
||||
color: c_8E9AB0,
|
||||
fontSize: 13.sp,
|
||||
);
|
||||
static TextStyle ts_8E9AB0_14sp = TextStyle(
|
||||
color: c_8E9AB0,
|
||||
fontSize: 14.sp,
|
||||
);
|
||||
static TextStyle ts_8E9AB0_15sp = TextStyle(
|
||||
color: c_8E9AB0,
|
||||
fontSize: 15.sp,
|
||||
);
|
||||
static TextStyle ts_8E9AB0_16sp = TextStyle(
|
||||
color: c_8E9AB0,
|
||||
fontSize: 16.sp,
|
||||
);
|
||||
static TextStyle ts_8E9AB0_17sp = TextStyle(
|
||||
color: c_8E9AB0,
|
||||
fontSize: 17.sp,
|
||||
);
|
||||
static TextStyle ts_8E9AB0_opacity50_17sp = TextStyle(
|
||||
color: c_8E9AB0_opacity50,
|
||||
fontSize: 17.sp,
|
||||
);
|
||||
|
||||
static TextStyle ts_0C1C33_10sp = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 10.sp,
|
||||
);
|
||||
static TextStyle ts_0C1C33_12sp = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 12.sp,
|
||||
);
|
||||
static TextStyle ts_0C1C33_12sp_medium = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_0C1C33_14sp = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 14.sp,
|
||||
);
|
||||
static TextStyle ts_0C1C33_14sp_medium = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_0C1C33_17sp = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 17.sp,
|
||||
);
|
||||
static TextStyle ts_0C1C33_17sp_medium = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_0C1C33_17sp_semibold = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
static TextStyle ts_0C1C33_20sp = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 20.sp,
|
||||
);
|
||||
static TextStyle ts_0C1C33_20sp_medium = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_0C1C33_20sp_semibold = TextStyle(
|
||||
color: c_0C1C33,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
static TextStyle ts_0089FF_10sp_semibold = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
static TextStyle ts_0089FF_10sp = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 10.sp,
|
||||
);
|
||||
static TextStyle ts_0089FF_12sp = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 12.sp,
|
||||
);
|
||||
static TextStyle ts_0089FF_14sp = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 14.sp,
|
||||
);
|
||||
static TextStyle ts_0089FF_16sp = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 16.sp,
|
||||
);
|
||||
static TextStyle ts_0089FF_16sp_medium = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_0089FF_17sp = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 17.sp,
|
||||
);
|
||||
static TextStyle ts_0089FF_17sp_semibold = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
static TextStyle ts_0089FF_17sp_medium = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_0089FF_14sp_medium = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
|
||||
static TextStyle ts_0089FF_22sp_semibold = TextStyle(
|
||||
color: c_0089FF,
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
static TextStyle ts_FF381F_17sp = TextStyle(
|
||||
color: c_FF381F,
|
||||
fontSize: 17.sp,
|
||||
);
|
||||
static TextStyle ts_FF381F_14sp = TextStyle(
|
||||
color: c_FF381F,
|
||||
fontSize: 14.sp,
|
||||
);
|
||||
static TextStyle ts_FF381F_12sp = TextStyle(
|
||||
color: c_FF381F,
|
||||
fontSize: 12.sp,
|
||||
);
|
||||
static TextStyle ts_FF381F_10sp = TextStyle(
|
||||
color: c_FF381F,
|
||||
fontSize: 10.sp,
|
||||
);
|
||||
|
||||
static TextStyle ts_6085B1_17sp_medium = TextStyle(
|
||||
color: c_6085B1,
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle ts_6085B1_17sp = TextStyle(
|
||||
color: c_6085B1,
|
||||
fontSize: 17.sp,
|
||||
);
|
||||
static TextStyle ts_6085B1_12sp = TextStyle(
|
||||
color: c_6085B1,
|
||||
fontSize: 12.sp,
|
||||
);
|
||||
static TextStyle ts_6085B1_14sp = TextStyle(
|
||||
color: c_6085B1,
|
||||
fontSize: 14.sp,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'config.dart';
|
||||
|
||||
class Urls {
|
||||
static final onlineStatus = "${Config.imApiUrl}/manager/get_users_online_status";
|
||||
static final queryAllUsers = "${Config.imApiUrl}/manager/get_all_users_uid";
|
||||
static final updateUserInfo = "${Config.appAuthUrl}/user/update";
|
||||
static final searchFriendInfo = "${Config.appAuthUrl}/friend/search";
|
||||
static final getUsersFullInfo = "${Config.appAuthUrl}/user/find/full";
|
||||
static final searchUserFullInfo = "${Config.appAuthUrl}/user/search/full";
|
||||
|
||||
static final getVerificationCode = "${Config.appAuthUrl}/account/code/send";
|
||||
static final checkVerificationCode = "${Config.appAuthUrl}/account/code/verify";
|
||||
static final register = "${Config.appAuthUrl}/account/register";
|
||||
|
||||
static final resetPwd = "${Config.appAuthUrl}/account/password/reset";
|
||||
static final changePwd = "${Config.appAuthUrl}/account/password/change";
|
||||
static final login = "${Config.appAuthUrl}/account/login";
|
||||
|
||||
static final upgrade = "${Config.appAuthUrl}/app/check";
|
||||
static final getClientConfig = '${Config.appAuthUrl}/client_config/get';
|
||||
static final getTokenForRTC = "${Config.appAuthUrl}/user/rtc/get_token";
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:talker_dio_logger/talker_dio_logger.dart';
|
||||
|
||||
class ApiService {
|
||||
static final _instance = ApiService._internal();
|
||||
|
||||
factory ApiService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
final Dio dio = Dio();
|
||||
|
||||
ApiService._internal() {
|
||||
final talkerDioLogger = TalkerDioLogger(
|
||||
settings: const TalkerDioLoggerSettings(
|
||||
printRequestHeaders: kDebugMode,
|
||||
printRequestData: kDebugMode,
|
||||
printResponseMessage: kDebugMode,
|
||||
printResponseData: kDebugMode,
|
||||
printResponseHeaders: false,
|
||||
),
|
||||
);
|
||||
|
||||
dio.options
|
||||
..connectTimeout = const Duration(seconds: 30)
|
||||
..receiveTimeout = const Duration(seconds: 30);
|
||||
|
||||
dio.interceptors
|
||||
..add(talkerDioLogger)
|
||||
..add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
return handler.next(options); //continue
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
return handler.next(response); // continue
|
||||
},
|
||||
onError: (DioException e, handler) {
|
||||
return handler.next(e); //continue
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
dio.options.connectTimeout = const Duration(seconds: 30);
|
||||
dio.options.receiveTimeout = const Duration(seconds: 30);
|
||||
}
|
||||
|
||||
void setBaseUrl(String baseUrl) {
|
||||
dio.options.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
void setToken(String token) {
|
||||
dio.options.headers['token'] = token;
|
||||
}
|
||||
|
||||
Future get(String path, {Map<String, dynamic>? queryParams}) async {
|
||||
try {
|
||||
dio.options.headers['operationID'] = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
final response = await dio.get(path, queryParameters: queryParams);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final result = ApiResponse.fromJson(response.data as Map<String, dynamic>);
|
||||
|
||||
if (result.errCode == 0) {
|
||||
return result.data;
|
||||
} else {
|
||||
return Future.error(result.errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
return Future.error(response.data);
|
||||
} on DioException catch (e) {
|
||||
_handleError(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future post(String path, {Map<String, dynamic>? data, String? token}) async {
|
||||
try {
|
||||
final operationID = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
dio.options.headers['operationID'] = operationID;
|
||||
|
||||
final response = await dio.post(path, data: data);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final result = ApiResponse.fromJson(response.data as Map<String, dynamic>);
|
||||
|
||||
if (result.errCode == 0) {
|
||||
return result.data;
|
||||
} else {
|
||||
final exception = ApiException(code: result.errCode, message: result.errMsg, operationID: operationID);
|
||||
|
||||
return Future.error(exception);
|
||||
}
|
||||
}
|
||||
|
||||
return Future.error(response.data);
|
||||
} on DioException catch (e) {
|
||||
_handleError(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future download(
|
||||
String url, {
|
||||
required String cachePath,
|
||||
CancelToken? cancelToken,
|
||||
Function(int count, int total)? onProgress,
|
||||
}) {
|
||||
return dio.download(
|
||||
url,
|
||||
cachePath,
|
||||
cancelToken: cancelToken,
|
||||
onReceiveProgress: onProgress,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleError(DioException error) {
|
||||
Logger.print('DioError: ${error.message}', isError: true);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiResponse {
|
||||
int errCode;
|
||||
String errMsg;
|
||||
String errDlt;
|
||||
dynamic data;
|
||||
|
||||
ApiResponse.fromJson(Map<String, dynamic> map)
|
||||
: errCode = map["errCode"] ?? -1,
|
||||
errMsg = map["errMsg"] ?? '',
|
||||
errDlt = map["errDlt"] ?? '',
|
||||
data = map["data"];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['errCode'] = errCode;
|
||||
data['errMsg'] = errMsg;
|
||||
data['errDlt'] = errDlt;
|
||||
data['data'] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiException implements Exception {
|
||||
final int code;
|
||||
final String? message;
|
||||
final String? operationID;
|
||||
|
||||
ApiException({required this.code, this.message, this.operationID});
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:sprintf/sprintf.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class DataSp {
|
||||
static const _loginCertificate = 'loginCertificate';
|
||||
static const _loginAccount = 'loginAccount';
|
||||
static const _server = "server";
|
||||
static const _ip = 'ip';
|
||||
static const _deviceID = 'deviceID';
|
||||
static const _ignoreUpdate = 'ignoreUpdate';
|
||||
static const _language = "language";
|
||||
static const _groupApplication = "%s_groupApplication";
|
||||
static const _friendApplication = "%s_friendApplication";
|
||||
|
||||
static const _screenPassword = '%s_screenPassword';
|
||||
static const _enabledBiometric = '%s_enabledBiometric';
|
||||
static const _chatFontSizeFactor = '%s_chatFontSizeFactor';
|
||||
static const _chatBackground = '%s_chatBackground_%s';
|
||||
static const _loginType = 'loginType';
|
||||
static const _meetingInProgress = '%_meetingInProgress';
|
||||
|
||||
DataSp._();
|
||||
|
||||
static init() async {
|
||||
await SpUtil().init();
|
||||
}
|
||||
|
||||
static String getKey(String key, {String key2 = ""}) {
|
||||
return sprintf(key, [OpenIM.iMManager.userID, key2]);
|
||||
}
|
||||
|
||||
static String? get imToken => getLoginCertificate()?.imToken;
|
||||
|
||||
static String? get chatToken => getLoginCertificate()?.chatToken;
|
||||
|
||||
static String? get userID => getLoginCertificate()?.userID;
|
||||
|
||||
static Future<bool>? putLoginCertificate(LoginCertificate lc) {
|
||||
return SpUtil().putObject(_loginCertificate, lc);
|
||||
}
|
||||
|
||||
static Future<bool>? putLoginAccount(Map map) {
|
||||
return SpUtil().putObject(_loginAccount, map);
|
||||
}
|
||||
|
||||
static LoginCertificate? getLoginCertificate() {
|
||||
return SpUtil().getObj(_loginCertificate, (v) => LoginCertificate.fromJson(v.cast()));
|
||||
}
|
||||
|
||||
static Future<bool>? removeLoginCertificate() {
|
||||
return SpUtil().remove(_loginCertificate);
|
||||
}
|
||||
|
||||
static Map? getLoginAccount() {
|
||||
return SpUtil().getObject(_loginAccount);
|
||||
}
|
||||
|
||||
static Future<bool>? putServerConfig(Map<String, String> config) {
|
||||
return SpUtil().putObject(_server, config);
|
||||
}
|
||||
|
||||
static Map? getServerConfig() {
|
||||
return SpUtil().getObject(_server);
|
||||
}
|
||||
|
||||
static Future<bool>? putServerIP(String ip) {
|
||||
return SpUtil().putString(ip, ip);
|
||||
}
|
||||
|
||||
static String? getServerIP() {
|
||||
return SpUtil().getString(_ip);
|
||||
}
|
||||
|
||||
static String getDeviceID() {
|
||||
String id = SpUtil().getString(_deviceID) ?? '';
|
||||
if (id.isEmpty) {
|
||||
id = const Uuid().v4();
|
||||
SpUtil().putString(_deviceID, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<bool>? putIgnoreVersion(String version) {
|
||||
return SpUtil().putString(_ignoreUpdate, version);
|
||||
}
|
||||
|
||||
static String? getIgnoreVersion() {
|
||||
return SpUtil().getString(_ignoreUpdate);
|
||||
}
|
||||
|
||||
static Future<bool>? putLanguage(int index) {
|
||||
return SpUtil().putInt(_language, index);
|
||||
}
|
||||
|
||||
static int? getLanguage() {
|
||||
return SpUtil().getInt(_language);
|
||||
}
|
||||
|
||||
static Future<bool>? putHaveReadUnHandleGroupApplication(List<String> idList) {
|
||||
return SpUtil().putStringList(getKey(_groupApplication), idList);
|
||||
}
|
||||
|
||||
static Future<bool>? putHaveReadUnHandleFriendApplication(List<String> idList) {
|
||||
return SpUtil().putStringList(getKey(_friendApplication), idList);
|
||||
}
|
||||
|
||||
static List<String>? getHaveReadUnHandleGroupApplication() {
|
||||
return SpUtil().getStringList(getKey(_groupApplication), defValue: []);
|
||||
}
|
||||
|
||||
static List<String>? getHaveReadUnHandleFriendApplication() {
|
||||
return SpUtil().getStringList(getKey(_friendApplication), defValue: []);
|
||||
}
|
||||
|
||||
static Future<bool>? putLockScreenPassword(String password) {
|
||||
return SpUtil().putString(getKey(_screenPassword), password);
|
||||
}
|
||||
|
||||
static Future<bool>? clearLockScreenPassword() {
|
||||
return SpUtil().remove(getKey(_screenPassword));
|
||||
}
|
||||
|
||||
static String? getLockScreenPassword() {
|
||||
return SpUtil().getString(getKey(_screenPassword), defValue: null);
|
||||
}
|
||||
|
||||
static Future<bool>? openBiometric() {
|
||||
return SpUtil().putBool(getKey(_enabledBiometric), true);
|
||||
}
|
||||
|
||||
static bool? isEnabledBiometric() {
|
||||
return SpUtil().getBool(getKey(_enabledBiometric), defValue: null);
|
||||
}
|
||||
|
||||
static Future<bool>? closeBiometric() {
|
||||
return SpUtil().remove(getKey(_enabledBiometric));
|
||||
}
|
||||
|
||||
static Future<bool>? putChatFontSizeFactor(double factor) {
|
||||
return SpUtil().putDouble(getKey(_chatFontSizeFactor), factor);
|
||||
}
|
||||
|
||||
static double getChatFontSizeFactor() {
|
||||
return SpUtil().getDouble(
|
||||
getKey(_chatFontSizeFactor),
|
||||
defValue: Config.textScaleFactor,
|
||||
)!;
|
||||
}
|
||||
|
||||
static Future<bool>? putChatBackground(String toUid, String path) {
|
||||
return SpUtil().putString(getKey(_chatBackground, key2: toUid), path);
|
||||
}
|
||||
|
||||
static String? getChatBackground(String toUid) {
|
||||
return SpUtil().getString(getKey(_chatBackground, key2: toUid));
|
||||
}
|
||||
|
||||
static Future<bool>? clearChatBackground(String toUid) {
|
||||
return SpUtil().remove(getKey(_chatBackground, key2: toUid));
|
||||
}
|
||||
|
||||
static Future<bool>? putLoginType(int type) {
|
||||
return SpUtil().putInt(_loginType, type);
|
||||
}
|
||||
|
||||
static int getLoginType() {
|
||||
return SpUtil().getInt(_loginType) ?? 0;
|
||||
}
|
||||
|
||||
static Future<bool>? putMeetingInProgress(String meetingID) {
|
||||
return SpUtil().putString(getKey(_meetingInProgress), meetingID);
|
||||
}
|
||||
|
||||
static String? getMeetingInProgress() {
|
||||
return SpUtil().getString(
|
||||
getKey(_meetingInProgress),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool>? removeMeetingInProgress() {
|
||||
return SpUtil().remove(getKey(_meetingInProgress));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:talker_dio_logger/talker_dio_logger.dart';
|
||||
|
||||
var dio = Dio();
|
||||
|
||||
class HttpUtil {
|
||||
HttpUtil._();
|
||||
|
||||
static void init() {
|
||||
dio
|
||||
..interceptors.add(
|
||||
TalkerDioLogger(
|
||||
settings: const TalkerDioLoggerSettings(
|
||||
printRequestHeaders: kDebugMode,
|
||||
printRequestData: kDebugMode,
|
||||
printResponseMessage: kDebugMode,
|
||||
printResponseData: kDebugMode,
|
||||
printResponseHeaders: kDebugMode,
|
||||
),
|
||||
),
|
||||
)
|
||||
..interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
|
||||
return handler.next(options); //continue
|
||||
}, onResponse: (response, handler) {
|
||||
return handler.next(response); // continue
|
||||
}, onError: (DioError e, handler) {
|
||||
return handler.next(e); //continue
|
||||
}));
|
||||
|
||||
dio.options.baseUrl = Config.imApiUrl;
|
||||
dio.options.connectTimeout = const Duration(seconds: 30); //30s
|
||||
dio.options.receiveTimeout = const Duration(seconds: 30);
|
||||
}
|
||||
|
||||
static String get operationID => DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
static Future post(
|
||||
String path, {
|
||||
dynamic data,
|
||||
bool showErrorToast = true,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
try {
|
||||
data ??= {};
|
||||
options ??= Options();
|
||||
options.headers ??= {};
|
||||
options.headers!['operationID'] = operationID;
|
||||
|
||||
var result = await dio.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
var resp = ApiResp.fromJson(result.data!);
|
||||
if (resp.errCode == 0) {
|
||||
return resp.data;
|
||||
} else {
|
||||
if (showErrorToast) {
|
||||
IMViews.showToast(resp.errDlt);
|
||||
}
|
||||
|
||||
return Future.error((resp.errCode, resp.errMsg));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error is DioException) {
|
||||
final errorMsg = '接口:$path 信息:${error.message}';
|
||||
if (showErrorToast) IMViews.showToast(errorMsg);
|
||||
return Future.error(errorMsg);
|
||||
}
|
||||
final errorMsg = '接口:$path 信息:${error.toString()}';
|
||||
if (showErrorToast) IMViews.showToast(errorMsg);
|
||||
return Future.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> uploadImageForMinio({
|
||||
required String path,
|
||||
bool compress = true,
|
||||
}) async {
|
||||
String fileName = path.substring(path.lastIndexOf("/") + 1);
|
||||
|
||||
String? compressPath;
|
||||
if (compress) {
|
||||
File? compressFile = await IMUtils.compressImageAndGetFile(File(path));
|
||||
compressPath = compressFile?.path;
|
||||
Logger.print('compressPath: $compressPath');
|
||||
}
|
||||
final bytes = await File(compressPath ?? path).readAsBytes();
|
||||
final mf = MultipartFile.fromBytes(bytes, filename: fileName);
|
||||
|
||||
var formData =
|
||||
FormData.fromMap({'operationID': '${DateTime.now().millisecondsSinceEpoch}', 'fileType': 1, 'file': mf});
|
||||
|
||||
var resp = await dio.post<Map<String, dynamic>>(
|
||||
"${Config.imApiUrl}/third/minio_upload",
|
||||
data: formData,
|
||||
options: Options(headers: {'token': DataSp.imToken}),
|
||||
);
|
||||
return resp.data?['data']['URL'];
|
||||
}
|
||||
|
||||
static Future download(
|
||||
String url, {
|
||||
required String cachePath,
|
||||
CancelToken? cancelToken,
|
||||
Function(int count, int total)? onProgress,
|
||||
}) {
|
||||
return dio.download(
|
||||
url,
|
||||
cachePath,
|
||||
options: Options(
|
||||
receiveTimeout: const Duration(minutes: 10),
|
||||
),
|
||||
cancelToken: cancelToken,
|
||||
onReceiveProgress: onProgress,
|
||||
);
|
||||
}
|
||||
|
||||
static Future saveUrlPicture(
|
||||
String url, {
|
||||
CancelToken? cancelToken,
|
||||
Function(int count, int total)? onProgress,
|
||||
VoidCallback? onCompletion,
|
||||
}) async {
|
||||
final name = url.substring(url.lastIndexOf('/') + 1);
|
||||
final cachePath = await IMUtils.createTempFile(dir: 'picture', name: name);
|
||||
var intervalDo = IntervalDo();
|
||||
|
||||
return download(
|
||||
url,
|
||||
cachePath: cachePath,
|
||||
cancelToken: cancelToken,
|
||||
onProgress: (int count, int total) async {
|
||||
onProgress?.call(count, total);
|
||||
if (total == -1) {
|
||||
onCompletion?.call();
|
||||
intervalDo.drop(
|
||||
fun: () async {
|
||||
saveFileToGallerySaver(File(cachePath), showTaost: EasyLoading.isShow);
|
||||
},
|
||||
milliseconds: 1500);
|
||||
}
|
||||
if (count == total) {
|
||||
saveFileToGallerySaver(File(cachePath), showTaost: EasyLoading.isShow);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future saveImage(Image image) async {
|
||||
var byteData = await image.toByteData(format: ImageByteFormat.png);
|
||||
if (byteData != null) {
|
||||
Uint8List uint8list = byteData.buffer.asUint8List();
|
||||
var result = await ImageGallerySaverPlus.saveImage(Uint8List.fromList(uint8list));
|
||||
if (result != null) {
|
||||
var tips = StrRes.saveSuccessfully;
|
||||
if (Platform.isAndroid) {
|
||||
final filePath = result['filePath'].split('//').last;
|
||||
tips = '${StrRes.saveSuccessfully}:$filePath';
|
||||
}
|
||||
IMViews.showToast(tips);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future saveUrlVideo(
|
||||
String url, {
|
||||
CancelToken? cancelToken,
|
||||
Function(int count, int total)? onProgress,
|
||||
VoidCallback? onCompletion,
|
||||
}) async {
|
||||
final name = url.substring(url.lastIndexOf('/') + 1);
|
||||
final cachePath = await IMUtils.createTempFile(dir: 'video', name: name);
|
||||
|
||||
if (File(cachePath).existsSync()) {
|
||||
onCompletion?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
return download(
|
||||
url,
|
||||
cachePath: cachePath,
|
||||
cancelToken: cancelToken,
|
||||
onProgress: (int count, int total) async {
|
||||
onProgress?.call(count, total);
|
||||
if (count == total) {
|
||||
onCompletion?.call();
|
||||
final result = await ImageGallerySaverPlus.saveFile(cachePath);
|
||||
if (result != null) {
|
||||
var tips = StrRes.saveSuccessfully;
|
||||
if (Platform.isAndroid) {
|
||||
final filePath = result['filePath'].split('//').last;
|
||||
tips = '${StrRes.saveSuccessfully}:$filePath';
|
||||
}
|
||||
IMViews.showToast(tips);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future saveFileToGallerySaver(File file, {String? name, bool showTaost = true}) async {
|
||||
Permissions.storage(() async {
|
||||
var tips = StrRes.saveSuccessfully;
|
||||
Logger.print('saveFileToGallerySaver: ${file.path}');
|
||||
final imageBytes = await file.readAsBytes();
|
||||
|
||||
final result = await ImageGallerySaverPlus.saveImage(imageBytes, name: name);
|
||||
if (result != null && showTaost) {
|
||||
if (Platform.isAndroid) {
|
||||
final filePath = result['filePath'].split('//').last;
|
||||
tips = '${StrRes.saveSuccessfully}:$filePath';
|
||||
}
|
||||
IMViews.showToast(tips);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ImageUtil {
|
||||
ImageUtil._();
|
||||
|
||||
static const _package = "openim_common";
|
||||
|
||||
static Widget assetImage(
|
||||
String res, {
|
||||
double? width,
|
||||
double? height,
|
||||
BoxFit? fit,
|
||||
Color? color,
|
||||
}) =>
|
||||
Image.asset(
|
||||
res,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
color: color,
|
||||
package: _package,
|
||||
);
|
||||
|
||||
static Widget networkImage({
|
||||
required String url,
|
||||
double? width,
|
||||
double? height,
|
||||
int? cacheWidth,
|
||||
int? cacheHeight,
|
||||
BoxFit? fit,
|
||||
bool loadProgress = true,
|
||||
bool clearMemoryCacheWhenDispose = false,
|
||||
bool lowMemory = false,
|
||||
Widget? errorWidget,
|
||||
BorderRadius? borderRadius,
|
||||
}) =>
|
||||
ExtendedImage.network(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
borderRadius: borderRadius,
|
||||
cacheWidth: _calculateCacheWidth(width, cacheWidth, lowMemory),
|
||||
cacheHeight: _calculateCacheHeight(height, cacheHeight, lowMemory),
|
||||
cacheRawData: true,
|
||||
clearMemoryCacheWhenDispose: clearMemoryCacheWhenDispose,
|
||||
handleLoadingProgress: true,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
{
|
||||
final ImageChunkEvent? loadingProgress = state.loadingProgress;
|
||||
final double? progress = loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||
: null;
|
||||
|
||||
return SizedBox(
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
child: loadProgress
|
||||
? Center(
|
||||
child: SizedBox(
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
value: progress,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
case LoadState.completed:
|
||||
return null;
|
||||
case LoadState.failed:
|
||||
state.imageProvider.evict();
|
||||
return errorWidget ??
|
||||
(ImageRes.pictureError.toImage
|
||||
..width = width
|
||||
..height = height);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
static Widget fileImage({
|
||||
required File file,
|
||||
double? width,
|
||||
double? height,
|
||||
int? cacheWidth,
|
||||
int? cacheHeight,
|
||||
BoxFit? fit,
|
||||
bool loadProgress = true,
|
||||
bool clearMemoryCacheWhenDispose = false,
|
||||
bool lowMemory = false,
|
||||
Widget? errorWidget,
|
||||
BorderRadius? borderRadius,
|
||||
}) =>
|
||||
ExtendedImage.file(
|
||||
file,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
borderRadius: borderRadius,
|
||||
cacheWidth: _calculateCacheWidth(width, cacheWidth, lowMemory),
|
||||
cacheHeight: _calculateCacheHeight(height, cacheHeight, lowMemory),
|
||||
clearMemoryCacheWhenDispose: clearMemoryCacheWhenDispose,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
cacheRawData: true,
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
{
|
||||
final ImageChunkEvent? loadingProgress = state.loadingProgress;
|
||||
final double? progress = loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||
: null;
|
||||
|
||||
return SizedBox(
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
child: loadProgress
|
||||
? Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
value: progress,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
case LoadState.completed:
|
||||
return null;
|
||||
case LoadState.failed:
|
||||
state.imageProvider.evict();
|
||||
return errorWidget ?? ImageRes.pictureError.toImage;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
static int? _calculateCacheWidth(
|
||||
double? width,
|
||||
int? cacheWidth,
|
||||
bool lowMemory,
|
||||
) {
|
||||
if (!lowMemory) return null;
|
||||
if (null != cacheWidth) return cacheWidth;
|
||||
final maxW = .6.sw;
|
||||
return (width == null ? maxW : (width < maxW ? width : maxW)).toInt();
|
||||
}
|
||||
|
||||
static int? _calculateCacheHeight(
|
||||
double? height,
|
||||
int? cacheHeight,
|
||||
bool lowMemory,
|
||||
) {
|
||||
if (!lowMemory) return null;
|
||||
if (null != cacheHeight) return cacheHeight;
|
||||
final maxH = .6.sh;
|
||||
return (height == null ? maxH : (height < maxH ? height : maxH)).toInt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
|
||||
class Logger {
|
||||
static Logger? _instance;
|
||||
|
||||
factory Logger() {
|
||||
final instance = _instance ??= Logger._();
|
||||
instance._setPlatformInfo();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
Logger._();
|
||||
|
||||
void _setPlatformInfo() async {
|
||||
final pkg = DeviceInfoPlugin();
|
||||
final deviceInfo = await pkg.deviceInfo;
|
||||
|
||||
if (deviceInfo is AndroidDeviceInfo) {
|
||||
final apiVersion = deviceInfo.version.sdkInt;
|
||||
|
||||
_header = '[*flutter*Android/$apiVersion]';
|
||||
} else if (deviceInfo is IosDeviceInfo) {
|
||||
final osVersion = deviceInfo.systemVersion;
|
||||
|
||||
_header = '[*flutter*iOS/$osVersion]';
|
||||
}
|
||||
}
|
||||
|
||||
String _header = '*flutter*iOS';
|
||||
|
||||
static void print(dynamic text,
|
||||
{bool isError = false,
|
||||
String? fileName,
|
||||
String? functionName,
|
||||
String? errorMsg,
|
||||
List<dynamic>? keyAndValues,
|
||||
bool onlyConsole = false}) {
|
||||
final time = DateTime.now().toIso8601String();
|
||||
|
||||
log(
|
||||
'$time ${Logger()._header} [Console]: $text, ${keyAndValues != null ? ', $keyAndValues' : ''}, isError [${isError || errorMsg != null}]',
|
||||
);
|
||||
if (!onlyConsole) {
|
||||
OpenIM.iMManager.logs(
|
||||
msgs:
|
||||
'$time ${Logger()._header} [${functionName ?? ''}]: $text, ${keyAndValues != null ? ', $keyAndValues' : ''}',
|
||||
err: errorMsg,
|
||||
keyAndValues: keyAndValues ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../openim_common.dart';
|
||||
|
||||
class MultiThreadDownloader {
|
||||
final Dio dio = Dio();
|
||||
final String url;
|
||||
final int threads; // Number of threads
|
||||
final String fileName;
|
||||
final int? length;
|
||||
|
||||
MultiThreadDownloader({required this.url, this.threads = 4, required this.fileName, this.length});
|
||||
|
||||
String? _realUrl;
|
||||
late int _fileSize; // Total file size
|
||||
final CancelToken _cancelToken = CancelToken();
|
||||
|
||||
Future<String?> start() async {
|
||||
_fileSize = await _getFileSize() ?? 0;
|
||||
if (_fileSize == 0) {
|
||||
Logger.print('Unable to retrieve file size');
|
||||
return null;
|
||||
}
|
||||
Logger.print('File size: $_fileSize bytes');
|
||||
|
||||
Directory appDocDir = await getApplicationDocumentsDirectory();
|
||||
String filePath = '${appDocDir.path}/$fileName';
|
||||
|
||||
final chunkSize = (_fileSize / threads).ceil(); // Size of each chunk
|
||||
|
||||
List<Future<File>> futures = [];
|
||||
|
||||
for (int i = 0; i < threads; i++) {
|
||||
final start = i * chunkSize;
|
||||
final end = (i == threads - 1) ? _fileSize - 1 : (start + chunkSize - 1);
|
||||
|
||||
futures.add(_downloadChunk(start, end, i, filePath));
|
||||
}
|
||||
|
||||
await Future.wait(futures);
|
||||
|
||||
Logger.print('All chunks downloaded, file path: $filePath');
|
||||
final path = await mergeChunks(filePath); // Merge chunks into a single file
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
Future<int?> _getFileSize() async {
|
||||
try {
|
||||
_realUrl = await fetchRedirectedUrl(url: url);
|
||||
Logger.print('get file read url: url $_realUrl');
|
||||
|
||||
if (length != null) {
|
||||
return length;
|
||||
}
|
||||
|
||||
final response = await dio.head(
|
||||
_realUrl!,
|
||||
);
|
||||
|
||||
final contentLength = response.headers.value(Headers.contentLengthHeader);
|
||||
return contentLength != null ? int.tryParse(contentLength) : null;
|
||||
} catch (e) {
|
||||
Logger.print('Failed to get file size: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> _downloadChunk(int start, int end, int threadIndex, String filePath) async {
|
||||
String tempFilePath = '$filePath.part$threadIndex';
|
||||
|
||||
Logger.print('Thread $threadIndex downloading range: $start-$end');
|
||||
|
||||
try {
|
||||
final response = await dio.download(
|
||||
_realUrl!,
|
||||
tempFilePath,
|
||||
options: Options(
|
||||
headers: {
|
||||
'Range': 'bytes=$start-$end',
|
||||
},
|
||||
),
|
||||
cancelToken: _cancelToken,
|
||||
);
|
||||
Logger.print('Thread $threadIndex download completed: ${response.statusCode}');
|
||||
return File(tempFilePath);
|
||||
} catch (e) {
|
||||
Logger.print('Thread $threadIndex download failed: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> mergeChunks(String filePath) async {
|
||||
File file = File(filePath);
|
||||
IOSink fileSink = file.openWrite();
|
||||
|
||||
try {
|
||||
for (int i = 0; i < threads; i++) {
|
||||
File chunkFile = File('$filePath.part$i');
|
||||
List<int> chunkBytes = await chunkFile.readAsBytes();
|
||||
fileSink.add(chunkBytes);
|
||||
await chunkFile.delete(); // Delete temporary chunk file after merging
|
||||
}
|
||||
} finally {
|
||||
await fileSink.close();
|
||||
}
|
||||
|
||||
Logger.print('File merge completed: $filePath');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
Future<String> fetchRedirectedUrl({required String url}) async {
|
||||
final myRequest = await HttpClient().getUrl(Uri.parse(url));
|
||||
myRequest.followRedirects = false;
|
||||
final myResponse = await myRequest.close();
|
||||
return myResponse.headers.value(HttpHeaders.locationHeader).toString();
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
_cancelToken.cancel('Download cancelled');
|
||||
Logger.print('Download cancelled');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
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;
|
||||
}
|
||||
|
||||
static void camera(Function()? onGranted) async {
|
||||
if (await Permission.camera.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.camera.isPermanentlyDenied || await Permission.camera.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.camera.title);
|
||||
}
|
||||
}
|
||||
|
||||
static void storage(Function()? onGranted) async {
|
||||
if (!Platform.isAndroid) {
|
||||
onGranted?.call();
|
||||
} else {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
late Permission permisson;
|
||||
|
||||
if (androidInfo.version.sdkInt <= 32) {
|
||||
permisson = Permission.storage;
|
||||
} else {
|
||||
permisson = Permission.manageExternalStorage;
|
||||
}
|
||||
if (await permisson.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await permisson.isPermanentlyDenied || await permisson.isDenied) {
|
||||
_showPermissionDeniedDialog(permisson.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void manageExternalStorage(Function()? onGranted) async {
|
||||
if (await Permission.manageExternalStorage.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.storage.isPermanentlyDenied || await Permission.storage.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.storage.title);
|
||||
}
|
||||
}
|
||||
|
||||
static void microphone(Function()? onGranted) async {
|
||||
if (await Permission.microphone.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
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) {
|
||||
_showPermissionDeniedDialog(Permission.speech.title);
|
||||
}
|
||||
}
|
||||
|
||||
static void photos(Function()? onGranted) async {
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
if (androidInfo.version.sdkInt <= 32) {
|
||||
storage(onGranted);
|
||||
} else {
|
||||
if (await Permission.photos.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.photos.isPermanentlyDenied || await Permission.photos.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.photos.title);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (await Permission.photos.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.photos.isPermanentlyDenied || await Permission.photos.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.photos.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> notification() async {
|
||||
if (await Permission.notification.request().isGranted) {
|
||||
return true;
|
||||
}
|
||||
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,
|
||||
Permission.microphone,
|
||||
];
|
||||
bool isAllGranted = true;
|
||||
var msg = '';
|
||||
|
||||
for (var permission in permissions) {
|
||||
final state = await permission.request();
|
||||
isAllGranted = isAllGranted && state.isGranted;
|
||||
if (!state.isGranted) {
|
||||
msg += '${permission.title}、';
|
||||
}
|
||||
}
|
||||
if (isAllGranted) {
|
||||
onGranted?.call();
|
||||
} else {
|
||||
msg = msg.substring(0, msg.length - 1);
|
||||
_showPermissionDeniedDialog(msg);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> media() async {
|
||||
final permissions = [
|
||||
Permission.camera,
|
||||
Permission.microphone,
|
||||
];
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
if (androidInfo.version.sdkInt <= 32) {
|
||||
permissions.add(Permission.storage);
|
||||
} else {
|
||||
permissions.add(Permission.photos);
|
||||
}
|
||||
} else {
|
||||
permissions.add(Permission.photos);
|
||||
}
|
||||
|
||||
bool isAllGranted = true;
|
||||
var msg = '';
|
||||
|
||||
for (var permission in permissions) {
|
||||
final state = await permission.request();
|
||||
isAllGranted = isAllGranted && state.isGranted;
|
||||
if (!state.isGranted) {
|
||||
msg += '${permission.title}、';
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAllGranted) {
|
||||
msg = msg.substring(0, msg.length - 1);
|
||||
_showPermissionDeniedDialog(msg);
|
||||
}
|
||||
|
||||
return isAllGranted;
|
||||
}
|
||||
|
||||
static void storageAndMicrophone(Function()? onGranted) async {
|
||||
final permissions = [
|
||||
Permission.microphone,
|
||||
];
|
||||
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
|
||||
if (androidInfo.version.sdkInt <= 32) {
|
||||
permissions.add(Permission.storage);
|
||||
} else {
|
||||
permissions.add(Permission.manageExternalStorage);
|
||||
}
|
||||
|
||||
bool isAllGranted = true;
|
||||
var msg = '';
|
||||
|
||||
for (var permission in permissions) {
|
||||
final state = await permission.request();
|
||||
isAllGranted = isAllGranted && state.isGranted;
|
||||
if (!state.isGranted) {
|
||||
msg += '${permission.title}、';
|
||||
}
|
||||
}
|
||||
if (isAllGranted) {
|
||||
onGranted?.call();
|
||||
} else {
|
||||
msg = msg.substring(0, msg.length - 1);
|
||||
_showPermissionDeniedDialog(msg);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<Permission, PermissionStatus>> request(List<Permission> permissions) async {
|
||||
Map<Permission, PermissionStatus> statuses = await permissions.request();
|
||||
return statuses;
|
||||
}
|
||||
|
||||
static void _showPermissionDeniedDialog(String tips) {
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(StrRes.permissionDeniedTitle),
|
||||
content: Text(
|
||||
sprintf(StrRes.permissionDeniedHint, [tips]),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(StrRes.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(StrRes.determine),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
openAppSettings();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension PermissionExt on Permission {
|
||||
String get title {
|
||||
switch (this) {
|
||||
case Permission.storage:
|
||||
return StrRes.externalStorage;
|
||||
case Permission.photos:
|
||||
return StrRes.gallery;
|
||||
case Permission.camera:
|
||||
return StrRes.camera;
|
||||
case Permission.microphone:
|
||||
return StrRes.microphone;
|
||||
case Permission.notification:
|
||||
return StrRes.notification;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SpUtil {
|
||||
SharedPreferences? prefs;
|
||||
|
||||
SpUtil._();
|
||||
|
||||
static final SpUtil _singleton = SpUtil._();
|
||||
|
||||
factory SpUtil() => _singleton;
|
||||
|
||||
Future init() async {
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
return prefs;
|
||||
}
|
||||
|
||||
Future<bool>? putObject(String key, Object value) {
|
||||
return prefs?.setString(key, json.encode(value));
|
||||
}
|
||||
|
||||
T? getObj<T>(String key, T Function(Map v) f, {T? defValue}) {
|
||||
final map = getObject(key);
|
||||
return map == null ? defValue : f(map);
|
||||
}
|
||||
|
||||
Map? getObject(String key) {
|
||||
final data = prefs?.getString(key);
|
||||
return (data == null || data.isEmpty) ? null : json.decode(data);
|
||||
}
|
||||
|
||||
Future<bool>? putObjectList(String key, List<Object> list) {
|
||||
final dataList = list.map((value) => json.encode(value)).toList();
|
||||
return prefs?.setStringList(key, dataList);
|
||||
}
|
||||
|
||||
List<T>? getObjList<T>(
|
||||
String key,
|
||||
T Function(Map v) f, {
|
||||
List<T>? defValue = const [],
|
||||
}) {
|
||||
List<Map>? dataList = getObjectList(key);
|
||||
List<T>? list = dataList?.map((value) => f(value)).toList();
|
||||
return list ?? defValue;
|
||||
}
|
||||
|
||||
List<Map>? getObjectList(String key) {
|
||||
List<String>? dataLis = prefs?.getStringList(key);
|
||||
return dataLis?.map((value) {
|
||||
Map dataMap = json.decode(value);
|
||||
return dataMap;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
String? getString(String key, {String? defValue = ''}) {
|
||||
return prefs?.getString(key) ?? defValue;
|
||||
}
|
||||
|
||||
Future<bool>? putString(String key, String value) {
|
||||
return prefs?.setString(key, value);
|
||||
}
|
||||
|
||||
bool? getBool(String key, {bool? defValue = false}) {
|
||||
return prefs?.getBool(key) ?? defValue;
|
||||
}
|
||||
|
||||
Future<bool>? putBool(String key, bool value) {
|
||||
return prefs?.setBool(key, value);
|
||||
}
|
||||
|
||||
int? getInt(String key, {int? defValue = 0}) {
|
||||
return prefs?.getInt(key) ?? defValue;
|
||||
}
|
||||
|
||||
Future<bool>? putInt(String key, int value) {
|
||||
return prefs?.setInt(key, value);
|
||||
}
|
||||
|
||||
double? getDouble(String key, {double? defValue = 0.0}) {
|
||||
return prefs?.getDouble(key) ?? defValue;
|
||||
}
|
||||
|
||||
Future<bool>? putDouble(String key, double value) {
|
||||
return prefs?.setDouble(key, value);
|
||||
}
|
||||
|
||||
List<String>? getStringList(String key, {List<String>? defValue = const []}) {
|
||||
return prefs?.getStringList(key) ?? defValue;
|
||||
}
|
||||
|
||||
Future<bool>? putStringList(String key, List<String> value) {
|
||||
return prefs?.setStringList(key, value);
|
||||
}
|
||||
|
||||
dynamic getDynamic(String key, {Object? defValue}) {
|
||||
return prefs?.get(key) ?? defValue;
|
||||
}
|
||||
|
||||
bool? haveKey(String key) {
|
||||
return prefs?.getKeys().contains(key);
|
||||
}
|
||||
|
||||
bool? containsKey(String key) {
|
||||
return prefs?.containsKey(key);
|
||||
}
|
||||
|
||||
Set<String>? getKeys() {
|
||||
return prefs?.getKeys();
|
||||
}
|
||||
|
||||
Future<bool>? remove(String key) {
|
||||
return prefs?.remove(key);
|
||||
}
|
||||
|
||||
Future<bool>? clear() {
|
||||
return prefs?.clear();
|
||||
}
|
||||
|
||||
Future<void>? reload() {
|
||||
return prefs?.reload();
|
||||
}
|
||||
|
||||
bool isInitialized() {
|
||||
return null != prefs;
|
||||
}
|
||||
|
||||
SharedPreferences? getSp() {
|
||||
return prefs;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
typedef RecordFc = Function(int sec, String path);
|
||||
|
||||
class VoiceRecord {
|
||||
static const _dir = "voice";
|
||||
static const _ext = ".m4a";
|
||||
late String _path;
|
||||
int _startTimestamp = 0;
|
||||
final int _tag;
|
||||
final RecordFc onFinished;
|
||||
final RecordFc onInterrupt;
|
||||
final int maxRecordSec;
|
||||
final Function(int duration)? onDuration;
|
||||
final _audioRecorder = AudioRecorder();
|
||||
Timer? _timer;
|
||||
|
||||
VoiceRecord({
|
||||
required this.maxRecordSec,
|
||||
required this.onInterrupt,
|
||||
required this.onFinished,
|
||||
this.onDuration,
|
||||
}) : _tag = _now();
|
||||
|
||||
start() async {
|
||||
if (await _audioRecorder.hasPermission()) {
|
||||
var path = (await getApplicationDocumentsDirectory()).path;
|
||||
_path = '$path/$_dir/$_tag$_ext';
|
||||
File file = File(_path);
|
||||
if (!(await file.exists())) {
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
await _audioRecorder.start(RecordConfig(), path: _path);
|
||||
_startTimestamp = _now();
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
|
||||
final duration = ((_now() - _startTimestamp) ~/ 1000);
|
||||
onDuration?.call(duration);
|
||||
if (duration >= maxRecordSec) {
|
||||
await stop(isInterrupt: true);
|
||||
onInterrupt(maxRecordSec, _path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stop({bool isInterrupt = false}) async {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
if (await _audioRecorder.isRecording()) {
|
||||
await _audioRecorder.stop();
|
||||
if (isInterrupt) return;
|
||||
onFinished((_now() - _startTimestamp) ~/ 1000, _path);
|
||||
}
|
||||
}
|
||||
|
||||
static int _now() => DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
typedef CustomAvatarBuilder = Widget? Function();
|
||||
|
||||
class AvatarView extends StatelessWidget {
|
||||
const AvatarView({
|
||||
Key? key,
|
||||
this.width,
|
||||
this.height,
|
||||
this.onTap,
|
||||
this.url,
|
||||
this.file,
|
||||
this.builder,
|
||||
this.text,
|
||||
this.textStyle,
|
||||
this.onLongPress,
|
||||
this.isCircle = false,
|
||||
this.borderRadius,
|
||||
this.enabledPreview = false,
|
||||
this.lowMemory = false,
|
||||
this.nineGridUrl = const [],
|
||||
this.isGroup = false,
|
||||
this.showDefaultAvatar = true,
|
||||
}) : super(key: key);
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Function()? onTap;
|
||||
final Function()? onLongPress;
|
||||
final String? url;
|
||||
final File? file;
|
||||
final CustomAvatarBuilder? builder;
|
||||
final bool isCircle;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool enabledPreview;
|
||||
final String? text;
|
||||
final TextStyle? textStyle;
|
||||
final bool lowMemory;
|
||||
final List<String> nineGridUrl;
|
||||
final bool isGroup;
|
||||
final bool showDefaultAvatar;
|
||||
|
||||
double get _avatarSize => min(width ?? 44.w, height ?? 44.h);
|
||||
|
||||
TextStyle get _textStyle => textStyle ?? Styles.ts_FFFFFF_16sp;
|
||||
|
||||
Color get _textAvatarBgColor => Styles.c_0089FF;
|
||||
|
||||
String? get _showName {
|
||||
if (isGroup) return null;
|
||||
if (text != null && text!.trim().isNotEmpty) {
|
||||
return text!.substring(0, 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get isUrlValid => IMUtils.isUrlValid(url);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var tag = const Uuid().v4();
|
||||
var child = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onTap ??
|
||||
((enabledPreview && isUrlValid)
|
||||
? () => IMUtils.previewUrlPicture([MediaSource(thumbnail: url!, url: url)])
|
||||
: null),
|
||||
onLongPress: onLongPress,
|
||||
child: builder?.call() ?? (nineGridUrl.isNotEmpty ? _nineGridAvatar() : _normalAvatar()),
|
||||
);
|
||||
return Hero(
|
||||
tag: tag,
|
||||
child: isCircle
|
||||
? ClipOval(child: child)
|
||||
: ClipRRect(
|
||||
borderRadius: borderRadius ?? BorderRadius.circular(6.r),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _normalAvatar() => !isUrlValid ? _textAvatar() : _networkImageAvatar();
|
||||
|
||||
Widget _textAvatar() => Container(
|
||||
width: _avatarSize,
|
||||
height: _avatarSize,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.rectangle,
|
||||
color: _textAvatarBgColor,
|
||||
),
|
||||
child: null == _showName
|
||||
? (showDefaultAvatar
|
||||
? FaIcon(
|
||||
isGroup ? FontAwesomeIcons.userGroup : FontAwesomeIcons.solidUser,
|
||||
color: Colors.white,
|
||||
size: _avatarSize / 2,
|
||||
)
|
||||
: null)
|
||||
: Text(_showName!, style: _textStyle),
|
||||
);
|
||||
|
||||
Widget _networkImageAvatar() => file != null
|
||||
? ImageUtil.fileImage(file: file!)
|
||||
: ImageUtil.networkImage(
|
||||
url: url!,
|
||||
width: _avatarSize,
|
||||
height: _avatarSize,
|
||||
fit: BoxFit.cover,
|
||||
lowMemory: lowMemory,
|
||||
loadProgress: false,
|
||||
errorWidget: _textAvatar(),
|
||||
);
|
||||
|
||||
Widget _nineGridAvatar() => Container(
|
||||
width: _avatarSize,
|
||||
height: _avatarSize,
|
||||
color: Colors.grey[300],
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
alignment: Alignment.center,
|
||||
child: _nineGridColumn(),
|
||||
);
|
||||
|
||||
Widget _nineGridColumn() {
|
||||
double width = 0.0;
|
||||
double margin = 2.0;
|
||||
int row1Length = 0;
|
||||
int row2Length = 0;
|
||||
int row3Length = 0;
|
||||
var list = <Widget>[];
|
||||
switch (nineGridUrl.length) {
|
||||
case 1:
|
||||
width = _avatarSize;
|
||||
row1Length = 1;
|
||||
break;
|
||||
case 2:
|
||||
width = _avatarSize / 2;
|
||||
row1Length = 2;
|
||||
break;
|
||||
case 3:
|
||||
width = _avatarSize / 2;
|
||||
row1Length = 1;
|
||||
row2Length = 2;
|
||||
break;
|
||||
case 4:
|
||||
width = _avatarSize / 2;
|
||||
row1Length = 2;
|
||||
row2Length = 2;
|
||||
break;
|
||||
case 5:
|
||||
width = _avatarSize / 3;
|
||||
row1Length = 2;
|
||||
row2Length = 3;
|
||||
break;
|
||||
case 6:
|
||||
width = _avatarSize / 3;
|
||||
row1Length = 3;
|
||||
row2Length = 3;
|
||||
break;
|
||||
case 7:
|
||||
width = _avatarSize / 3;
|
||||
row1Length = 1;
|
||||
row2Length = 3;
|
||||
row3Length = 3;
|
||||
break;
|
||||
case 8:
|
||||
width = _avatarSize / 3;
|
||||
row1Length = 2;
|
||||
row2Length = 3;
|
||||
row3Length = 3;
|
||||
break;
|
||||
case 9:
|
||||
width = _avatarSize / 3;
|
||||
row1Length = 3;
|
||||
row2Length = 3;
|
||||
row3Length = 3;
|
||||
break;
|
||||
}
|
||||
if (row1Length > 0) {
|
||||
list.add(_nineGridRow(
|
||||
length: row1Length,
|
||||
start: 0,
|
||||
size: width,
|
||||
margin: margin,
|
||||
));
|
||||
}
|
||||
if (row2Length > 0) {
|
||||
list.add(_nineGridRow(
|
||||
length: row2Length,
|
||||
start: row1Length,
|
||||
size: width,
|
||||
margin: margin,
|
||||
));
|
||||
}
|
||||
if (row3Length > 0) {
|
||||
list.add(_nineGridRow(
|
||||
length: row3Length,
|
||||
start: row1Length + row2Length,
|
||||
size: width,
|
||||
margin: margin,
|
||||
));
|
||||
}
|
||||
return Column(
|
||||
children: list,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _nineGridRow({
|
||||
required int length,
|
||||
required int start,
|
||||
required double size,
|
||||
required double margin,
|
||||
}) {
|
||||
Widget nineGridImage(String? url, double size) => _normalAvatar();
|
||||
Widget nineGridLine({
|
||||
double? width,
|
||||
double? height,
|
||||
}) =>
|
||||
Container(height: height, width: width, color: Colors.white);
|
||||
var list = <Widget>[];
|
||||
for (var i = 0; i < length; i++) {
|
||||
start += i;
|
||||
list.add(nineGridImage(nineGridUrl.elementAt(start), size));
|
||||
if (i != length - 1) {
|
||||
list.add(nineGridLine(width: margin, height: size));
|
||||
}
|
||||
}
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: list,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RedDotView extends StatelessWidget {
|
||||
const RedDotView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FF381F,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x26C61B4A),
|
||||
offset: Offset(1.15.w, 1.15.h),
|
||||
blurRadius: 57.58.r,
|
||||
),
|
||||
BoxShadow(
|
||||
color: const Color(0x1AC61B4A),
|
||||
offset: Offset(2.3.w, 2.3.h),
|
||||
blurRadius: 11.52.r,
|
||||
),
|
||||
BoxShadow(
|
||||
color: const Color(0x0DC61B4A),
|
||||
offset: Offset(4.61.w, 4.61.h),
|
||||
blurRadius: 17.28.r,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:azlistview/azlistview.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class WrapAzListView<T extends ISuspensionBean> extends StatelessWidget {
|
||||
const WrapAzListView({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<T> data;
|
||||
final int itemCount;
|
||||
final Widget Function(BuildContext context, T data, int index) itemBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AzListView(
|
||||
data: data,
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var model = data[index];
|
||||
return itemBuilder(context, model, index);
|
||||
},
|
||||
susItemBuilder: (BuildContext context, int index) {
|
||||
var model = data[index];
|
||||
if ('↑' == model.getSuspensionTag()) {
|
||||
return Container();
|
||||
}
|
||||
return _buildTagView(model.getSuspensionTag());
|
||||
},
|
||||
susItemHeight: 23.h,
|
||||
indexBarData: SuspensionUtil.getTagIndexList(data),
|
||||
indexBarOptions: IndexBarOptions(
|
||||
needRebuild: true,
|
||||
selectTextStyle: Styles.ts_FFFFFF_12sp,
|
||||
indexHintWidth: 96,
|
||||
indexHintHeight: 97,
|
||||
indexHintDecoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(ImageRes.indexBarBg, package: 'openim_common'),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
indexHintAlignment: Alignment.centerRight,
|
||||
indexHintTextStyle: Styles.ts_0C1C33_20sp_semibold,
|
||||
indexHintOffset: const Offset(-30, 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTagView(String tag) => Container(
|
||||
height: 23.h,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
alignment: Alignment.centerLeft,
|
||||
width: 1.sw,
|
||||
color: Styles.c_E8EAEF,
|
||||
child: tag.toText..style = Styles.ts_8E9AB0_14sp,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class BottomBar extends StatelessWidget {
|
||||
const BottomBar({
|
||||
Key? key,
|
||||
this.index = 0,
|
||||
required this.items,
|
||||
}) : super(key: key);
|
||||
final int index;
|
||||
final List<BottomBarItem> items;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 56.h,
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
border: BorderDirectional(
|
||||
top: BorderSide(
|
||||
color: Styles.c_E8EAEF,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
items.length,
|
||||
(index) => _buildItemView(
|
||||
i: index,
|
||||
item: items.elementAt(index),
|
||||
)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemView({required int i, required BottomBarItem item}) => Expanded(
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () => item.onDoubleClick?.call(i),
|
||||
onTapDown: (_) => item.onClick?.call(i),
|
||||
child: Container(
|
||||
color: Styles.c_FFFFFF,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
(i == index ? item.selectedImgRes.toImage : item.unselectedImgRes.toImage)
|
||||
..width = item.imgWidth
|
||||
..height = item.imgHeight,
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Transform.translate(
|
||||
offset: const Offset(2, -2),
|
||||
child: UnreadCountView(count: item.count ?? 0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
4.verticalSpace,
|
||||
item.label.toText
|
||||
..style = i == index
|
||||
? (item.selectedStyle ?? Styles.ts_0089FF_10sp_semibold)
|
||||
: (item.unselectedStyle ?? Styles.ts_8E9AB0_10sp_semibold),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
/*child: InkWell(
|
||||
onTap: () {
|
||||
if (item.onClick != null) item.onClick!(i);
|
||||
},
|
||||
onDoubleTap: () => item.onDoubleClick?.call(i),
|
||||
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
(i == index
|
||||
? item.selectedImgRes.toImage
|
||||
: item.unselectedImgRes.toImage)
|
||||
..width = item.imgWidth
|
||||
..height = item.imgHeight,
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Transform.translate(
|
||||
offset: const Offset(2, -2),
|
||||
child: UnreadCountView(count: item.count ?? 0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
4.verticalSpace,
|
||||
item.label.toText
|
||||
..style = i == index
|
||||
? (item.selectedStyle ?? Styles.ts_0089FF_10sp_semibold)
|
||||
: (item.unselectedStyle ?? Styles.ts_8E9AB0_10sp_semibold),
|
||||
],
|
||||
),
|
||||
),*/
|
||||
);
|
||||
}
|
||||
|
||||
class BottomBarItem {
|
||||
final String selectedImgRes;
|
||||
final String unselectedImgRes;
|
||||
final String label;
|
||||
final TextStyle? selectedStyle;
|
||||
final TextStyle? unselectedStyle;
|
||||
final double imgWidth;
|
||||
final double imgHeight;
|
||||
final Function(int index)? onClick;
|
||||
final Function(int index)? onDoubleClick;
|
||||
final Stream<int>? steam;
|
||||
final int? count;
|
||||
|
||||
BottomBarItem(
|
||||
{required this.selectedImgRes,
|
||||
required this.unselectedImgRes,
|
||||
required this.label,
|
||||
this.selectedStyle,
|
||||
this.unselectedStyle,
|
||||
required this.imgWidth,
|
||||
required this.imgHeight,
|
||||
this.onClick,
|
||||
this.onDoubleClick,
|
||||
this.steam,
|
||||
this.count});
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class BottomSheetView extends StatelessWidget {
|
||||
const BottomSheetView({
|
||||
Key? key,
|
||||
required this.items,
|
||||
this.itemHeight,
|
||||
this.textStyle,
|
||||
this.mainAxisAlignment,
|
||||
this.isOverlaySheet = false,
|
||||
this.onCancel,
|
||||
}) : super(key: key);
|
||||
final List<SheetItem> items;
|
||||
final double? itemHeight;
|
||||
final TextStyle? textStyle;
|
||||
final MainAxisAlignment? mainAxisAlignment;
|
||||
final bool isOverlaySheet;
|
||||
final Function()? onCancel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: items.map(_parseItem).toList(),
|
||||
),
|
||||
),
|
||||
10.verticalSpace,
|
||||
_itemBgView(
|
||||
label: StrRes.cancel,
|
||||
onTap: isOverlaySheet ? onCancel : () => Get.back(),
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
alignment: MainAxisAlignment.center,
|
||||
),
|
||||
10.verticalSpace,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _parseItem(SheetItem item) {
|
||||
BorderRadius? borderRadius;
|
||||
int length = items.length;
|
||||
bool isLast = items.indexOf(item) == items.length - 1;
|
||||
bool isFirst = items.indexOf(item) == 0;
|
||||
if (length == 1) {
|
||||
borderRadius = item.borderRadius ?? BorderRadius.circular(6.r);
|
||||
} else {
|
||||
borderRadius = item.borderRadius ??
|
||||
BorderRadius.only(
|
||||
topLeft: isFirst ? Radius.circular(6.r) : Radius.zero,
|
||||
topRight: isFirst ? Radius.circular(6.r) : Radius.zero,
|
||||
bottomLeft: isLast ? Radius.circular(6.r) : Radius.zero,
|
||||
bottomRight: isLast ? Radius.circular(6.r) : Radius.zero,
|
||||
);
|
||||
}
|
||||
return _itemBgView(
|
||||
label: item.label,
|
||||
textStyle: item.textStyle,
|
||||
icon: item.icon,
|
||||
alignment: item.alignment,
|
||||
line: !isLast,
|
||||
borderRadius: borderRadius,
|
||||
onTap: () {
|
||||
if (!isOverlaySheet) Get.back(result: item.result);
|
||||
item.onTap?.call();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _itemBgView({
|
||||
required String label,
|
||||
String? icon,
|
||||
Function()? onTap,
|
||||
BorderRadius? borderRadius,
|
||||
TextStyle? textStyle,
|
||||
MainAxisAlignment? alignment,
|
||||
bool line = false,
|
||||
}) =>
|
||||
Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: line
|
||||
? BoxDecoration(
|
||||
border: BorderDirectional(
|
||||
bottom: BorderSide(color: Styles.c_E8EAEF, width: 0.5),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
height: itemHeight ?? 56.h,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
alignment ?? mainAxisAlignment ?? MainAxisAlignment.center,
|
||||
children: [
|
||||
if (null != icon) 10.horizontalSpace,
|
||||
if (null != icon) _image(icon),
|
||||
if (null != icon) 5.horizontalSpace,
|
||||
_text(label, textStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
_text(String label, TextStyle? style) =>
|
||||
label.toText..style = (style ?? textStyle ?? Styles.ts_0C1C33_17sp);
|
||||
|
||||
_image(String icon) => icon.toImage
|
||||
..width = 24.w
|
||||
..height = 24.h;
|
||||
}
|
||||
|
||||
class SheetItem {
|
||||
final String label;
|
||||
final TextStyle? textStyle;
|
||||
final String? icon;
|
||||
final Function()? onTap;
|
||||
final BorderRadius? borderRadius;
|
||||
final MainAxisAlignment? alignment;
|
||||
final dynamic result;
|
||||
|
||||
SheetItem({
|
||||
required this.label,
|
||||
this.textStyle,
|
||||
this.icon,
|
||||
this.onTap,
|
||||
this.borderRadius,
|
||||
this.alignment,
|
||||
this.result,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class Button extends StatelessWidget {
|
||||
const Button({
|
||||
Key? key,
|
||||
required this.text,
|
||||
this.enabled = true,
|
||||
this.enabledColor,
|
||||
this.disabledColor,
|
||||
this.radius,
|
||||
this.textStyle,
|
||||
this.disabledTextStyle,
|
||||
this.onTap,
|
||||
this.height,
|
||||
this.margin,
|
||||
this.padding,
|
||||
}) : super(key: key);
|
||||
final Color? enabledColor;
|
||||
final Color? disabledColor;
|
||||
final double? radius;
|
||||
final TextStyle? textStyle;
|
||||
final TextStyle? disabledTextStyle;
|
||||
final String text;
|
||||
final double? height;
|
||||
final Function()? onTap;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: margin,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Ink(
|
||||
height: height ?? 44.h,
|
||||
decoration: BoxDecoration(
|
||||
color: enabled ? enabledColor ?? Styles.c_0089FF : disabledColor ?? Styles.c_0089FF_opacity50,
|
||||
borderRadius: BorderRadius.circular(radius ?? 4.r),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(radius ?? 4.r),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: padding,
|
||||
child: Text(
|
||||
text,
|
||||
style: textStyle ?? Styles.ts_FFFFFF_17sp_semibold,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImageTextButton extends StatelessWidget {
|
||||
const ImageTextButton({
|
||||
Key? key,
|
||||
required this.icon,
|
||||
required this.text,
|
||||
this.textStyle,
|
||||
this.color,
|
||||
this.height,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
final String icon;
|
||||
final String text;
|
||||
final TextStyle? textStyle;
|
||||
final Color? color;
|
||||
final double? height;
|
||||
final Function()? onTap;
|
||||
|
||||
ImageTextButton.call({super.key, this.onTap})
|
||||
: icon = ImageRes.audioAndVideoCall,
|
||||
text = StrRes.audioAndVideoCall,
|
||||
color = Styles.c_FFFFFF,
|
||||
textStyle = null,
|
||||
height = null;
|
||||
|
||||
ImageTextButton.message({super.key, this.onTap})
|
||||
: icon = ImageRes.message,
|
||||
text = StrRes.sendMessage,
|
||||
color = Styles.c_0089FF,
|
||||
textStyle = Styles.ts_FFFFFF_17sp,
|
||||
height = null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Ink(
|
||||
height: height ?? 46.h,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
color: color,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon.toImage
|
||||
..width = 20.w
|
||||
..height = 20.h,
|
||||
6.horizontalSpace,
|
||||
text.toText..style = textStyle ?? Styles.ts_0C1C33_17sp,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
enum BubbleType {
|
||||
send,
|
||||
receiver,
|
||||
}
|
||||
|
||||
class ChatBubble extends StatelessWidget {
|
||||
const ChatBubble({
|
||||
Key? key,
|
||||
this.margin,
|
||||
this.constraints,
|
||||
this.alignment = Alignment.center,
|
||||
this.backgroundColor,
|
||||
this.child,
|
||||
required this.bubbleType,
|
||||
}) : super(key: key);
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final BoxConstraints? constraints;
|
||||
final AlignmentGeometry? alignment;
|
||||
final Color? backgroundColor;
|
||||
final Widget? child;
|
||||
final BubbleType bubbleType;
|
||||
|
||||
bool get isISend => bubbleType == BubbleType.send;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
constraints: constraints,
|
||||
margin: margin,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h),
|
||||
alignment: alignment,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
backgroundColor ?? (isISend ? Styles.c_CCE7FE : Styles.c_F4F5F7),
|
||||
borderRadius: borderRadius(isISend),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatCallItemView extends StatelessWidget {
|
||||
const ChatCallItemView({
|
||||
Key? key,
|
||||
required this.type,
|
||||
required this.content,
|
||||
}) : super(key: key);
|
||||
|
||||
final String content;
|
||||
final String type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
children: [
|
||||
(type == 'audio' ? ImageRes.voiceCallMsg : ImageRes.videoCallMsg).toImage
|
||||
..width = 18.w
|
||||
..height = 18.h
|
||||
..color = (/*isISend ? Styles.c_FFFFFF : */ Styles.c_0C1C33),
|
||||
8.horizontalSpace,
|
||||
Text(
|
||||
content,
|
||||
style: /*isISend ? Styles.ts_FFFFFF_17sp : */ Styles.ts_0C1C33_17sp,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatCarteView extends StatelessWidget {
|
||||
const ChatCarteView({
|
||||
Key? key,
|
||||
required this.cardElem,
|
||||
}) : super(key: key);
|
||||
final CardElem cardElem;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: locationWidth,
|
||||
height: 91.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: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AvatarView(
|
||||
width: 44.w,
|
||||
height: 44.h,
|
||||
url: cardElem.faceURL,
|
||||
text: cardElem.nickname,
|
||||
textStyle: Styles.ts_FFFFFF_14sp_medium,
|
||||
),
|
||||
10.horizontalSpace,
|
||||
Flexible(
|
||||
child: cardElem.nickname!.toText
|
||||
..style = Styles.ts_0C1C33_17sp
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(color: Styles.c_E8EAEF, height: 1),
|
||||
Container(
|
||||
height: 26.h,
|
||||
padding: EdgeInsets.only(top: 4.h, bottom: 4.h, left: 17.w),
|
||||
child: StrRes.carte.toText..style = Styles.ts_8E9AB0_12sp,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatCustomEmojiView extends StatelessWidget {
|
||||
const ChatCustomEmojiView({
|
||||
Key? key,
|
||||
this.index,
|
||||
this.data,
|
||||
this.heroTag,
|
||||
required this.isISend,
|
||||
}) : super(key: key);
|
||||
|
||||
final int? index;
|
||||
|
||||
final String? data;
|
||||
final bool isISend;
|
||||
final String? heroTag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
if (data != null) {
|
||||
var map = json.decode(data!);
|
||||
var url = map['url'];
|
||||
var w = map['width'] ?? 1.0;
|
||||
var h = map['height'] ?? 1.0;
|
||||
if (w is int) {
|
||||
w = w.toDouble();
|
||||
}
|
||||
if (h is int) {
|
||||
h = h.toDouble();
|
||||
}
|
||||
double trulyWidth;
|
||||
double trulyHeight;
|
||||
if (pictureWidth < w) {
|
||||
trulyWidth = pictureWidth;
|
||||
trulyHeight = trulyWidth * h / w;
|
||||
} else {
|
||||
trulyWidth = w;
|
||||
trulyHeight = h;
|
||||
}
|
||||
final child = ClipRRect(
|
||||
borderRadius: borderRadius(isISend),
|
||||
child: ImageUtil.networkImage(
|
||||
url: url,
|
||||
width: trulyWidth,
|
||||
height: trulyHeight,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
);
|
||||
return null != heroTag ? Hero(tag: heroTag!, child: child) : child;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logger.print('e:$e s:$s');
|
||||
}
|
||||
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatDelayedStatusView extends StatefulWidget {
|
||||
const ChatDelayedStatusView({
|
||||
super.key,
|
||||
required this.isSending,
|
||||
this.delay = true,
|
||||
});
|
||||
final bool isSending;
|
||||
final bool delay;
|
||||
|
||||
@override
|
||||
State<ChatDelayedStatusView> createState() => _ChatDelayedStatusViewState();
|
||||
}
|
||||
|
||||
class _ChatDelayedStatusViewState extends State<ChatDelayedStatusView> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: Future.delayed(
|
||||
Duration(seconds: widget.isSending && widget.delay ? 1 : 0),
|
||||
() => widget.isSending,
|
||||
),
|
||||
builder: (_, AsyncSnapshot<bool> hot) => Visibility(
|
||||
visible: hot.hasData && hot.data == true,
|
||||
child: CupertinoActivityIndicator(
|
||||
color: Styles.c_0089FF,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatDisableInputBox extends StatelessWidget {
|
||||
const ChatDisableInputBox({Key? key, this.type = 0}) : super(key: key);
|
||||
|
||||
final int type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return type == 0
|
||||
? Container(
|
||||
height: 56.h,
|
||||
color: Styles.c_F0F2F6,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ImageRes.warn.toImage
|
||||
..width = 14.w
|
||||
..height = 14.h,
|
||||
6.horizontalSpace,
|
||||
StrRes.notSendMessageNotInGroup.toText..style = Styles.ts_8E9AB0_14sp,
|
||||
],
|
||||
),
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart' hide Config;
|
||||
import 'package:should_rebuild/should_rebuild.dart';
|
||||
|
||||
class ChatEmojiView extends StatefulWidget {
|
||||
const ChatEmojiView({
|
||||
Key? key,
|
||||
this.favoriteList = const [],
|
||||
this.onAddFavorite,
|
||||
this.onSelectedFavorite,
|
||||
required this.textEditingController,
|
||||
this.height,
|
||||
this.customEmojiLayout,
|
||||
}) : super(key: key);
|
||||
final List<String> favoriteList;
|
||||
final Function()? onAddFavorite;
|
||||
final Function(int index, String url)? onSelectedFavorite;
|
||||
final TextEditingController textEditingController;
|
||||
final double? height;
|
||||
final Widget? customEmojiLayout;
|
||||
|
||||
@override
|
||||
State<ChatEmojiView> createState() => _ChatEmojiViewState();
|
||||
}
|
||||
|
||||
class _ChatEmojiViewState extends State<ChatEmojiView> {
|
||||
var _index = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Styles.c_FFFFFF,
|
||||
child: Column(
|
||||
children: [
|
||||
IndexedStack(
|
||||
index: _index,
|
||||
children: [
|
||||
widget.customEmojiLayout ??
|
||||
ShouldRebuild<EmojiLayout>(
|
||||
shouldRebuild: (oldWidget, newWidget) => false,
|
||||
child: EmojiLayout(
|
||||
controller: widget.textEditingController,
|
||||
),
|
||||
),
|
||||
_buildFavoriteLayout(),
|
||||
],
|
||||
),
|
||||
_buildTabView(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabView() => Container(
|
||||
height: 56.h,
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
border: BorderDirectional(
|
||||
top: BorderSide(
|
||||
color: Styles.c_E8EAEF,
|
||||
width: 1.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildTabSelectedBgView(selected: _index == 0, index: 0),
|
||||
_buildTabSelectedBgView(selected: _index == 1, index: 1),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildTabSelectedBgView({
|
||||
bool selected = false,
|
||||
int index = 0,
|
||||
}) =>
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_index = index;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
width: 62.w,
|
||||
height: 56.h,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? Styles.c_E8EAEF : null,
|
||||
),
|
||||
child: Center(
|
||||
child: (index == 0 ? ImageRes.emojiTab : ImageRes.favoriteTab).toImage
|
||||
..width = 28.w
|
||||
..height = 28.h
|
||||
..color = (selected ? Styles.c_0089FF : Styles.c_8E9AB0),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildFavoriteLayout() => Container(
|
||||
color: Styles.c_FFFFFF,
|
||||
height: widget.height ?? 188.h,
|
||||
child: GridView.builder(
|
||||
padding: EdgeInsets.fromLTRB(22.w, 12.h, 22.w, 22.h),
|
||||
itemCount: widget.favoriteList.length + 1,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
childAspectRatio: 1,
|
||||
mainAxisSpacing: 22.h,
|
||||
crossAxisSpacing: 22.w,
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return GestureDetector(
|
||||
onTap: widget.onAddFavorite,
|
||||
child: ImageRes.addFavorite.toImage
|
||||
..width = 66.w
|
||||
..height = 66.h,
|
||||
);
|
||||
}
|
||||
var url = widget.favoriteList.elementAt(index - 1);
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onSelectedFavorite?.call(index - 1, url),
|
||||
child: ImageUtil.networkImage(
|
||||
url: url,
|
||||
width: 66.w,
|
||||
height: 66.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class EmojiLayout extends StatelessWidget {
|
||||
const EmojiLayout({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
this.height,
|
||||
}) : super(key: key);
|
||||
final TextEditingController controller;
|
||||
final double? height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: height ?? 188.h,
|
||||
color: Styles.c_FFFFFF,
|
||||
child: EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) {
|
||||
controller
|
||||
..text += emoji.emoji
|
||||
..selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length));
|
||||
},
|
||||
onBackspacePressed: () {
|
||||
controller
|
||||
..text = controller.text.characters.skipLast(1).toString()
|
||||
..selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length));
|
||||
},
|
||||
config: Config(
|
||||
checkPlatformCompatibility: true,
|
||||
emojiViewConfig: EmojiViewConfig(
|
||||
columns: 8,
|
||||
recentsLimit: 9,
|
||||
emojiSizeMax: 28 * (Platform.isIOS ? 1.20 : 1.0),
|
||||
),
|
||||
skinToneConfig: SkinToneConfig(enabled: false),
|
||||
categoryViewConfig: CategoryViewConfig(),
|
||||
bottomActionBarConfig: BottomActionBarConfig(enabled: false),
|
||||
searchViewConfig: SearchViewConfig(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatFacePreview extends StatelessWidget {
|
||||
const ChatFacePreview({
|
||||
Key? key,
|
||||
required this.url,
|
||||
}) : super(key: key);
|
||||
final String url;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: TitleBar.back(title: StrRes.emoji),
|
||||
backgroundColor: Styles.c_FFFFFF,
|
||||
body: Center(
|
||||
child: _networkGestureImage(url),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _networkGestureImage(String url) => ExtendedImage.network(
|
||||
url,
|
||||
fit: BoxFit.contain,
|
||||
mode: ExtendedImageMode.gesture,
|
||||
clearMemoryCacheWhenDispose: true,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
handleLoadingProgress: true,
|
||||
enableSlideOutPage: true,
|
||||
initGestureConfigHandler: (ExtendedImageState state) {
|
||||
return GestureConfig(
|
||||
inPageView: true,
|
||||
initialScale: 1.0,
|
||||
maxScale: 5.0,
|
||||
animationMaxScale: 6.0,
|
||||
initialAlignment: InitialAlignment.center,
|
||||
);
|
||||
},
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
{
|
||||
final ImageChunkEvent? loadingProgress = state.loadingProgress;
|
||||
final double? progress = loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||
: null;
|
||||
|
||||
return SizedBox(
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Styles.c_0089FF,
|
||||
strokeWidth: 1.5,
|
||||
value: progress ?? 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
case LoadState.completed:
|
||||
return null;
|
||||
case LoadState.failed:
|
||||
state.imageProvider.evict();
|
||||
return ImageRes.pictureError.toImage;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatFileIconView extends StatelessWidget {
|
||||
const ChatFileIconView({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.downloadProgressView,
|
||||
}) : super(key: key);
|
||||
final Message message;
|
||||
final Widget? downloadProgressView;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fileName = message.fileElem!.fileName!;
|
||||
return Stack(
|
||||
children: [
|
||||
IMUtils.fileIcon(fileName).toImage
|
||||
..width = 38.w
|
||||
..height = 44.h,
|
||||
if (null != downloadProgressView) downloadProgressView!,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatFileView extends StatelessWidget {
|
||||
const ChatFileView({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.isISend,
|
||||
this.sendProgressStream,
|
||||
this.fileDownloadProgressView,
|
||||
}) : super(key: key);
|
||||
final Message message;
|
||||
final Stream<MsgStreamEv<int>>? sendProgressStream;
|
||||
final bool isISend;
|
||||
final Widget? fileDownloadProgressView;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
width: maxWidth,
|
||||
height: 64.h,
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
border: Border.all(color: Styles.c_E8EAEF, width: 1),
|
||||
borderRadius: borderRadius(isISend),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextWithMidEllipsis(
|
||||
message.fileElem?.fileName ?? '',
|
||||
style: Styles.ts_0C1C33_17sp,
|
||||
endPartLength: 8,
|
||||
),
|
||||
IMUtils.formatBytes(message.fileElem?.fileSize ?? 0).toText..style = Styles.ts_8E9AB0_14sp,
|
||||
],
|
||||
),
|
||||
),
|
||||
10.horizontalSpace,
|
||||
ChatFileIconView(
|
||||
message: message,
|
||||
downloadProgressView: fileDownloadProgressView,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:sprintf/sprintf.dart';
|
||||
|
||||
class ChatFriendRelationshipAbnormalHintView extends StatelessWidget {
|
||||
const ChatFriendRelationshipAbnormalHintView({
|
||||
Key? key,
|
||||
this.blockedByFriend = false,
|
||||
this.deletedByFriend = false,
|
||||
required this.name,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
final bool blockedByFriend;
|
||||
final bool deletedByFriend;
|
||||
final String name;
|
||||
final Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (blockedByFriend) {
|
||||
return StrRes.blockedByFriendHint.toText..style = Styles.ts_8E9AB0_12sp;
|
||||
} else if (deletedByFriend) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: sprintf(StrRes.deletedByFriendHint, [name]),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: StrRes.sendFriendVerification,
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()..onTap = onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:sprintf/sprintf.dart';
|
||||
|
||||
class ChatHintTextView extends StatelessWidget {
|
||||
const ChatHintTextView({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.onTapUserProfile,
|
||||
});
|
||||
final Message message;
|
||||
final ValueChanged<({String userID, String name, String? faceURL, String? groupID})> onTapUserProfile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
final groupID = message.groupID;
|
||||
final elem = message.notificationElem!;
|
||||
final map = json.decode(elem.detail!);
|
||||
switch (message.contentType) {
|
||||
case MessageType.groupCreatedNotification:
|
||||
{
|
||||
final ntf = GroupNotification.fromJson(map);
|
||||
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(ntf.opUser!),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: ntf.opUser!.userID!,
|
||||
name: ntf.opUser!.nickname!,
|
||||
faceURL: ntf.opUser!.faceURL,
|
||||
groupID: groupID
|
||||
)),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: sprintf(StrRes.createGroupNtf, ['']),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
case MessageType.groupInfoSetNotification:
|
||||
{
|
||||
final ntf = GroupNotification.fromJson(map);
|
||||
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(ntf.opUser!),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: ntf.opUser!.userID!,
|
||||
name: ntf.opUser!.nickname!,
|
||||
faceURL: ntf.opUser!.faceURL,
|
||||
groupID: groupID
|
||||
)),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: sprintf(StrRes.editGroupInfoNtf, ['']),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
case MessageType.memberQuitNotification:
|
||||
{
|
||||
final ntf = QuitGroupNotification.fromJson(map);
|
||||
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(ntf.quitUser!),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: ntf.quitUser!.userID!,
|
||||
name: ntf.quitUser!.nickname!,
|
||||
faceURL: ntf.quitUser!.faceURL,
|
||||
groupID: groupID
|
||||
)),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: sprintf(StrRes.quitGroupNtf, ['']),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
case MessageType.memberInvitedNotification:
|
||||
{
|
||||
final aMap = <String, String>{};
|
||||
final bMap = <String, String>{};
|
||||
final infoMap = <String, GroupMembersInfo>{};
|
||||
final ntf = InvitedJoinGroupNotification.fromJson(map);
|
||||
|
||||
aMap[ntf.opUser!.userID!] = IMUtils.getGroupMemberShowName(ntf.opUser!);
|
||||
infoMap[ntf.opUser!.userID!] = ntf.opUser!;
|
||||
|
||||
for (var user in ntf.invitedUserList!) {
|
||||
bMap[user.userID!] = IMUtils.getGroupMemberShowName(user);
|
||||
infoMap[user.userID!] = user;
|
||||
}
|
||||
|
||||
final a = ntf.opUser!.userID!;
|
||||
final b = bMap.keys.join('、');
|
||||
String pattern = '(${[a, ...bMap.keys].join('|')})';
|
||||
|
||||
final text = sprintf(StrRes.invitedJoinGroupNtf, [a, b]);
|
||||
final List<InlineSpan> children = <InlineSpan>[];
|
||||
text.splitMapJoin(
|
||||
RegExp(pattern),
|
||||
onMatch: (match) {
|
||||
final text = match[0]!;
|
||||
final value = aMap[text] ?? bMap[text] ?? '';
|
||||
final info = infoMap[text];
|
||||
children.add(TextSpan(
|
||||
text: value,
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: info?.userID ?? '',
|
||||
name: info?.nickname ?? '',
|
||||
faceURL: info?.faceURL,
|
||||
groupID: groupID
|
||||
)),
|
||||
));
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (text) {
|
||||
children.add(TextSpan(text: text, style: Styles.ts_8E9AB0_12sp));
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: children),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
case MessageType.memberKickedNotification:
|
||||
{
|
||||
final aMap = <String, String>{};
|
||||
final bMap = <String, String>{};
|
||||
final infoMap = <String, GroupMembersInfo>{};
|
||||
final ntf = KickedGroupMemeberNotification.fromJson(map);
|
||||
|
||||
aMap[ntf.opUser!.userID!] = IMUtils.getGroupMemberShowName(ntf.opUser!);
|
||||
infoMap[ntf.opUser!.userID!] = ntf.opUser!;
|
||||
|
||||
for (var user in ntf.kickedUserList!) {
|
||||
bMap[user.userID!] = IMUtils.getGroupMemberShowName(user);
|
||||
infoMap[user.userID!] = user;
|
||||
}
|
||||
|
||||
final a = ntf.opUser!.userID!;
|
||||
final b = bMap.keys.join('、');
|
||||
String pattern = '(${[a, ...bMap.keys].join('|')})';
|
||||
|
||||
final text = sprintf(StrRes.kickedGroupNtf, [b, a]);
|
||||
final List<InlineSpan> children = <InlineSpan>[];
|
||||
text.splitMapJoin(
|
||||
RegExp(pattern),
|
||||
onMatch: (match) {
|
||||
final text = match[0]!;
|
||||
final value = aMap[text] ?? bMap[text] ?? '';
|
||||
final info = infoMap[text];
|
||||
children.add(TextSpan(
|
||||
text: value,
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: info?.userID ?? '',
|
||||
name: info?.nickname ?? '',
|
||||
faceURL: info?.faceURL,
|
||||
groupID: groupID
|
||||
)),
|
||||
));
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (text) {
|
||||
children.add(TextSpan(text: text, style: Styles.ts_8E9AB0_12sp));
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: children),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
case MessageType.memberEnterNotification:
|
||||
{
|
||||
final ntf = EnterGroupNotification.fromJson(map);
|
||||
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(ntf.entrantUser!),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: ntf.entrantUser!.userID!,
|
||||
name: ntf.entrantUser!.nickname ?? '',
|
||||
faceURL: ntf.entrantUser!.faceURL,
|
||||
groupID: groupID,
|
||||
)),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: sprintf(StrRes.joinGroupNtf, ['']),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
case MessageType.dismissGroupNotification:
|
||||
{
|
||||
final ntf = GroupNotification.fromJson(map);
|
||||
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(ntf.opUser!),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: ntf.opUser!.userID!,
|
||||
name: ntf.opUser!.nickname ?? '',
|
||||
faceURL: ntf.opUser!.faceURL,
|
||||
groupID: groupID,
|
||||
)),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: sprintf(StrRes.dismissGroupNtf, ['']),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
case MessageType.groupOwnerTransferredNotification:
|
||||
{
|
||||
final ntf = GroupRightsTransferNoticication.fromJson(map);
|
||||
|
||||
final a = ntf.opUser!.userID;
|
||||
final b = ntf.newGroupOwner!.userID;
|
||||
final text = sprintf(StrRes.transferredGroupNtf, [a, b]);
|
||||
final List<InlineSpan> children = <InlineSpan>[];
|
||||
text.splitMapJoin(
|
||||
RegExp('($a|$b)'),
|
||||
onMatch: (match) {
|
||||
final text = match[0]!;
|
||||
final info = text == ntf.opUser!.userID ? ntf.opUser! : ntf.newGroupOwner!;
|
||||
children.add(TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(info),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: info.userID!,
|
||||
name: info.nickname ?? '',
|
||||
faceURL: info.faceURL,
|
||||
groupID: groupID,
|
||||
)),
|
||||
));
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (text) {
|
||||
children.add(TextSpan(text: text, style: Styles.ts_8E9AB0_12sp));
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: children),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
case MessageType.groupMemberMutedNotification:
|
||||
{
|
||||
final ntf = MuteMemberNotification.fromJson(map);
|
||||
final a = ntf.opUser!.userID;
|
||||
final b = ntf.mutedUser!.userID;
|
||||
final c = IMUtils.mutedTime(ntf.mutedSeconds!);
|
||||
final text = sprintf(StrRes.muteMemberNtf, [b, a, c]);
|
||||
final List<InlineSpan> children = <InlineSpan>[];
|
||||
text.splitMapJoin(
|
||||
RegExp('($a|$b)'),
|
||||
onMatch: (match) {
|
||||
final text = match[0]!;
|
||||
final info = text == ntf.opUser!.userID ? ntf.opUser! : ntf.mutedUser!;
|
||||
children.add(TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(info),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: info.userID!,
|
||||
name: info.nickname ?? '',
|
||||
faceURL: info.faceURL,
|
||||
groupID: groupID,
|
||||
)),
|
||||
));
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (text) {
|
||||
children.add(TextSpan(text: text, style: Styles.ts_8E9AB0_12sp));
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: children),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
case MessageType.groupMemberCancelMutedNotification:
|
||||
{
|
||||
final ntf = MuteMemberNotification.fromJson(map);
|
||||
final a = ntf.opUser!.userID;
|
||||
final b = ntf.mutedUser!.userID;
|
||||
final text = sprintf(StrRes.muteCancelMemberNtf, [b, a]);
|
||||
final List<InlineSpan> children = <InlineSpan>[];
|
||||
text.splitMapJoin(
|
||||
RegExp('($a|$b)'),
|
||||
onMatch: (match) {
|
||||
final text = match[0]!;
|
||||
final info = text == ntf.opUser!.userID ? ntf.opUser! : ntf.mutedUser!;
|
||||
children.add(TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(info),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: info.userID!,
|
||||
name: info.nickname ?? '',
|
||||
faceURL: info.faceURL,
|
||||
groupID: groupID,
|
||||
)),
|
||||
));
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (text) {
|
||||
children.add(TextSpan(text: text, style: Styles.ts_8E9AB0_12sp));
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: children),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
case MessageType.groupMutedNotification:
|
||||
{
|
||||
final ntf = MuteMemberNotification.fromJson(map);
|
||||
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(ntf.opUser!),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: ntf.opUser!.userID!,
|
||||
name: ntf.opUser!.nickname ?? '',
|
||||
faceURL: ntf.opUser!.faceURL,
|
||||
groupID: groupID,
|
||||
)),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: sprintf(StrRes.muteGroupNtf, ['']),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
case MessageType.groupCancelMutedNotification:
|
||||
{
|
||||
final ntf = MuteMemberNotification.fromJson(map);
|
||||
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(ntf.opUser!),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: ntf.opUser!.userID!,
|
||||
name: ntf.opUser!.nickname ?? '',
|
||||
faceURL: ntf.opUser!.faceURL,
|
||||
groupID: groupID,
|
||||
)),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: sprintf(StrRes.muteCancelGroupNtf, ['']),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
case MessageType.friendApplicationApprovedNotification:
|
||||
{
|
||||
return StrRes.friendAddedNtf.toText..style = Styles.ts_8E9AB0_12sp;
|
||||
}
|
||||
case MessageType.burnAfterReadingNotification:
|
||||
{
|
||||
final ntf = BurnAfterReadingNotification.fromJson(map);
|
||||
|
||||
return (ntf.isPrivate == true ? StrRes.openPrivateChatNtf : StrRes.closePrivateChatNtf).toText
|
||||
..style = Styles.ts_8E9AB0_12sp;
|
||||
}
|
||||
case MessageType.groupMemberInfoChangedNotification:
|
||||
final ntf = GroupMemberInfoChangedNotification.fromJson(map);
|
||||
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(ntf.opUser!),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: ntf.opUser!.userID!,
|
||||
name: ntf.opUser!.nickname ?? '',
|
||||
faceURL: ntf.opUser!.faceURL,
|
||||
groupID: groupID,
|
||||
)),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: sprintf(StrRes.memberInfoChangedNtf, ['']),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
case MessageType.groupInfoSetNameNotification:
|
||||
final ntf = GroupNotification.fromJson(map);
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: IMUtils.getGroupMemberShowName(ntf.opUser!),
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onTapUserProfile((
|
||||
userID: ntf.opUser!.userID!,
|
||||
name: ntf.opUser!.nickname ?? '',
|
||||
faceURL: ntf.opUser!.faceURL,
|
||||
groupID: groupID,
|
||||
)),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: sprintf(StrRes.whoModifyGroupName, ['', ntf.group?.groupName ?? ""]),
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
} catch (e) {
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThumbnailViewer extends StatefulWidget {
|
||||
final String? thumbnailUrl;
|
||||
final String? imageUrl;
|
||||
final File? thumbnailFile;
|
||||
final File? imageFile;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
ThumbnailViewer({this.thumbnailUrl, this.imageUrl, this.thumbnailFile, this.imageFile, this.onTap, this.onLongPress});
|
||||
|
||||
@override
|
||||
_ThumbnailViewerState createState() => _ThumbnailViewerState();
|
||||
}
|
||||
|
||||
class _ThumbnailViewerState extends State<ThumbnailViewer> {
|
||||
bool showThumbnail = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
onLongPress: widget.onLongPress,
|
||||
child: Center(
|
||||
child: showThumbnail
|
||||
? (widget.thumbnailFile != null
|
||||
? ExtendedImage.file(
|
||||
widget.thumbnailFile!,
|
||||
)
|
||||
: ExtendedImage.network(
|
||||
widget.thumbnailUrl!,
|
||||
fit: BoxFit.cover,
|
||||
loadStateChanged: (state) {
|
||||
if (state.extendedImageLoadState == LoadState.completed) {
|
||||
setState(() {
|
||||
showThumbnail = false;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
))
|
||||
: (widget.imageFile != null
|
||||
? ExtendedImage.file(widget.imageFile!)
|
||||
: ExtendedImage.network(
|
||||
widget.imageUrl!,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
import 'package:animate_do/animate_do.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
double kInputBoxMinHeight = 56.h;
|
||||
|
||||
class ChatInputBox extends StatefulWidget {
|
||||
const ChatInputBox({
|
||||
Key? key,
|
||||
required this.toolbox,
|
||||
required this.voiceRecordBar,
|
||||
required this.emojiView,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.style,
|
||||
this.atStyle,
|
||||
this.enabled = true,
|
||||
this.isNotInGroup = false,
|
||||
this.hintText,
|
||||
this.forceCloseToolboxSub,
|
||||
this.quoteContent,
|
||||
this.onClearQuote,
|
||||
this.onSend,
|
||||
this.directionalText,
|
||||
this.onCloseDirectional,
|
||||
}) : super(key: key);
|
||||
final FocusNode? focusNode;
|
||||
final TextEditingController? controller;
|
||||
final TextStyle? style;
|
||||
final TextStyle? atStyle;
|
||||
final bool enabled;
|
||||
final bool isNotInGroup;
|
||||
final String? hintText;
|
||||
final Widget toolbox;
|
||||
final Widget voiceRecordBar;
|
||||
final Widget emojiView;
|
||||
final Stream? forceCloseToolboxSub;
|
||||
final String? quoteContent;
|
||||
final Function()? onClearQuote;
|
||||
final ValueChanged<String>? onSend;
|
||||
final TextSpan? directionalText;
|
||||
final VoidCallback? onCloseDirectional;
|
||||
|
||||
@override
|
||||
State<ChatInputBox> createState() => _ChatInputBoxState();
|
||||
}
|
||||
|
||||
class _ChatInputBoxState extends State<ChatInputBox> /*with TickerProviderStateMixin */ {
|
||||
bool _toolsVisible = false;
|
||||
bool _emojiVisible = false;
|
||||
bool _leftKeyboardButton = false;
|
||||
bool _rightKeyboardButton = false;
|
||||
bool _sendButtonVisible = false;
|
||||
|
||||
bool get _showQuoteView => IMUtils.isNotNullEmptyStr(widget.quoteContent);
|
||||
|
||||
double get _opacity => (widget.enabled ? 1 : .4);
|
||||
|
||||
bool get _showDirectionalView => widget.directionalText != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.focusNode?.addListener(() {
|
||||
if (widget.focusNode!.hasFocus) {
|
||||
setState(() {
|
||||
_toolsVisible = false;
|
||||
_emojiVisible = false;
|
||||
_leftKeyboardButton = false;
|
||||
_rightKeyboardButton = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
widget.forceCloseToolboxSub?.listen((value) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_toolsVisible = false;
|
||||
_emojiVisible = false;
|
||||
_rightKeyboardButton = false;
|
||||
});
|
||||
});
|
||||
|
||||
widget.controller?.addListener(() {
|
||||
setState(() {
|
||||
_sendButtonVisible = widget.controller!.text.isNotEmpty;
|
||||
});
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.enabled) widget.controller?.clear();
|
||||
return widget.isNotInGroup
|
||||
? const ChatDisableInputBox()
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(minHeight: kInputBoxMinHeight),
|
||||
color: Styles.c_F0F2F6,
|
||||
child: Row(
|
||||
children: [
|
||||
12.horizontalSpace,
|
||||
(_leftKeyboardButton
|
||||
? (ImageRes.openKeyboard.toImage..onTap = onTapLeftKeyboard)
|
||||
: (ImageRes.openVoice.toImage..onTap = onTapSpeak))
|
||||
..width = 32.w
|
||||
..height = 32.h
|
||||
..opacity = _opacity,
|
||||
12.horizontalSpace,
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: _leftKeyboardButton,
|
||||
child: _textFiled,
|
||||
),
|
||||
Offstage(
|
||||
offstage: !_leftKeyboardButton,
|
||||
child: widget.voiceRecordBar,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
12.horizontalSpace,
|
||||
(_rightKeyboardButton
|
||||
? (ImageRes.openKeyboard.toImage..onTap = onTapRightKeyboard)
|
||||
: (ImageRes.openEmoji.toImage..onTap = onTapEmoji))
|
||||
..width = 32.w
|
||||
..height = 32.h
|
||||
..opacity = _opacity,
|
||||
12.horizontalSpace,
|
||||
(_sendButtonVisible ? ImageRes.sendMessage : ImageRes.openToolbox).toImage
|
||||
..width = 32.w
|
||||
..height = 32.h
|
||||
..opacity = _opacity
|
||||
..onTap = _sendButtonVisible ? send : toggleToolbox,
|
||||
12.horizontalSpace,
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_showQuoteView)
|
||||
_QuoteView(
|
||||
content: widget.quoteContent!,
|
||||
onClearQuote: widget.onClearQuote,
|
||||
),
|
||||
if (_showDirectionalView)
|
||||
_SubView(
|
||||
textSpan: widget.directionalText,
|
||||
onClose: () {
|
||||
widget.onCloseDirectional?.call();
|
||||
},
|
||||
),
|
||||
Visibility(
|
||||
visible: _toolsVisible,
|
||||
child: FadeInUp(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: widget.toolbox,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: _emojiVisible,
|
||||
child: FadeInUp(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: widget.emojiView,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _textFiled => Container(
|
||||
margin: EdgeInsets.only(top: 10.h, bottom: _showQuoteView ? 4.h : 10.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
child: ChatTextField(
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
style: widget.style ?? Styles.ts_0C1C33_17sp,
|
||||
atStyle: widget.atStyle ?? Styles.ts_0089FF_17sp,
|
||||
enabled: widget.enabled,
|
||||
hintText: widget.hintText,
|
||||
textAlign: widget.enabled ? TextAlign.start : TextAlign.center,
|
||||
),
|
||||
);
|
||||
|
||||
void send() {
|
||||
if (!widget.enabled) return;
|
||||
if (!_emojiVisible) focus();
|
||||
if (null != widget.onSend && null != widget.controller) {
|
||||
widget.onSend!(widget.controller!.text.toString().trim());
|
||||
}
|
||||
}
|
||||
|
||||
void toggleToolbox() {
|
||||
if (!widget.enabled) return;
|
||||
setState(() {
|
||||
_toolsVisible = !_toolsVisible;
|
||||
_emojiVisible = false;
|
||||
_leftKeyboardButton = false;
|
||||
_rightKeyboardButton = false;
|
||||
if (_toolsVisible) {
|
||||
unfocus();
|
||||
} else {
|
||||
focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onTapSpeak() {
|
||||
if (!widget.enabled) return;
|
||||
Permissions.microphone(() => setState(() {
|
||||
_leftKeyboardButton = true;
|
||||
_rightKeyboardButton = false;
|
||||
_toolsVisible = false;
|
||||
_emojiVisible = false;
|
||||
unfocus();
|
||||
}));
|
||||
}
|
||||
|
||||
void onTapLeftKeyboard() {
|
||||
if (!widget.enabled) return;
|
||||
setState(() {
|
||||
_leftKeyboardButton = false;
|
||||
_toolsVisible = false;
|
||||
_emojiVisible = false;
|
||||
focus();
|
||||
});
|
||||
}
|
||||
|
||||
void onTapRightKeyboard() {
|
||||
if (!widget.enabled) return;
|
||||
setState(() {
|
||||
_rightKeyboardButton = false;
|
||||
_toolsVisible = false;
|
||||
_emojiVisible = false;
|
||||
focus();
|
||||
});
|
||||
}
|
||||
|
||||
void onTapEmoji() {
|
||||
if (!widget.enabled) return;
|
||||
setState(() {
|
||||
_rightKeyboardButton = true;
|
||||
_leftKeyboardButton = false;
|
||||
_emojiVisible = true;
|
||||
_toolsVisible = false;
|
||||
unfocus();
|
||||
});
|
||||
}
|
||||
|
||||
focus() => FocusScope.of(context).requestFocus(widget.focusNode);
|
||||
|
||||
unfocus() => FocusScope.of(context).requestFocus(FocusNode());
|
||||
}
|
||||
|
||||
class _QuoteView extends StatelessWidget {
|
||||
const _QuoteView({
|
||||
Key? key,
|
||||
this.onClearQuote,
|
||||
required this.content,
|
||||
}) : super(key: key);
|
||||
final Function()? onClearQuote;
|
||||
final String content;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(bottom: 10.h, left: 56.w, right: 100.w),
|
||||
color: Styles.c_F0F2F6,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onClearQuote,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 1.h, horizontal: 4.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
content,
|
||||
style: Styles.ts_8E9AB0_14sp,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
ImageRes.delQuote.toImage
|
||||
..width = 14.w
|
||||
..height = 14.h,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SubView extends StatelessWidget {
|
||||
const _SubView({
|
||||
this.onClose,
|
||||
this.title,
|
||||
this.content,
|
||||
this.textSpan,
|
||||
}) : assert(content != null || textSpan != null, 'Either content or textSpan must be provided.');
|
||||
final VoidCallback? onClose;
|
||||
final String? title;
|
||||
final String? content;
|
||||
final InlineSpan? textSpan;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(bottom: 10.h, left: 56.w, right: 100.w),
|
||||
color: Styles.c_F0F2F6,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onClose,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 1.h, horizontal: 4.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
if (title != null)
|
||||
Text(
|
||||
title!,
|
||||
style: Styles.ts_8E9AB0_14sp,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (content != null)
|
||||
Text(
|
||||
title!,
|
||||
style: Styles.ts_8E9AB0_14sp,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (textSpan != null)
|
||||
Expanded(
|
||||
child: RichText(
|
||||
text: textSpan!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ImageRes.delQuote.toImage
|
||||
..width = 14.w
|
||||
..height = 14.h,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatItemContainer extends StatelessWidget {
|
||||
const ChatItemContainer({
|
||||
super.key,
|
||||
required this.id,
|
||||
this.leftFaceUrl,
|
||||
this.rightFaceUrl,
|
||||
this.leftNickname,
|
||||
this.rightNickname,
|
||||
this.timelineStr,
|
||||
this.timeStr,
|
||||
required this.isBubbleBg,
|
||||
required this.isISend,
|
||||
required this.hasRead,
|
||||
required this.isSending,
|
||||
required this.isSendFailed,
|
||||
this.ignorePointer = false,
|
||||
this.showLeftNickname = true,
|
||||
this.showRightNickname = false,
|
||||
required this.readingDuration,
|
||||
this.menus,
|
||||
required this.child,
|
||||
this.popupMenuController,
|
||||
this.sendStatusStream,
|
||||
this.onTapLeftAvatar,
|
||||
this.onTapRightAvatar,
|
||||
this.onLongPressLeftAvatar,
|
||||
this.onLongPressRightAvatar,
|
||||
this.onFailedToResend,
|
||||
});
|
||||
final String id;
|
||||
final String? leftFaceUrl;
|
||||
final String? rightFaceUrl;
|
||||
final String? leftNickname;
|
||||
final String? rightNickname;
|
||||
final String? timelineStr;
|
||||
final String? timeStr;
|
||||
final bool isBubbleBg;
|
||||
final bool isISend;
|
||||
final bool hasRead;
|
||||
final bool isSending;
|
||||
final bool isSendFailed;
|
||||
final bool ignorePointer;
|
||||
final bool showLeftNickname;
|
||||
final bool showRightNickname;
|
||||
final int readingDuration;
|
||||
final List<MenuInfo>? menus;
|
||||
final Widget child;
|
||||
final CustomPopupMenuController? popupMenuController;
|
||||
final Stream<MsgStreamEv<bool>>? sendStatusStream;
|
||||
final Function()? onTapLeftAvatar;
|
||||
final Function()? onTapRightAvatar;
|
||||
final Function()? onLongPressLeftAvatar;
|
||||
final Function()? onLongPressRightAvatar;
|
||||
final Function()? onFailedToResend;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
ignoring: ignorePointer,
|
||||
child: Column(
|
||||
children: [
|
||||
if (null != timelineStr)
|
||||
ChatTimelineView(
|
||||
timeStr: timelineStr!,
|
||||
margin: EdgeInsets.only(bottom: 20.h),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: isISend ? _buildRightView() : _buildLeftView()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChildView(BubbleType type) => (null != menus && menus!.isEmpty)
|
||||
? isBubbleBg
|
||||
? ChatBubble(bubbleType: type, child: child)
|
||||
: child
|
||||
: CopyCustomPopupMenu(
|
||||
controller: popupMenuController,
|
||||
menuBuilder: () => ChatLongPressMenu(
|
||||
popupMenuController: popupMenuController,
|
||||
menus: menus ?? allMenus,
|
||||
),
|
||||
pressType: PressType.longPress,
|
||||
arrowColor: Styles.c_0C1C33_opacity85,
|
||||
barrierColor: Colors.transparent,
|
||||
verticalMargin: 0,
|
||||
child: isBubbleBg ? ChatBubble(bubbleType: type, child: child) : child,
|
||||
);
|
||||
|
||||
Widget _buildLeftView() => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
AvatarView(
|
||||
width: 44.w,
|
||||
height: 44.h,
|
||||
textStyle: Styles.ts_FFFFFF_14sp_medium,
|
||||
url: leftFaceUrl,
|
||||
text: leftNickname,
|
||||
onTap: onTapLeftAvatar,
|
||||
onLongPress: onLongPressLeftAvatar,
|
||||
),
|
||||
10.horizontalSpace,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ChatNicknameView(
|
||||
nickname: showLeftNickname ? leftNickname : null,
|
||||
timeStr: timeStr,
|
||||
),
|
||||
4.verticalSpace,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildChildView(BubbleType.receiver),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildRightView() => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ChatNicknameView(
|
||||
nickname: showRightNickname ? rightNickname : null,
|
||||
timeStr: timeStr,
|
||||
),
|
||||
4.verticalSpace,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isSendFailed)
|
||||
ChatSendFailedView(
|
||||
id: id,
|
||||
isISend: isISend,
|
||||
onFailedToResend: onFailedToResend,
|
||||
isFailed: isSendFailed,
|
||||
stream: sendStatusStream,
|
||||
),
|
||||
if (isSending) ChatDelayedStatusView(isSending: isSending),
|
||||
4.horizontalSpace,
|
||||
_buildChildView(BubbleType.send),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
10.horizontalSpace,
|
||||
AvatarView(
|
||||
width: 44.w,
|
||||
height: 44.h,
|
||||
textStyle: Styles.ts_FFFFFF_14sp_medium,
|
||||
url: rightFaceUrl,
|
||||
text: rightNickname,
|
||||
onTap: onTapRightAvatar,
|
||||
onLongPress: onLongPressRightAvatar,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:focus_detector_v2/focus_detector_v2.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
double maxWidth = 247.w;
|
||||
double pictureWidth = 120.w;
|
||||
double videoWidth = 120.w;
|
||||
double locationWidth = 220.w;
|
||||
|
||||
BorderRadius borderRadius(bool isISend) => BorderRadius.only(
|
||||
topLeft: Radius.circular(isISend ? 6.r : 0),
|
||||
topRight: Radius.circular(isISend ? 0 : 6.r),
|
||||
bottomLeft: Radius.circular(6.r),
|
||||
bottomRight: Radius.circular(6.r),
|
||||
);
|
||||
|
||||
class MsgStreamEv<T> {
|
||||
final String id;
|
||||
final T value;
|
||||
|
||||
MsgStreamEv({required this.id, required this.value});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MsgStreamEv{msgId: $id, value: $value}';
|
||||
}
|
||||
}
|
||||
|
||||
class CustomTypeInfo {
|
||||
final Widget customView;
|
||||
final bool needBubbleBackground;
|
||||
final bool needChatItemContainer;
|
||||
|
||||
CustomTypeInfo(
|
||||
this.customView, [
|
||||
this.needBubbleBackground = true,
|
||||
this.needChatItemContainer = true,
|
||||
]);
|
||||
}
|
||||
|
||||
typedef CustomTypeBuilder = CustomTypeInfo? Function(
|
||||
BuildContext context,
|
||||
Message message,
|
||||
);
|
||||
typedef NotificationTypeBuilder = Widget? Function(
|
||||
BuildContext context,
|
||||
Message message,
|
||||
);
|
||||
typedef ItemViewBuilder = Widget? Function(
|
||||
BuildContext context,
|
||||
Message message,
|
||||
);
|
||||
typedef ItemVisibilityChange = void Function(
|
||||
Message message,
|
||||
bool visible,
|
||||
);
|
||||
|
||||
class ChatItemView extends StatefulWidget {
|
||||
const ChatItemView({
|
||||
Key? key,
|
||||
this.mediaItemBuilder,
|
||||
this.itemViewBuilder,
|
||||
this.customTypeBuilder,
|
||||
this.notificationTypeBuilder,
|
||||
this.sendStatusSubject,
|
||||
this.visibilityChange,
|
||||
this.timelineStr,
|
||||
this.leftNickname,
|
||||
this.leftFaceUrl,
|
||||
this.rightNickname,
|
||||
this.rightFaceUrl,
|
||||
required this.message,
|
||||
this.textScaleFactor = 1.0,
|
||||
this.readingDuration = 30,
|
||||
this.enabledReadStatus = true,
|
||||
this.showLongPressMenu = true,
|
||||
this.isPlayingSound = false,
|
||||
this.ignorePointer = false,
|
||||
this.enabledAddEmojiMenu = true,
|
||||
this.enabledCopyMenu = true,
|
||||
this.enabledDelMenu = true,
|
||||
this.enabledForwardMenu = true,
|
||||
this.enabledReplyMenu = true,
|
||||
this.enabledRevokeMenu = true,
|
||||
this.showLeftNickname = true,
|
||||
this.showRightNickname = false,
|
||||
this.onTapAddEmojiMenu,
|
||||
this.highlightColor,
|
||||
this.allAtMap = const {},
|
||||
this.patterns = const [],
|
||||
this.onTapLeftAvatar,
|
||||
this.onTapRightAvatar,
|
||||
this.onLongPressLeftAvatar,
|
||||
this.onLongPressRightAvatar,
|
||||
this.onTapCopyMenu,
|
||||
this.onTapDelMenu,
|
||||
this.onTapForwardMenu,
|
||||
this.onTapRevokeMenu,
|
||||
this.onVisibleTrulyText,
|
||||
this.onPopMenuShowChanged,
|
||||
this.onFailedToResend,
|
||||
this.closePopMenuSubject,
|
||||
this.onClickItemView,
|
||||
this.fileDownloadProgressView,
|
||||
required this.onTapUserProfile,
|
||||
}) : super(key: key);
|
||||
final ItemViewBuilder? mediaItemBuilder;
|
||||
final ItemViewBuilder? itemViewBuilder;
|
||||
final CustomTypeBuilder? customTypeBuilder;
|
||||
final NotificationTypeBuilder? notificationTypeBuilder;
|
||||
|
||||
final Subject<MsgStreamEv<bool>>? sendStatusSubject;
|
||||
|
||||
final ItemVisibilityChange? visibilityChange;
|
||||
final String? timelineStr;
|
||||
final String? leftNickname;
|
||||
final String? leftFaceUrl;
|
||||
final String? rightNickname;
|
||||
final String? rightFaceUrl;
|
||||
final Message message;
|
||||
|
||||
final double textScaleFactor;
|
||||
|
||||
final int readingDuration;
|
||||
|
||||
final bool enabledReadStatus;
|
||||
|
||||
final bool showLongPressMenu;
|
||||
|
||||
final bool isPlayingSound;
|
||||
|
||||
final bool ignorePointer;
|
||||
final bool enabledCopyMenu;
|
||||
final bool enabledDelMenu;
|
||||
final bool enabledForwardMenu;
|
||||
final bool enabledReplyMenu;
|
||||
final bool enabledRevokeMenu;
|
||||
final bool enabledAddEmojiMenu;
|
||||
final bool showLeftNickname;
|
||||
final bool showRightNickname;
|
||||
|
||||
final Color? highlightColor;
|
||||
final Map<String, String> allAtMap;
|
||||
final List<MatchPattern> patterns;
|
||||
final Function()? onTapLeftAvatar;
|
||||
final Function()? onTapRightAvatar;
|
||||
final Function()? onLongPressLeftAvatar;
|
||||
final Function()? onLongPressRightAvatar;
|
||||
final Function()? onTapCopyMenu;
|
||||
final Function()? onTapDelMenu;
|
||||
final Function()? onTapForwardMenu;
|
||||
final Function()? onTapRevokeMenu;
|
||||
final Function()? onTapAddEmojiMenu;
|
||||
final Function(String? text)? onVisibleTrulyText;
|
||||
final Function(bool show)? onPopMenuShowChanged;
|
||||
final Function()? onClickItemView;
|
||||
final ValueChanged<({String userID, String name, String? faceURL, String? groupID})> onTapUserProfile;
|
||||
|
||||
final Function()? onFailedToResend;
|
||||
|
||||
final Subject<bool>? closePopMenuSubject;
|
||||
|
||||
final Widget? fileDownloadProgressView;
|
||||
@override
|
||||
State<ChatItemView> createState() => _ChatItemViewState();
|
||||
}
|
||||
|
||||
class _ChatItemViewState extends State<ChatItemView> {
|
||||
final _popupCtrl = CustomPopupMenuController();
|
||||
|
||||
Message get _message => widget.message;
|
||||
|
||||
bool get _isISend => _message.sendID == OpenIM.iMManager.userID;
|
||||
|
||||
late StreamSubscription<bool> _keyboardSubs;
|
||||
StreamSubscription<bool>? _closeMenuSubs;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_popupCtrl.dispose();
|
||||
_keyboardSubs.cancel();
|
||||
_closeMenuSubs?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final keyboardVisibilityCtrl = KeyboardVisibilityController();
|
||||
|
||||
_keyboardSubs = keyboardVisibilityCtrl.onChange.listen((bool visible) {
|
||||
_popupCtrl.hideMenu();
|
||||
});
|
||||
|
||||
_popupCtrl.addListener(() {
|
||||
widget.onPopMenuShowChanged?.call(_popupCtrl.menuIsShowing);
|
||||
});
|
||||
|
||||
_closeMenuSubs = widget.closePopMenuSubject?.listen((value) {
|
||||
if (value == true) {
|
||||
_popupCtrl.hideMenu();
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FocusDetector(
|
||||
child: Container(
|
||||
color: widget.highlightColor,
|
||||
margin: EdgeInsets.only(bottom: 20.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
child: Center(child: _child),
|
||||
),
|
||||
onVisibilityLost: () {
|
||||
widget.visibilityChange?.call(widget.message, false);
|
||||
},
|
||||
onVisibilityGained: () {
|
||||
widget.visibilityChange?.call(widget.message, true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _child => widget.itemViewBuilder?.call(context, _message) ?? _buildChildView();
|
||||
|
||||
Widget _buildChildView() {
|
||||
Widget? child;
|
||||
String? senderNickname;
|
||||
String? senderFaceURL;
|
||||
bool isBubbleBg = false;
|
||||
/* if (_message.isCallType) {
|
||||
} else if (_message.isMeetingType) {
|
||||
} else if (_message.isDeletedByFriendType) {
|
||||
} else if (_message.isBlockedByFriendType) {
|
||||
} else if (_message.isEmojiType) {
|
||||
} else if (_message.isTagType) {
|
||||
}*/
|
||||
if (_message.isTextType) {
|
||||
isBubbleBg = true;
|
||||
child = ChatText(
|
||||
text: _message.textElem!.content!,
|
||||
patterns: widget.patterns,
|
||||
textScaleFactor: widget.textScaleFactor,
|
||||
onVisibleTrulyText: widget.onVisibleTrulyText,
|
||||
);
|
||||
} else if (_message.isPictureType) {
|
||||
child = widget.mediaItemBuilder?.call(context, _message) ??
|
||||
ChatPictureView(
|
||||
isISend: _isISend,
|
||||
message: _message,
|
||||
);
|
||||
} else if (_message.isVoiceType) {
|
||||
isBubbleBg = true;
|
||||
final sound = _message.soundElem;
|
||||
child = ChatVoiceView(
|
||||
isISend: _isISend,
|
||||
soundPath: sound?.soundPath,
|
||||
soundUrl: sound?.sourceUrl,
|
||||
duration: sound?.duration,
|
||||
isPlaying: widget.isPlayingSound,
|
||||
);
|
||||
} else if (_message.isVideoType) {
|
||||
child = widget.mediaItemBuilder?.call(context, _message) ??
|
||||
ChatVideoView(
|
||||
isISend: _isISend,
|
||||
message: _message,
|
||||
);
|
||||
} else if (_message.isFileType) {
|
||||
child = ChatFileView(
|
||||
message: _message,
|
||||
isISend: _isISend,
|
||||
fileDownloadProgressView: widget.fileDownloadProgressView,
|
||||
);
|
||||
} else if (_message.isLocationType) {
|
||||
final location = _message.locationElem;
|
||||
child = ChatLocationView(
|
||||
description: location!.description!,
|
||||
latitude: location.latitude!,
|
||||
longitude: location.longitude!,
|
||||
);
|
||||
} else if (_message.isCardType) {
|
||||
child = ChatCarteView(cardElem: _message.cardElem!);
|
||||
} else if (_message.isCustomFaceType) {
|
||||
final face = _message.faceElem;
|
||||
child = ChatCustomEmojiView(
|
||||
index: face?.index,
|
||||
data: face?.data,
|
||||
isISend: _isISend,
|
||||
heroTag: _message.clientMsgID,
|
||||
);
|
||||
} else if (_message.isCustomType) {
|
||||
final info = widget.customTypeBuilder?.call(context, _message);
|
||||
if (null != info) {
|
||||
isBubbleBg = info.needBubbleBackground;
|
||||
child = info.customView;
|
||||
if (!info.needChatItemContainer) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
} else if (_message.isRevokeType) {
|
||||
return child = ChatRevokeView(
|
||||
message: _message,
|
||||
);
|
||||
} else if (_message.isNotificationType) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: ChatHintTextView(
|
||||
message: _message,
|
||||
onTapUserProfile: widget.onTapUserProfile,
|
||||
),
|
||||
);
|
||||
}
|
||||
senderNickname ??= widget.leftNickname ?? _message.senderNickname;
|
||||
senderFaceURL ??= widget.leftFaceUrl ?? _message.senderFaceUrl;
|
||||
return child = ChatItemContainer(
|
||||
id: _message.clientMsgID!,
|
||||
isISend: _isISend,
|
||||
leftNickname: senderNickname,
|
||||
leftFaceUrl: senderFaceURL,
|
||||
rightNickname: widget.rightNickname ?? OpenIM.iMManager.userInfo.nickname,
|
||||
rightFaceUrl: widget.rightFaceUrl ?? OpenIM.iMManager.userInfo.faceURL,
|
||||
showLeftNickname: widget.showLeftNickname,
|
||||
showRightNickname: widget.showRightNickname,
|
||||
timelineStr: widget.timelineStr,
|
||||
timeStr: IMUtils.getChatTimeline(_message.sendTime!, 'HH:mm:ss'),
|
||||
hasRead: _message.isRead!,
|
||||
isSending: _message.isVideoType ? false : _message.status == MessageStatus.sending,
|
||||
isSendFailed: _message.status == MessageStatus.failed,
|
||||
isBubbleBg: child == null ? true : isBubbleBg,
|
||||
menus: widget.showLongPressMenu ? _menusItem : [],
|
||||
ignorePointer: widget.ignorePointer,
|
||||
readingDuration: widget.readingDuration,
|
||||
sendStatusStream: widget.sendStatusSubject,
|
||||
onFailedToResend: widget.onFailedToResend,
|
||||
popupMenuController: _popupCtrl,
|
||||
onLongPressLeftAvatar: widget.onLongPressLeftAvatar,
|
||||
onLongPressRightAvatar: widget.onLongPressRightAvatar,
|
||||
onTapLeftAvatar: widget.onTapLeftAvatar,
|
||||
onTapRightAvatar: widget.onTapRightAvatar,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: widget.onClickItemView,
|
||||
child: child ?? ChatText(text: StrRes.unsupportedMessage),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<MenuInfo> get _menusItem => [
|
||||
if (widget.enabledCopyMenu)
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuCopy,
|
||||
text: StrRes.menuCopy,
|
||||
enabled: widget.enabledCopyMenu,
|
||||
onTap: widget.onTapCopyMenu,
|
||||
),
|
||||
if (widget.enabledDelMenu)
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuDel,
|
||||
text: StrRes.menuDel,
|
||||
enabled: widget.enabledDelMenu,
|
||||
onTap: widget.onTapDelMenu,
|
||||
),
|
||||
if (widget.enabledForwardMenu)
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuForward,
|
||||
text: StrRes.menuForward,
|
||||
enabled: widget.enabledForwardMenu,
|
||||
onTap: widget.onTapForwardMenu,
|
||||
),
|
||||
if (widget.enabledRevokeMenu)
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuRevoke,
|
||||
text: StrRes.menuRevoke,
|
||||
enabled: widget.enabledRevokeMenu,
|
||||
onTap: widget.onTapRevokeMenu,
|
||||
),
|
||||
if (widget.enabledAddEmojiMenu)
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuAddFace,
|
||||
text: StrRes.menuAdd,
|
||||
enabled: widget.enabledAddEmojiMenu,
|
||||
onTap: widget.onTapAddEmojiMenu,
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
extension ScrollControllerExt on ScrollController {
|
||||
Future scrollToBottom(Function()? onScrollStop) async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
while (position.pixels != position.maxScrollExtent) {
|
||||
jumpTo(position.maxScrollExtent);
|
||||
await SchedulerBinding.instance.endOfFrame;
|
||||
}
|
||||
onScrollStop?.call();
|
||||
});
|
||||
}
|
||||
|
||||
Future scrollToTop() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
while (position.pixels != position.minScrollExtent) {
|
||||
jumpTo(position.minScrollExtent);
|
||||
await SchedulerBinding.instance.endOfFrame;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CustomChatListViewController<E> extends ChangeNotifier {
|
||||
final _topList = <E>[];
|
||||
|
||||
final _bottomList = <E>[];
|
||||
|
||||
List<E> get topList => _topList;
|
||||
|
||||
List<E> get bottomList => _bottomList;
|
||||
|
||||
List<E> get list => _topList + _bottomList;
|
||||
|
||||
int get length => list.length;
|
||||
|
||||
bool topHasMore = true;
|
||||
bool bottomHasMore = true;
|
||||
|
||||
CustomChatListViewController(List<E> list) {
|
||||
_bottomList.addAll(list);
|
||||
}
|
||||
|
||||
void insertToTop(E data) {
|
||||
_topList.insert(0, data);
|
||||
}
|
||||
|
||||
void insertAllToTop(Iterable<E> iterable) {
|
||||
_topList.insertAll(0, iterable);
|
||||
}
|
||||
|
||||
void insertToBottom(E data) {
|
||||
_bottomList.add(data);
|
||||
}
|
||||
|
||||
void insertAllToBottom(Iterable<E> iterable) {
|
||||
_bottomList.addAll(iterable);
|
||||
}
|
||||
|
||||
void bottomLoadCompleted(bool hasMore) {
|
||||
bottomHasMore = hasMore;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void topLoadCompleted(bool hasMore) {
|
||||
topHasMore = hasMore;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
E elementAt(int position) => list.elementAt(position);
|
||||
|
||||
E removeAt(int position) => list.removeAt(position);
|
||||
|
||||
bool remove(Object? value) => list.remove(value);
|
||||
}
|
||||
|
||||
typedef CustomChatListViewItemBuilder<T> = Widget Function(
|
||||
BuildContext context,
|
||||
int index,
|
||||
int position,
|
||||
T data,
|
||||
);
|
||||
|
||||
class CustomChatListView extends StatefulWidget {
|
||||
const CustomChatListView({
|
||||
Key? key,
|
||||
required this.itemBuilder,
|
||||
required this.controller,
|
||||
this.scrollController,
|
||||
this.onScrollToTopLoad,
|
||||
this.onScrollToBottomLoad,
|
||||
this.enabledBottomLoad = false,
|
||||
this.enabledTopLoad = false,
|
||||
this.indicatorColor,
|
||||
}) : super(key: key);
|
||||
|
||||
final CustomChatListViewItemBuilder itemBuilder;
|
||||
|
||||
final CustomChatListViewController controller;
|
||||
|
||||
final ScrollController? scrollController;
|
||||
|
||||
final Future<bool> Function()? onScrollToTopLoad;
|
||||
|
||||
final Future<bool> Function()? onScrollToBottomLoad;
|
||||
|
||||
final bool enabledTopLoad;
|
||||
|
||||
final bool enabledBottomLoad;
|
||||
|
||||
final Color? indicatorColor;
|
||||
|
||||
@override
|
||||
State<CustomChatListView> createState() => _CustomChatListViewState();
|
||||
}
|
||||
|
||||
class _CustomChatListViewState extends State<CustomChatListView> {
|
||||
final Key centerKey = const ValueKey('second-sliver-list');
|
||||
|
||||
bool _bottomHasMore = true;
|
||||
|
||||
bool _topHasMore = true;
|
||||
|
||||
bool get _isBottom => widget.scrollController!.offset == widget.scrollController!.position.maxScrollExtent;
|
||||
|
||||
bool get _isTop => widget.scrollController!.offset == widget.scrollController!.position.minScrollExtent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.controller.addListener(_loadFinished);
|
||||
widget.scrollController?.addListener(_scrollListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_loadFinished);
|
||||
widget.scrollController?.removeListener(_scrollListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _loadFinished() {
|
||||
setState(() {
|
||||
_topHasMore = widget.controller.topHasMore;
|
||||
_bottomHasMore = widget.controller.bottomHasMore;
|
||||
});
|
||||
}
|
||||
|
||||
void _scrollListener() {
|
||||
if (widget.enabledBottomLoad && _isBottom && _bottomHasMore) {
|
||||
_onScrollToBottomLoadMore();
|
||||
} else if (widget.enabledTopLoad && _isTop && _topHasMore) {
|
||||
_onScrollToTopLoadMore();
|
||||
}
|
||||
}
|
||||
|
||||
void _onScrollToBottomLoadMore() {
|
||||
widget.onScrollToBottomLoad?.call().then((hasMore) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_bottomHasMore = hasMore;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _onScrollToTopLoadMore() {
|
||||
widget.onScrollToTopLoad?.call().then((hasMore) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_topHasMore = hasMore;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildLoadMoreView() => Container(
|
||||
alignment: Alignment.center,
|
||||
height: 44,
|
||||
child: CupertinoActivityIndicator(
|
||||
color: widget.indicatorColor ?? Colors.blueAccent,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
center: centerKey,
|
||||
controller: widget.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: <Widget>[
|
||||
if (_topHasMore && widget.enabledTopLoad) SliverToBoxAdapter(child: _buildLoadMoreView()),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(_, index) {
|
||||
return widget.itemBuilder(
|
||||
context,
|
||||
index,
|
||||
widget.controller.topList.length - index - 1,
|
||||
widget.controller.topList.elementAt(widget.controller.topList.length - 1 - index),
|
||||
);
|
||||
},
|
||||
childCount: widget.controller.topList.length,
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
key: centerKey,
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(_, index) {
|
||||
return widget.itemBuilder(
|
||||
context,
|
||||
index,
|
||||
widget.controller.topList.length + index,
|
||||
widget.controller.bottomList.elementAt(index),
|
||||
);
|
||||
},
|
||||
childCount: widget.controller.bottomList.length,
|
||||
),
|
||||
),
|
||||
if (_bottomHasMore && widget.enabledBottomLoad) SliverToBoxAdapter(child: _buildLoadMoreView()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatListView extends StatefulWidget {
|
||||
const ChatListView({
|
||||
Key? key,
|
||||
this.physics,
|
||||
this.onTouch,
|
||||
this.itemCount,
|
||||
this.controller,
|
||||
required this.itemBuilder,
|
||||
this.enabledScrollTopLoad = false,
|
||||
this.onScrollToBottomLoad,
|
||||
this.onScrollToTopLoad,
|
||||
this.onScrollToBottom,
|
||||
this.onScrollToTop,
|
||||
}) : super(key: key);
|
||||
final ScrollController? controller;
|
||||
final ScrollPhysics? physics;
|
||||
final int? itemCount;
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
|
||||
final Future<bool> Function()? onScrollToBottomLoad;
|
||||
|
||||
final Future<bool> Function()? onScrollToTopLoad;
|
||||
final Function()? onScrollToBottom;
|
||||
final Function()? onScrollToTop;
|
||||
|
||||
final bool enabledScrollTopLoad;
|
||||
final Function()? onTouch;
|
||||
|
||||
@override
|
||||
State<ChatListView> createState() => _ChatListViewState();
|
||||
}
|
||||
|
||||
class _ChatListViewState extends State<ChatListView> {
|
||||
bool _scrollToBottomLoadMore = true;
|
||||
bool _scrollToTopLoadMore = true;
|
||||
|
||||
bool get _isBottom => widget.controller!.offset >= widget.controller!.position.maxScrollExtent;
|
||||
|
||||
bool get _isTop => widget.controller!.offset <= 0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller?.removeListener(_scrollListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_onScrollToBottomLoadMore();
|
||||
widget.controller?.addListener(_scrollListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
_scrollListener() {
|
||||
if (_isBottom) {
|
||||
Logger.print('-------------ChatListView scroll to bottom');
|
||||
_onScrollToBottomLoadMore();
|
||||
} else if (_isTop) {
|
||||
Logger.print('-------------ChatListView scroll to top');
|
||||
_onScrollToTopLoadMore();
|
||||
}
|
||||
}
|
||||
|
||||
void _onScrollToBottomLoadMore() {
|
||||
widget.onScrollToBottom?.call();
|
||||
widget.onScrollToBottomLoad?.call().then((hasMore) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_scrollToBottomLoadMore = hasMore;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _onScrollToTopLoadMore() {
|
||||
widget.onScrollToTop?.call();
|
||||
if (widget.enabledScrollTopLoad) {
|
||||
widget.onScrollToTopLoad?.call().then((hasMore) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_scrollToTopLoadMore = hasMore;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget get loadMoreView => Container(
|
||||
alignment: Alignment.center,
|
||||
height: 44,
|
||||
child: CupertinoActivityIndicator(color: Styles.c_0089FF),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TouchCloseSoftKeyboard(
|
||||
onTouch: widget.onTouch,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: widget.physics ?? const ClampingScrollPhysics(),
|
||||
itemCount: widget.itemCount ?? 0,
|
||||
padding: EdgeInsets.only(top: 10.h),
|
||||
controller: widget.controller,
|
||||
itemBuilder: (context, index) => _wrapLoadMoreItem(index),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapLoadMoreItem(int index) {
|
||||
final child = widget.itemBuilder(context, index);
|
||||
if (index == widget.itemCount! - 1) {
|
||||
return _scrollToBottomLoadMore ? Column(children: [loadMoreView, child]) : child;
|
||||
}
|
||||
if (index == 0 && widget.enabledScrollTopLoad) {
|
||||
return _scrollToTopLoadMore ? Column(children: [child, loadMoreView]) : child;
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class PositionRetainedScrollPhysics extends ClampingScrollPhysics {
|
||||
final bool shouldRetain;
|
||||
|
||||
const PositionRetainedScrollPhysics({super.parent, this.shouldRetain = true});
|
||||
|
||||
@override
|
||||
PositionRetainedScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return PositionRetainedScrollPhysics(
|
||||
parent: buildParent(ancestor),
|
||||
shouldRetain: shouldRetain,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double adjustPositionForNewDimensions({
|
||||
required ScrollMetrics oldPosition,
|
||||
required ScrollMetrics newPosition,
|
||||
required bool isScrolling,
|
||||
required double velocity,
|
||||
}) {
|
||||
final position = super.adjustPositionForNewDimensions(
|
||||
oldPosition: oldPosition,
|
||||
newPosition: newPosition,
|
||||
isScrolling: isScrolling,
|
||||
velocity: velocity,
|
||||
);
|
||||
|
||||
final diff = newPosition.maxScrollExtent - oldPosition.maxScrollExtent;
|
||||
|
||||
if (oldPosition.pixels > oldPosition.minScrollExtent && diff > 0 && shouldRetain) {
|
||||
return position + diff;
|
||||
} else {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatLocationView extends StatelessWidget {
|
||||
const ChatLocationView({
|
||||
Key? key,
|
||||
required this.description,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
}) : super(key: key);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatNicknameView extends StatelessWidget {
|
||||
const ChatNicknameView({
|
||||
Key? key,
|
||||
this.nickname,
|
||||
this.timeStr,
|
||||
}) : super(key: key);
|
||||
final String? nickname;
|
||||
final String? timeStr;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: '',
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
children: [
|
||||
if (null != nickname)
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 100.w),
|
||||
margin: EdgeInsets.only(right: 6.w),
|
||||
child: nickname!.toText
|
||||
..style = Styles.ts_8E9AB0_12sp
|
||||
..maxLines = 1
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
TextSpan(text: timeStr),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatPicturePreview extends StatelessWidget {
|
||||
ChatPicturePreview({
|
||||
Key? key,
|
||||
this.currentIndex = 0,
|
||||
this.images = const [],
|
||||
this.heroTag,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
}) : controller = images.length > 1 ? ExtendedPageController(initialPage: currentIndex, pageSpacing: 50) : null,
|
||||
super(key: key);
|
||||
final int currentIndex;
|
||||
final List<MediaSource> images;
|
||||
final String? heroTag;
|
||||
final Function()? onTap;
|
||||
final Function(String url)? onLongPress;
|
||||
final ExtendedPageController? controller;
|
||||
GlobalKey<ExtendedImageSlidePageState> slidePagekey = GlobalKey<ExtendedImageSlidePageState>();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExtendedImageSlidePage(
|
||||
key: slidePagekey,
|
||||
slideAxis: SlideAxis.vertical,
|
||||
slidePageBackgroundHandler: (offset, pageSize) => defaultSlidePageBackgroundHandler(
|
||||
color: Colors.black,
|
||||
offset: offset,
|
||||
pageSize: pageSize,
|
||||
),
|
||||
child: MetaHero(
|
||||
heroTag: heroTag,
|
||||
onTap: onTap ?? () => Get.back(),
|
||||
onLongPress: () {
|
||||
final index = controller?.page?.round() ?? 0;
|
||||
onLongPress?.call(images[index].url!);
|
||||
},
|
||||
child: _childView,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _childView {
|
||||
return images.length == 1 ? _networkGestureImage(images[0]) : _pageView;
|
||||
}
|
||||
|
||||
Widget get _pageView => ExtendedImageGesturePageView.builder(
|
||||
controller: controller,
|
||||
onPageChanged: (int index) {},
|
||||
itemCount: images.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _networkGestureImage(images.elementAt(index));
|
||||
},
|
||||
);
|
||||
|
||||
Widget _networkGestureImage(MediaSource source) => ExtendedImage.network(
|
||||
source.thumbnail,
|
||||
fit: BoxFit.contain,
|
||||
mode: ExtendedImageMode.gesture,
|
||||
clearMemoryCacheWhenDispose: true,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
handleLoadingProgress: true,
|
||||
enableSlideOutPage: true,
|
||||
initGestureConfigHandler: (ExtendedImageState state) {
|
||||
return GestureConfig(
|
||||
inPageView: true,
|
||||
initialScale: 1.0,
|
||||
maxScale: 5.0,
|
||||
animationMaxScale: 6.0,
|
||||
initialAlignment: InitialAlignment.center,
|
||||
);
|
||||
},
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
{
|
||||
if (source.url?.isVideoFileName == true) {
|
||||
return null;
|
||||
}
|
||||
final ImageChunkEvent? loadingProgress = state.loadingProgress;
|
||||
final double? progress = loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||
: null;
|
||||
|
||||
return SizedBox(
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Styles.c_0089FF,
|
||||
strokeWidth: 1.5,
|
||||
value: progress ?? 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
case LoadState.completed:
|
||||
final url = source.url;
|
||||
if (url?.isVideoFileName == true) {
|
||||
return Center(
|
||||
child: ChatVideoPlayerView(
|
||||
url: url,
|
||||
coverUrl: source.thumbnail,
|
||||
));
|
||||
}
|
||||
return Center(
|
||||
child: ExtendedImage.network(url!),
|
||||
);
|
||||
case LoadState.failed:
|
||||
state.imageProvider.evict();
|
||||
return ImageRes.pictureError.toImage;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class MetaHero extends StatelessWidget {
|
||||
const MetaHero({
|
||||
Key? key,
|
||||
required this.heroTag,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
}) : super(key: key);
|
||||
final Widget child;
|
||||
final String? heroTag;
|
||||
final Function()? onTap;
|
||||
final Function()? onLongPress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final view = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
child: child,
|
||||
);
|
||||
return heroTag == null ? view : Hero(tag: heroTag!, child: view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class ChatPictureView extends StatefulWidget {
|
||||
const ChatPictureView({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.isISend,
|
||||
}) : super(key: key);
|
||||
final bool isISend;
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
State<ChatPictureView> createState() => _ChatPictureViewState();
|
||||
}
|
||||
|
||||
class _ChatPictureViewState extends State<ChatPictureView> {
|
||||
String? _sourcePath;
|
||||
String? _sourceUrl;
|
||||
|
||||
String? _snapshotUrl;
|
||||
late double _trulyWidth;
|
||||
late double _trulyHeight;
|
||||
|
||||
Message get _message => widget.message;
|
||||
|
||||
Widget? _child;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final picture = _message.pictureElem;
|
||||
_sourcePath = picture?.sourcePath;
|
||||
|
||||
_sourceUrl = picture?.bigPicture?.url;
|
||||
final snap = picture?.snapshotPicture?.url;
|
||||
_snapshotUrl = snap?.adjustThumbnailAbsoluteString(960);
|
||||
|
||||
var w = picture?.sourcePicture?.width?.toDouble() ?? 1.0;
|
||||
var h = picture?.sourcePicture?.height?.toDouble() ?? 1.0;
|
||||
|
||||
if (pictureWidth > w) {
|
||||
_trulyWidth = w;
|
||||
_trulyHeight = h;
|
||||
} else {
|
||||
_trulyWidth = pictureWidth;
|
||||
_trulyHeight = _trulyWidth * h / w;
|
||||
}
|
||||
|
||||
final height = pictureWidth * 1.sh / 1.sw;
|
||||
|
||||
if (_trulyHeight > 2 * height) {
|
||||
_trulyHeight = _trulyWidth;
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
if (_sourcePath?.contains('/Library/Caches/') == true) {
|
||||
getApplicationCacheDirectory().then((value) {
|
||||
final path = _sourcePath!.split('/Library/Caches').last;
|
||||
_sourcePath = value.path + path;
|
||||
_createChildView();
|
||||
});
|
||||
} else {
|
||||
_createChildView();
|
||||
}
|
||||
} else {
|
||||
_createChildView();
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<bool> _checkingPath() async {
|
||||
var valid = IMUtils.isNotNullEmptyStr(_sourcePath);
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
final exist = await File(_sourcePath!).exists();
|
||||
valid = valid && exist;
|
||||
} else {
|
||||
valid = valid && File(_sourcePath!).existsSync();
|
||||
}
|
||||
_message.exMap['validPath_$_sourcePath'] = valid;
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
bool? get isValidPath => _message.exMap['validPath_$_sourcePath'];
|
||||
|
||||
_createChildView() async {
|
||||
if (widget.isISend && (isValidPath == true || isValidPath == null && await _checkingPath())) {
|
||||
_child = _buildPathPicture(path: _sourcePath!);
|
||||
} else if (IMUtils.isNotNullEmptyStr(_snapshotUrl)) {
|
||||
_child = _buildUrlPicture(url: _snapshotUrl!);
|
||||
} else if (IMUtils.isNotNullEmptyStr(_sourceUrl)) {
|
||||
_child = _buildUrlPicture(url: _sourceUrl!);
|
||||
}
|
||||
if (null != _child) {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildUrlPicture({required String url}) => ImageUtil.networkImage(
|
||||
url: url,
|
||||
height: _trulyHeight,
|
||||
width: _trulyWidth,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
|
||||
Widget _buildPathPicture({required String path}) => Stack(
|
||||
children: [
|
||||
ImageUtil.fileImage(
|
||||
file: File(path),
|
||||
height: _trulyHeight,
|
||||
width: _trulyWidth,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = ClipRRect(
|
||||
borderRadius: borderRadius(widget.isISend),
|
||||
child: SizedBox(width: _trulyWidth, height: _trulyHeight, child: _child),
|
||||
);
|
||||
return child;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class MenuInfo {
|
||||
String icon;
|
||||
String text;
|
||||
Function()? onTap;
|
||||
bool enabled;
|
||||
|
||||
MenuInfo({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
this.onTap,
|
||||
this.enabled = true,
|
||||
});
|
||||
}
|
||||
|
||||
class ChatLongPressMenu extends StatelessWidget {
|
||||
final CustomPopupMenuController? popupMenuController;
|
||||
final List<MenuInfo> menus;
|
||||
|
||||
const ChatLongPressMenu({
|
||||
Key? key,
|
||||
required this.popupMenuController,
|
||||
required this.menus,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 256.w, maxHeight: 122.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_0C1C33_opacity85,
|
||||
borderRadius: BorderRadius.circular(15.r),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(15.w, 6.h, 15.w, 3.h),
|
||||
child: Wrap(
|
||||
children: menus
|
||||
.map((e) => _menuItem(
|
||||
icon: e.icon,
|
||||
label: e.text,
|
||||
onTap: e.onTap,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
/*child: GridView.count(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 7.h),
|
||||
crossAxisCount: count,
|
||||
crossAxisSpacing: 4.w,
|
||||
mainAxisSpacing: 4.h,
|
||||
childAspectRatio: 42 / 52,
|
||||
children: menus
|
||||
.map((e) => _menuItem(
|
||||
icon: e.icon,
|
||||
label: e.text,
|
||||
onTap: e.onTap,
|
||||
))
|
||||
.toList(),
|
||||
),*/
|
||||
);
|
||||
}
|
||||
|
||||
Widget _menuItem({
|
||||
required String icon,
|
||||
required String label,
|
||||
Function()? onTap,
|
||||
}) =>
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
popupMenuController?.hideMenu();
|
||||
onTap?.call();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: SizedBox(
|
||||
width: 42.w,
|
||||
height: 52.h,
|
||||
child: _MenuItemView(icon: icon, label: label),
|
||||
),
|
||||
/*child: SizedBox(
|
||||
width: 42.w,
|
||||
height: 52.h,
|
||||
child: _MenuItemView(icon: icon, label: label),
|
||||
),*/
|
||||
);
|
||||
}
|
||||
|
||||
class _MenuItemView extends StatelessWidget {
|
||||
const _MenuItemView({
|
||||
Key? key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
}) : super(key: key);
|
||||
final String icon;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
icon.toImage
|
||||
..width = 28.w
|
||||
..height = 28.h,
|
||||
label.toText
|
||||
..style = Styles.ts_FFFFFF_10sp
|
||||
..maxLines = 1
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final allMenus = <MenuInfo>[
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuCopy,
|
||||
text: StrRes.menuCopy,
|
||||
onTap: () {},
|
||||
),
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuDel,
|
||||
text: StrRes.menuDel,
|
||||
onTap: () {},
|
||||
),
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuForward,
|
||||
text: StrRes.menuForward,
|
||||
onTap: () {},
|
||||
),
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuReply,
|
||||
text: StrRes.menuReply,
|
||||
onTap: () {},
|
||||
),
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuMulti,
|
||||
text: StrRes.menuMulti,
|
||||
onTap: () {},
|
||||
),
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuRevoke,
|
||||
text: StrRes.menuRevoke,
|
||||
onTap: () {},
|
||||
),
|
||||
MenuInfo(
|
||||
icon: ImageRes.menuAddFace,
|
||||
text: StrRes.menuAdd,
|
||||
onTap: () {},
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatRadio extends StatelessWidget {
|
||||
const ChatRadio({
|
||||
Key? key,
|
||||
required this.checked,
|
||||
this.onTap,
|
||||
this.enabled = true,
|
||||
}) : super(key: key);
|
||||
final bool checked;
|
||||
final Function()? onTap;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child:
|
||||
(checked || !enabled ? ImageRes.radioSel : ImageRes.radioNor).toImage
|
||||
..width = 20.w
|
||||
..height = 20.h
|
||||
..opacity = (enabled ? 1 : .5),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:sprintf/sprintf.dart';
|
||||
|
||||
class ChatReadTagView extends StatelessWidget {
|
||||
const ChatReadTagView({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
final Message message;
|
||||
final Function()? onTap;
|
||||
|
||||
int get _needReadMemberCount {
|
||||
final hasReadCount = message.attachedInfoElem?.groupHasReadInfo?.hasReadCount ?? 0;
|
||||
final unreadCount = message.attachedInfoElem?.groupHasReadInfo?.unreadCount ?? 0;
|
||||
return hasReadCount + unreadCount;
|
||||
}
|
||||
|
||||
int get _unreadCount => message.attachedInfoElem?.groupHasReadInfo?.unreadCount ?? 0;
|
||||
|
||||
bool get isRead => message.isRead!;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (message.isSingleChat) {
|
||||
return (isRead ? StrRes.hasRead : StrRes.unread).toText..style = (isRead ? Styles.ts_8E9AB0_12sp : Styles.ts_0089FF_12sp);
|
||||
} else {
|
||||
if (_needReadMemberCount == 0) return const SizedBox();
|
||||
bool isAllRead = _unreadCount <= 0;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: (isAllRead ? StrRes.allRead : sprintf(StrRes.nPersonUnRead, [_unreadCount])).toText
|
||||
..style = (isAllRead ? Styles.ts_8E9AB0_12sp : Styles.ts_0089FF_12sp),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:sprintf/sprintf.dart';
|
||||
|
||||
class ChatRevokeView extends StatelessWidget {
|
||||
const ChatRevokeView({
|
||||
Key? key,
|
||||
required this.message,
|
||||
}) : super(key: key);
|
||||
final Message message;
|
||||
|
||||
bool get _isISend => message.sendID == OpenIM.iMManager.userID;
|
||||
|
||||
String get _who => _isISend ? StrRes.you : message.senderNickname ?? '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bridge = PackageBridge.viewUserProfileBridge;
|
||||
final groupID = message.groupID;
|
||||
String? revoker, sender;
|
||||
final value = <String, String>{};
|
||||
|
||||
var map = json.decode(message.notificationElem!.detail!);
|
||||
var info = RevokedInfo.fromJson(map);
|
||||
if (info.revokerID == info.sourceMessageSendID) {
|
||||
revoker = _who;
|
||||
} else {
|
||||
if (info.revokerID == OpenIM.iMManager.userID) {
|
||||
revoker = info.revokerID!;
|
||||
value[revoker] = StrRes.you;
|
||||
} else {
|
||||
revoker = info.revokerID!;
|
||||
value[revoker] = info.revokerNickname!;
|
||||
}
|
||||
if (info.sourceMessageSendID == OpenIM.iMManager.userID) {
|
||||
sender = info.sourceMessageSendID!;
|
||||
value[sender] = StrRes.you;
|
||||
} else {
|
||||
sender = info.sourceMessageSendID!;
|
||||
value[sender] = info.sourceMessageSenderNickname!;
|
||||
}
|
||||
}
|
||||
|
||||
final List<InlineSpan> children = <InlineSpan>[];
|
||||
if (sender != null) {
|
||||
final text = sprintf(StrRes.aRevokeBMsg, [revoker, sender]);
|
||||
text.splitMapJoin(
|
||||
RegExp('($revoker|$sender)'),
|
||||
onMatch: (match) {
|
||||
final matchText = match[0]!;
|
||||
final nickname = value[matchText];
|
||||
children.add(TextSpan(
|
||||
text: nickname,
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => bridge?.viewUserProfile(
|
||||
matchText,
|
||||
nickname,
|
||||
null,
|
||||
groupID,
|
||||
),
|
||||
));
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (text) {
|
||||
children.add(TextSpan(text: text, style: Styles.ts_8E9AB0_12sp));
|
||||
return '';
|
||||
},
|
||||
);
|
||||
} else {
|
||||
children
|
||||
..add(TextSpan(
|
||||
text: '$revoker ',
|
||||
style: Styles.ts_0089FF_12sp,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => bridge?.viewUserProfile(
|
||||
info.sourceMessageSendID!,
|
||||
info.revokerNickname,
|
||||
null,
|
||||
groupID,
|
||||
),
|
||||
))
|
||||
..add(TextSpan(
|
||||
text: StrRes.revokeMsg,
|
||||
style: Styles.ts_8E9AB0_12sp,
|
||||
));
|
||||
}
|
||||
return RichText(
|
||||
text: TextSpan(children: children),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatSendFailedView extends StatefulWidget {
|
||||
const ChatSendFailedView({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.isISend,
|
||||
this.isFailed = false,
|
||||
this.stream,
|
||||
this.onFailedToResend,
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final bool isISend;
|
||||
final Stream<MsgStreamEv<bool>>? stream;
|
||||
final bool isFailed;
|
||||
final Function()? onFailedToResend;
|
||||
|
||||
@override
|
||||
State<ChatSendFailedView> createState() => _ChatSendFailedViewState();
|
||||
}
|
||||
|
||||
class _ChatSendFailedViewState extends State<ChatSendFailedView> {
|
||||
late bool _failed;
|
||||
StreamSubscription? _statusSubs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_failed = widget.isFailed;
|
||||
_statusSubs = widget.stream?.listen((event) {
|
||||
if (!mounted) return;
|
||||
if (widget.id == event.id) {
|
||||
setState(() {
|
||||
_failed = !event.value;
|
||||
});
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_statusSubs?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Visibility(
|
||||
visible: widget.isISend && _failed,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_failed = false;
|
||||
});
|
||||
widget.onFailedToResend?.call();
|
||||
},
|
||||
child: ImageRes.failedToResend.toImage
|
||||
..width = 16.w
|
||||
..height = 16.h,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatText extends StatelessWidget {
|
||||
const ChatText({
|
||||
Key? key,
|
||||
this.isISend = false,
|
||||
required this.text,
|
||||
this.prefixSpan,
|
||||
this.patterns = const <MatchPattern>[],
|
||||
this.textAlign = TextAlign.left,
|
||||
this.overflow = TextOverflow.clip,
|
||||
this.textStyle,
|
||||
this.maxLines,
|
||||
this.textScaleFactor = 1.0,
|
||||
this.model = TextModel.match,
|
||||
this.onVisibleTrulyText,
|
||||
}) : super(key: key);
|
||||
final bool isISend;
|
||||
final String text;
|
||||
final TextStyle? textStyle;
|
||||
final InlineSpan? prefixSpan;
|
||||
final TextAlign textAlign;
|
||||
final TextOverflow overflow;
|
||||
final int? maxLines;
|
||||
final double textScaleFactor;
|
||||
final List<MatchPattern> patterns;
|
||||
final TextModel model;
|
||||
final Function(String? text)? onVisibleTrulyText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MatchTextView(
|
||||
text: text,
|
||||
textStyle: textStyle ??
|
||||
(isISend ? Styles.ts_FFFFFF_17sp : Styles.ts_0C1C33_17sp),
|
||||
matchTextStyle: Styles.ts_0089FF_17sp,
|
||||
prefixSpan: prefixSpan,
|
||||
textAlign: textAlign,
|
||||
overflow: overflow,
|
||||
textScaleFactor: textScaleFactor,
|
||||
patterns: patterns,
|
||||
model: model,
|
||||
maxLines: maxLines,
|
||||
onVisibleTrulyText: onVisibleTrulyText,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatTextField extends StatelessWidget {
|
||||
final FocusNode? focusNode;
|
||||
final TextEditingController? controller;
|
||||
final String? hintText;
|
||||
|
||||
final TextStyle? style;
|
||||
final TextStyle? atStyle;
|
||||
final bool enabled;
|
||||
final TextAlign textAlign;
|
||||
|
||||
const ChatTextField({
|
||||
Key? key,
|
||||
this.focusNode,
|
||||
this.controller,
|
||||
this.hintText,
|
||||
this.style,
|
||||
this.atStyle,
|
||||
this.enabled = true,
|
||||
this.textAlign = TextAlign.start,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExtendedTextField(
|
||||
style: style,
|
||||
focusNode: focusNode,
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.multiline,
|
||||
enabled: enabled,
|
||||
autofocus: false,
|
||||
minLines: 1,
|
||||
maxLines: 4,
|
||||
textAlign: textAlign,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
hintText: hintText,
|
||||
hintStyle: Styles.ts_8E9AB0_17sp,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 4.w,
|
||||
vertical: 8.h,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatTimelineView extends StatelessWidget {
|
||||
const ChatTimelineView({
|
||||
Key? key,
|
||||
required this.timeStr,
|
||||
this.margin,
|
||||
}) : super(key: key);
|
||||
final String timeStr;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: margin,
|
||||
padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 6.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_F4F5F7,
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
child: timeStr.toText..style = Styles.ts_8E9AB0_12sp,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatToolBox extends StatelessWidget {
|
||||
const ChatToolBox({
|
||||
super.key,
|
||||
this.onTapAlbum,
|
||||
this.onTapCall,
|
||||
this.onTapCamera,
|
||||
this.onTapCard,
|
||||
this.onTapFile,
|
||||
this.onTapLocation,
|
||||
this.onTapDirectionalMessage,
|
||||
});
|
||||
final Function()? onTapAlbum;
|
||||
final Function()? onTapCamera;
|
||||
final Function()? onTapCall;
|
||||
final Function()? onTapFile;
|
||||
final Function()? onTapCard;
|
||||
final Function()? onTapLocation;
|
||||
final VoidCallback? onTapDirectionalMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = [
|
||||
ToolboxItemInfo(
|
||||
text: StrRes.toolboxAlbum,
|
||||
icon: ImageRes.toolboxAlbum,
|
||||
onTap: () => Permissions.photos(onTapAlbum),
|
||||
),
|
||||
ToolboxItemInfo(
|
||||
text: StrRes.toolboxCamera,
|
||||
icon: ImageRes.toolboxCamera,
|
||||
onTap: () => Permissions.cameraAndMicrophone(onTapCamera),
|
||||
),
|
||||
if (onTapCall != null)
|
||||
ToolboxItemInfo(
|
||||
text: StrRes.toolboxCall,
|
||||
icon: ImageRes.toolboxCall,
|
||||
onTap: () => Permissions.cameraAndMicrophone(onTapCall),
|
||||
),
|
||||
ToolboxItemInfo(
|
||||
text: StrRes.toolboxFile,
|
||||
icon: ImageRes.toolboxFile,
|
||||
onTap: () => Permissions.storage(onTapFile),
|
||||
),
|
||||
ToolboxItemInfo(
|
||||
text: StrRes.toolboxCard,
|
||||
icon: ImageRes.toolboxCard,
|
||||
onTap: onTapCard,
|
||||
),
|
||||
ToolboxItemInfo(
|
||||
text: StrRes.toolboxLocation,
|
||||
icon: ImageRes.toolboxLocation,
|
||||
onTap: () => Permissions.location(onTapLocation),
|
||||
),
|
||||
if (onTapDirectionalMessage != null)
|
||||
ToolboxItemInfo(
|
||||
text: StrRes.toolboxDirectionalMessage,
|
||||
icon: ImageRes.toolboxDirectionalMessage,
|
||||
onTap: onTapDirectionalMessage,
|
||||
),
|
||||
];
|
||||
|
||||
return Container(
|
||||
color: Styles.c_F0F2F6,
|
||||
height: 224.h,
|
||||
child: GridView.builder(
|
||||
itemCount: items.length,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16.w,
|
||||
right: 16.w,
|
||||
top: 6.h,
|
||||
bottom: 6.h,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
childAspectRatio: 78.w / 105.h,
|
||||
crossAxisSpacing: 10.w,
|
||||
mainAxisSpacing: 2.h,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
final item = items.elementAt(index);
|
||||
return _buildItemView(
|
||||
icon: item.icon,
|
||||
text: item.text,
|
||||
onTap: item.onTap,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemView({
|
||||
required String text,
|
||||
required String icon,
|
||||
Function()? onTap,
|
||||
}) =>
|
||||
Column(
|
||||
children: [
|
||||
icon.toImage
|
||||
..width = 58.w
|
||||
..height = 58.h
|
||||
..onTap = onTap,
|
||||
10.verticalSpace,
|
||||
text.toText..style = Styles.ts_0C1C33_12sp,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class ToolboxItemInfo {
|
||||
String text;
|
||||
String icon;
|
||||
Function()? onTap;
|
||||
|
||||
ToolboxItemInfo({required this.text, required this.icon, this.onTap});
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../custom_cupertino_controls.dart';
|
||||
|
||||
abstract class VideoControllerService {
|
||||
Future<VideoPlayerController> getVideo(String videoUrl);
|
||||
Future<File?> getCacheFile(String videoUrl);
|
||||
}
|
||||
|
||||
class CachedVideoControllerService extends VideoControllerService {
|
||||
final BaseCacheManager _cacheManager;
|
||||
|
||||
CachedVideoControllerService(this._cacheManager);
|
||||
|
||||
@override
|
||||
Future<VideoPlayerController> getVideo(String videoUrl) async {
|
||||
final file = await getCacheFile(videoUrl);
|
||||
|
||||
if (file == null) {
|
||||
Logger.print('[VideoControllerService]: No video in cache');
|
||||
|
||||
Logger.print('[VideoControllerService]: Saving video to cache');
|
||||
unawaited(_cacheManager.downloadFile(videoUrl));
|
||||
|
||||
return VideoPlayerController.networkUrl(Uri.parse(videoUrl),
|
||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
httpHeaders: Platform.isAndroid
|
||||
? {}
|
||||
: {
|
||||
'AVURLAssetOutOfBandMIMETypeKey': 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
|
||||
});
|
||||
} else {
|
||||
Logger.print('[VideoControllerService]: Loading video from cache');
|
||||
return VideoPlayerController.file(
|
||||
file,
|
||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<File?> getCacheFile(String videoUrl) async {
|
||||
final fileInfo = await _cacheManager.getFileFromCache(videoUrl);
|
||||
|
||||
return fileInfo?.file;
|
||||
}
|
||||
}
|
||||
|
||||
class ChatVideoPlayerView extends StatefulWidget {
|
||||
const ChatVideoPlayerView({
|
||||
super.key,
|
||||
this.path,
|
||||
this.url,
|
||||
this.coverUrl,
|
||||
this.file,
|
||||
this.heroTag,
|
||||
this.onDownload,
|
||||
this.autoPlay = true,
|
||||
this.muted = false,
|
||||
});
|
||||
final String? path;
|
||||
final String? url;
|
||||
final File? file;
|
||||
final String? coverUrl;
|
||||
final String? heroTag;
|
||||
final bool autoPlay;
|
||||
final bool muted;
|
||||
final Function(String? url, File? file)? onDownload;
|
||||
|
||||
@override
|
||||
State<ChatVideoPlayerView> createState() => _ChatVideoPlayerViewState();
|
||||
}
|
||||
|
||||
class _ChatVideoPlayerViewState extends State<ChatVideoPlayerView> with SingleTickerProviderStateMixin {
|
||||
late VideoPlayerController _videoPlayerController;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
final _cachedVideoControllerService = CachedVideoControllerService(DefaultCacheManager());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initializePlayer();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Logger.print('[ChatVideoPlayerView]: dispose');
|
||||
|
||||
() async {
|
||||
await _chewieController?.pause();
|
||||
await _videoPlayerController.pause();
|
||||
await _videoPlayerController.dispose();
|
||||
_chewieController?.dispose();
|
||||
}();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> initializePlayer() async {
|
||||
var file = widget.file;
|
||||
|
||||
if (file == null) {
|
||||
bool existFile = false;
|
||||
if (IMUtils.isNotNullEmptyStr(_path) && (await Permissions.checkStorage())) {
|
||||
file = File(_path!);
|
||||
existFile = await file.exists();
|
||||
if (!existFile) {
|
||||
file = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null != file && file.existsSync()) {
|
||||
_videoPlayerController = VideoPlayerController.file(
|
||||
file,
|
||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
);
|
||||
} else {
|
||||
_videoPlayerController = await _cachedVideoControllerService.getVideo(_url!);
|
||||
}
|
||||
|
||||
await _videoPlayerController.initialize();
|
||||
if (widget.muted) {
|
||||
_videoPlayerController.setVolume(0);
|
||||
}
|
||||
_createChewieController();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _createChewieController() {
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController,
|
||||
autoPlay: widget.autoPlay,
|
||||
looping: false,
|
||||
allowFullScreen: false,
|
||||
allowPlaybackSpeedChanging: false,
|
||||
showControlsOnInitialize: true,
|
||||
customControls: CustomCupertinoControls(backgroundColor: Colors.black.withOpacity(0.7), iconColor: Colors.white),
|
||||
optionsTranslation: OptionsTranslation(
|
||||
playbackSpeedButtonText: StrRes.playSpeed,
|
||||
cancelButtonText: StrRes.cancel,
|
||||
),
|
||||
additionalOptions: (context) => [
|
||||
OptionItem(
|
||||
onTap: () async {
|
||||
final file = await _cachedVideoControllerService.getCacheFile(widget.url!);
|
||||
widget.onDownload?.call(widget.url, file);
|
||||
Get.back();
|
||||
},
|
||||
iconData: Icons.download_outlined,
|
||||
title: StrRes.download,
|
||||
),
|
||||
],
|
||||
errorBuilder: (context, errorMessage) {
|
||||
return Center(
|
||||
child: Text(
|
||||
errorMessage,
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleVideo() async {
|
||||
await _videoPlayerController.pause();
|
||||
await initializePlayer();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_chewieController != null && _chewieController!.videoPlayerController.value.isInitialized)
|
||||
Chewie(controller: _chewieController!)
|
||||
else
|
||||
_buildCoverView(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoverView(BuildContext context) => null != widget.coverUrl
|
||||
? Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: ImageUtil.networkImage(
|
||||
url: widget.coverUrl!,
|
||||
loadProgress: false,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
fit: BoxFit.fitWidth),
|
||||
),
|
||||
const Center(
|
||||
child: CupertinoActivityIndicator(
|
||||
color: Colors.white,
|
||||
radius: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container();
|
||||
|
||||
String? get _path => widget.path;
|
||||
|
||||
String? get _url => widget.url;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class ChatVideoView extends StatefulWidget {
|
||||
const ChatVideoView({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.isISend,
|
||||
}) : super(key: key);
|
||||
final bool isISend;
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
State<ChatVideoView> createState() => _ChatVideoViewState();
|
||||
}
|
||||
|
||||
class _ChatVideoViewState extends State<ChatVideoView> {
|
||||
late double _trulyWidth;
|
||||
late double _trulyHeight;
|
||||
String? _snapshotUrl;
|
||||
String? _snapshotPath;
|
||||
Widget? _child;
|
||||
|
||||
Message get _message => widget.message;
|
||||
@override
|
||||
void initState() {
|
||||
final video = _message.videoElem;
|
||||
_snapshotUrl = video?.snapshotUrl?.adjustThumbnailAbsoluteString(960);
|
||||
_snapshotPath = video?.snapshotPath;
|
||||
|
||||
var w = video?.snapshotWidth?.toDouble() ?? 1.0;
|
||||
var h = video?.snapshotHeight?.toDouble() ?? 1.0;
|
||||
|
||||
_trulyWidth = pictureWidth;
|
||||
_trulyHeight = _trulyWidth * h / w;
|
||||
|
||||
if (Platform.isIOS) {
|
||||
if (_snapshotPath?.contains('/Library/Caches/') == true) {
|
||||
getApplicationCacheDirectory().then((value) {
|
||||
final path = _snapshotPath!.split('/Library/Caches').last;
|
||||
_snapshotPath = value.path + path;
|
||||
_createThumbView();
|
||||
});
|
||||
} else {
|
||||
_createThumbView();
|
||||
}
|
||||
} else {
|
||||
_createThumbView();
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<bool> _checkingPath() async {
|
||||
var valid = IMUtils.isNotNullEmptyStr(_snapshotPath);
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
final exist = File(_snapshotPath!).existsSync();
|
||||
valid = valid && exist;
|
||||
} else {
|
||||
valid = valid && File(_snapshotPath!).existsSync();
|
||||
}
|
||||
_message.exMap['validPath_$_snapshotPath'] = valid;
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
bool? get isValidPath => _message.exMap['validPath_$_snapshotPath'];
|
||||
|
||||
_createThumbView() async {
|
||||
if (widget.isISend && (isValidPath == true || isValidPath == null && await _checkingPath())) {
|
||||
_child = ImageUtil.fileImage(
|
||||
file: File(_snapshotPath!),
|
||||
height: _trulyHeight,
|
||||
width: _trulyWidth,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
} else if (IMUtils.isNotNullEmptyStr(_snapshotUrl)) {
|
||||
_child = ImageUtil.networkImage(
|
||||
url: _snapshotUrl!,
|
||||
width: _trulyWidth,
|
||||
height: _trulyHeight,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
}
|
||||
if (null != _child) {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ClipRRect(
|
||||
borderRadius: borderRadius(widget.isISend),
|
||||
child: SizedBox(
|
||||
width: _trulyWidth,
|
||||
height: _trulyHeight,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (null != _child) _child!,
|
||||
ImageRes.videoPause.toImage
|
||||
..width = 40.w
|
||||
..height = 40.h,
|
||||
if (null != _message.videoElem?.duration)
|
||||
Positioned(
|
||||
bottom: 2.h,
|
||||
right: 3.w,
|
||||
child: IMUtils.seconds2HMS(_message.videoElem!.duration!).toText..style = Styles.ts_FFFFFF_12sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatVoiceReadStatusView extends StatelessWidget {
|
||||
const ChatVoiceReadStatusView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: 6.w,
|
||||
height: 6.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FF381F,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
double kVoiceRecordBarHeight = 36.h;
|
||||
|
||||
enum RecordBarStatus {
|
||||
holdTalk,
|
||||
releaseToSend,
|
||||
liftFingerToCancelSend,
|
||||
}
|
||||
|
||||
class ChatVoiceRecordBar extends StatefulWidget {
|
||||
const ChatVoiceRecordBar({
|
||||
Key? key,
|
||||
required this.onLongPressStart,
|
||||
required this.onLongPressEnd,
|
||||
required this.onLongPressMoveUpdate,
|
||||
this.speakBarColor,
|
||||
this.speakTextStyle,
|
||||
this.interruptListener,
|
||||
this.onChangedBarStatus,
|
||||
}) : super(key: key);
|
||||
final Function(LongPressStartDetails details) onLongPressStart;
|
||||
final Function(LongPressEndDetails details) onLongPressEnd;
|
||||
final Function(LongPressMoveUpdateDetails details) onLongPressMoveUpdate;
|
||||
final Color? speakBarColor;
|
||||
final TextStyle? speakTextStyle;
|
||||
final Stream<bool>? interruptListener;
|
||||
final Function(RecordBarStatus status)? onChangedBarStatus;
|
||||
|
||||
@override
|
||||
State<ChatVoiceRecordBar> createState() => _ChatVoiceRecordBarState();
|
||||
}
|
||||
|
||||
class _ChatVoiceRecordBarState extends State<ChatVoiceRecordBar> {
|
||||
bool _pressing = false;
|
||||
bool _canCancel = false;
|
||||
final double _offset = 40.h;
|
||||
StreamSubscription? _sub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_sub = widget.interruptListener?.listen((interrupt) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_pressing = false;
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: (details) {
|
||||
setState(() {
|
||||
_pressing = true;
|
||||
});
|
||||
},
|
||||
onTapUp: (details) {
|
||||
setState(() {
|
||||
_pressing = false;
|
||||
});
|
||||
},
|
||||
onTapCancel: () {
|
||||
setState(() {
|
||||
_pressing = false;
|
||||
});
|
||||
},
|
||||
onLongPressStart: (details) {
|
||||
HapticFeedback.heavyImpact();
|
||||
widget.onLongPressStart(details);
|
||||
setState(() {
|
||||
_pressing = true;
|
||||
});
|
||||
},
|
||||
onLongPressEnd: (details) {
|
||||
widget.onLongPressEnd(details);
|
||||
setState(() {
|
||||
_pressing = false;
|
||||
_canCancel = false;
|
||||
});
|
||||
},
|
||||
onLongPressMoveUpdate: (details) {
|
||||
widget.onLongPressMoveUpdate(details);
|
||||
|
||||
Offset global = details.globalPosition;
|
||||
setState(() {
|
||||
_canCancel = global.dy < (1.sh - kInputBoxMinHeight - _offset);
|
||||
widget.onChangedBarStatus
|
||||
?.call(_canCancel ? RecordBarStatus.liftFingerToCancelSend : RecordBarStatus.releaseToSend);
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
height: kVoiceRecordBarHeight,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.speakBarColor ?? (_pressing ? Styles.c_8E9AB0_opacity30 : Styles.c_FFFFFF),
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
child: Text(
|
||||
_pressing ? (_canCancel ? StrRes.liftFingerToCancelSend : StrRes.releaseToSend) : StrRes.holdTalk,
|
||||
style: widget.speakTextStyle ?? Styles.ts_0C1C33_14sp_medium,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
typedef SpeakViewChildBuilder = Widget Function(ChatVoiceRecordBar recordBar);
|
||||
|
||||
class ChatVoiceRecordLayout extends StatefulWidget {
|
||||
const ChatVoiceRecordLayout({
|
||||
Key? key,
|
||||
required this.builder,
|
||||
this.locale,
|
||||
this.onCompleted,
|
||||
this.speakTextStyle,
|
||||
this.speakBarColor,
|
||||
this.maxRecordSec = 60,
|
||||
}) : super(key: key);
|
||||
|
||||
final SpeakViewChildBuilder builder;
|
||||
final Locale? locale;
|
||||
final Function(int sec, String path)? onCompleted;
|
||||
final Color? speakBarColor;
|
||||
final TextStyle? speakTextStyle;
|
||||
|
||||
final int maxRecordSec;
|
||||
|
||||
@override
|
||||
State<ChatVoiceRecordLayout> createState() => _ChatVoiceRecordLayoutState();
|
||||
}
|
||||
|
||||
class _ChatVoiceRecordLayoutState extends State<ChatVoiceRecordLayout> {
|
||||
final _interruptSub = PublishSubject<bool>();
|
||||
bool _showVoiceRecordView = false;
|
||||
RecordBarStatus _status = RecordBarStatus.holdTalk;
|
||||
bool _isCancelSend = false;
|
||||
|
||||
void _completed(int sec, String path) {
|
||||
if (_isCancelSend) {
|
||||
File(path).delete();
|
||||
_status = RecordBarStatus.holdTalk;
|
||||
_isCancelSend = false;
|
||||
} else {
|
||||
if (sec == 0) {
|
||||
File(path).delete();
|
||||
IMViews.showToast(StrRes.talkTooShort);
|
||||
} else {
|
||||
widget.onCompleted?.call(sec, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_interruptSub.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ChatVoiceRecordBar get _createSpeakBar => ChatVoiceRecordBar(
|
||||
speakBarColor: widget.speakBarColor,
|
||||
speakTextStyle: widget.speakTextStyle,
|
||||
interruptListener: _interruptSub.stream,
|
||||
onChangedBarStatus: (status) {
|
||||
if (status != _status) {
|
||||
setState(() {
|
||||
_isCancelSend = status == RecordBarStatus.liftFingerToCancelSend;
|
||||
_status = status;
|
||||
});
|
||||
}
|
||||
},
|
||||
onLongPressMoveUpdate: (details) {},
|
||||
onLongPressEnd: (details) async {
|
||||
setState(() {
|
||||
_showVoiceRecordView = false;
|
||||
});
|
||||
},
|
||||
onLongPressStart: (details) {
|
||||
setState(() {
|
||||
_showVoiceRecordView = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
widget.builder(_createSpeakBar),
|
||||
Visibility(
|
||||
visible: _showVoiceRecordView,
|
||||
child: ChatRecordVoiceView(
|
||||
onCompleted: _completed,
|
||||
onInterrupt: () {
|
||||
setState(() {
|
||||
_interruptSub.add(true);
|
||||
_showVoiceRecordView = false;
|
||||
});
|
||||
},
|
||||
builder: (_, sec) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 138.w,
|
||||
height: 124.h,
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: _isCancelSend ? Styles.c_FF381F_opacity70 : Styles.c_0C1C33_opacity60,
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
IMUtils.seconds2HMS(sec).toText..style = Styles.ts_FFFFFF_12sp,
|
||||
Expanded(child: _lottieAnimWidget),
|
||||
(_isCancelSend ? StrRes.liftFingerToCancelSend : StrRes.releaseToSendSwipeUpToCancel).toText
|
||||
..style = Styles.ts_FFFFFF_12sp,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _lottieAnimWidget => Lottie.asset(
|
||||
'assets/anim/voice_record.json',
|
||||
fit: BoxFit.contain,
|
||||
package: 'openim_common',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
class ChatRecordVoiceView extends StatefulWidget {
|
||||
const ChatRecordVoiceView({
|
||||
Key? key,
|
||||
required this.builder,
|
||||
this.maxRecordSec = 60,
|
||||
this.onInterrupt,
|
||||
this.onCompleted,
|
||||
}) : super(key: key);
|
||||
final int maxRecordSec;
|
||||
final Function()? onInterrupt;
|
||||
final Function(int sec, String path)? onCompleted;
|
||||
final Widget Function(BuildContext context, int sec) builder;
|
||||
|
||||
@override
|
||||
State<ChatRecordVoiceView> createState() => _ChatRecordVoiceViewState();
|
||||
}
|
||||
|
||||
class _ChatRecordVoiceViewState extends State<ChatRecordVoiceView> {
|
||||
static const _dir = "voice";
|
||||
static const _ext = ".m4a";
|
||||
late String _path;
|
||||
int _startTimestamp = 0;
|
||||
final _audioRecorder = AudioRecorder();
|
||||
Timer? _timer;
|
||||
int _duration = 0;
|
||||
|
||||
static int _now() => DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
(() async {
|
||||
if (await _audioRecorder.hasPermission()) {
|
||||
_path = '${await IMUtils.createTempDir(dir: _dir)}/${_now()}$_ext';
|
||||
await _audioRecorder.start(RecordConfig(), path: _path);
|
||||
_startTimestamp = _now();
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
|
||||
setState(() {
|
||||
_duration = ((_now() - _startTimestamp) ~/ 1000);
|
||||
if (_duration >= widget.maxRecordSec) {
|
||||
widget.onInterrupt?.call();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
(() async {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
|
||||
if (await _audioRecorder.isRecording()) {
|
||||
await _audioRecorder.stop();
|
||||
}
|
||||
widget.onCompleted?.call(_duration, _path);
|
||||
})();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.builder.call(context, _duration);
|
||||
}
|
||||
|
||||
class _ArrowClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
Path path = Path();
|
||||
path.moveTo(0, -2);
|
||||
path.lineTo(size.width, -2);
|
||||
path.lineTo(size.width / 2, size.height * 2 / 4);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<Path> oldClipper) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ChatVoiceView extends StatelessWidget {
|
||||
final bool isISend;
|
||||
final String? soundPath;
|
||||
final String? soundUrl;
|
||||
final int? duration;
|
||||
final bool isPlaying;
|
||||
|
||||
const ChatVoiceView({
|
||||
Key? key,
|
||||
required this.isISend,
|
||||
this.soundPath,
|
||||
this.soundUrl,
|
||||
this.duration,
|
||||
this.isPlaying = false,
|
||||
}) : super(key: key);
|
||||
|
||||
Widget _buildVoiceAnimView() {
|
||||
return isISend
|
||||
? Row(
|
||||
children: [
|
||||
'${duration ?? 0}``'.toText..style = Styles.ts_0089FF_17sp,
|
||||
4.horizontalSpace,
|
||||
RotatedBox(
|
||||
quarterTurns: 90,
|
||||
child: isPlaying
|
||||
? (ImageRes.voiceBlueAnim.toLottie
|
||||
..height = 25.h
|
||||
..width = 25.w
|
||||
..fit = BoxFit.fitHeight)
|
||||
: (ImageRes.voiceBlue.toImage
|
||||
..width = 25.w
|
||||
..height = 25.h),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isPlaying)
|
||||
ImageRes.voiceBlueAnim.toLottie
|
||||
..width = 24.w
|
||||
..height = 24.h
|
||||
..fit = BoxFit.fitHeight
|
||||
else
|
||||
ImageRes.voiceBlue.toImage
|
||||
..width = 24.w
|
||||
..height = 24.h,
|
||||
4.horizontalSpace,
|
||||
'${duration ?? 0}``'.toText..style = Styles.ts_0089FF_17sp,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: isISend ? _margin : 0,
|
||||
right: !isISend ? _margin : 0,
|
||||
),
|
||||
child: _buildVoiceAnimView(),
|
||||
);
|
||||
}
|
||||
|
||||
double get _margin {
|
||||
final maxWidth = 100.w;
|
||||
const maxDuration = 60;
|
||||
double diff = (duration ?? 0) * maxWidth / maxDuration;
|
||||
return diff > maxWidth ? maxWidth : diff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:sprintf/sprintf.dart';
|
||||
|
||||
class NewMessageIndicator extends StatelessWidget {
|
||||
const NewMessageIndicator({
|
||||
Key? key,
|
||||
this.newMessageCount = 0,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
final int newMessageCount;
|
||||
final Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 13.w, vertical: 7.h),
|
||||
constraints: BoxConstraints(minHeight: 31.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
border: Border.all(color: Styles.c_E8EAEF, width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset(0, 6.h),
|
||||
blurRadius: 16.r,
|
||||
spreadRadius: 1.r,
|
||||
color: Styles.c_8E9AB0_opacity16,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ImageRes.scrollDown.toImage
|
||||
..width = 16.w
|
||||
..height = 16.h,
|
||||
4.horizontalSpace,
|
||||
sprintf(StrRes.nMessage, [newMessageCount]).toText
|
||||
..style = Styles.ts_0089FF_12sp,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class WaterMarkBgView extends StatelessWidget {
|
||||
const WaterMarkBgView({
|
||||
Key? key,
|
||||
this.path,
|
||||
this.text = '',
|
||||
this.newMessageCount = 0,
|
||||
this.textStyle,
|
||||
this.backgroundColor,
|
||||
required this.child,
|
||||
this.topView,
|
||||
this.bottomView,
|
||||
this.floatView,
|
||||
this.onSeeNewMessage,
|
||||
}) : super(key: key);
|
||||
final String? path;
|
||||
final String text;
|
||||
final int newMessageCount;
|
||||
final TextStyle? textStyle;
|
||||
final Color? backgroundColor;
|
||||
final Widget child;
|
||||
final Widget? topView;
|
||||
final Widget? bottomView;
|
||||
final Widget? floatView;
|
||||
final Function()? onSeeNewMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: backgroundColor,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (path?.isNotEmpty == true) Image.file(File(path!), fit: BoxFit.cover),
|
||||
if (text.isNotEmpty) _buildWaterMarkTextView(context: context),
|
||||
Column(
|
||||
children: [
|
||||
if (null != topView) topView!,
|
||||
Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
child,
|
||||
if (newMessageCount > 0)
|
||||
Positioned(
|
||||
bottom: 10.h,
|
||||
child: NewMessageIndicator(
|
||||
newMessageCount: newMessageCount,
|
||||
onTap: onSeeNewMessage,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (null != bottomView) bottomView!
|
||||
],
|
||||
),
|
||||
if (null != floatView) floatView!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaterMarkTextView({required BuildContext context}) {
|
||||
var style = textStyle ??
|
||||
TextStyle(
|
||||
color: Color(0x707070).withOpacity(0.25),
|
||||
fontSize: 16.sp,
|
||||
);
|
||||
double screenW = MediaQuery.of(context).size.width;
|
||||
double screenH = MediaQuery.of(context).size.height;
|
||||
var size = _textSize(text, style);
|
||||
double itemW = size.width;
|
||||
double itemH = size.height;
|
||||
|
||||
int rowCount = (screenW / itemW).round() + 1;
|
||||
int columnCount = (screenH / itemH).round() + 1;
|
||||
|
||||
double maxW = screenW * 1.5;
|
||||
double maxH = screenH * 1.5;
|
||||
|
||||
List<Widget> children = List.filled(
|
||||
columnCount * rowCount,
|
||||
Transform.rotate(
|
||||
angle: -15 * pi / 180,
|
||||
child: Text(
|
||||
text,
|
||||
style: style,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
)),
|
||||
);
|
||||
return ClipRect(
|
||||
child: OverflowBox(
|
||||
maxWidth: maxW,
|
||||
maxHeight: maxH,
|
||||
alignment: Alignment.center,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: 40.w,
|
||||
runSpacing: 70.h,
|
||||
children: children,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Size _textSize(String text, TextStyle style) {
|
||||
final TextPainter textPainter =
|
||||
TextPainter(text: TextSpan(text: text, style: style), maxLines: 1, textDirection: TextDirection.ltr)
|
||||
..layout(minWidth: 0, maxWidth: double.infinity);
|
||||
return textPainter.size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,828 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:chewie/src/animated_play_pause.dart';
|
||||
import 'package:chewie/src/center_play_button.dart';
|
||||
import 'package:chewie/src/chewie_player.dart';
|
||||
import 'package:chewie/src/chewie_progress_colors.dart';
|
||||
import 'package:chewie/src/cupertino/cupertino_progress_bar.dart';
|
||||
import 'package:chewie/src/cupertino/widgets/cupertino_options_dialog.dart';
|
||||
import 'package:chewie/src/helpers/utils.dart';
|
||||
import 'package:chewie/src/models/option_item.dart';
|
||||
import 'package:chewie/src/models/subtitle_model.dart';
|
||||
import 'package:chewie/src/notifiers/index.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class CustomCupertinoControls extends StatefulWidget {
|
||||
const CustomCupertinoControls({
|
||||
required this.backgroundColor,
|
||||
required this.iconColor,
|
||||
this.showPlayButton = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final Color backgroundColor;
|
||||
final Color iconColor;
|
||||
final bool showPlayButton;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _CustomCupertinoControlsState();
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomCupertinoControlsState extends State<CustomCupertinoControls> with SingleTickerProviderStateMixin {
|
||||
late PlayerNotifier notifier;
|
||||
late VideoPlayerValue _latestValue;
|
||||
double? _latestVolume;
|
||||
Timer? _hideTimer;
|
||||
final marginSize = 5.0;
|
||||
Timer? _expandCollapseTimer;
|
||||
Timer? _initTimer;
|
||||
bool _dragging = false;
|
||||
Duration? _subtitlesPosition;
|
||||
bool _subtitleOn = false;
|
||||
Timer? _bufferingDisplayTimer;
|
||||
bool _displayBufferingIndicator = false;
|
||||
|
||||
late VideoPlayerController controller;
|
||||
|
||||
ChewieController get chewieController => _chewieController!;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notifier = Provider.of<PlayerNotifier>(context, listen: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_latestValue.hasError) {
|
||||
return chewieController.errorBuilder != null
|
||||
? chewieController.errorBuilder!(
|
||||
context,
|
||||
chewieController.videoPlayerController.value.errorDescription!,
|
||||
)
|
||||
: const Center(
|
||||
child: Icon(
|
||||
CupertinoIcons.exclamationmark_circle,
|
||||
color: Colors.white,
|
||||
size: 42,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final backgroundColor = widget.backgroundColor;
|
||||
final iconColor = widget.iconColor;
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
final barHeight = orientation == Orientation.portrait ? 30.0 : 47.0;
|
||||
final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0;
|
||||
|
||||
return MouseRegion(
|
||||
onHover: (_) => _cancelAndRestartTimer(),
|
||||
child: GestureDetector(
|
||||
onTap: () => _cancelAndRestartTimer(),
|
||||
child: AbsorbPointer(
|
||||
absorbing: notifier.hideStuff,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_displayBufferingIndicator)
|
||||
const Center(
|
||||
child: CupertinoActivityIndicator(
|
||||
color: Colors.white,
|
||||
radius: 15,
|
||||
),
|
||||
)
|
||||
else
|
||||
_buildHitArea(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
_buildTopBar(
|
||||
backgroundColor,
|
||||
iconColor,
|
||||
barHeight,
|
||||
buttonPadding,
|
||||
),
|
||||
const Spacer(),
|
||||
if (_subtitleOn)
|
||||
Transform.translate(
|
||||
offset: Offset(
|
||||
0.0,
|
||||
notifier.hideStuff ? barHeight * 0.8 : 0.0,
|
||||
),
|
||||
child: _buildSubtitles(chewieController.subtitle!),
|
||||
),
|
||||
_buildBottomBar(backgroundColor, iconColor, barHeight),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispose() {
|
||||
controller.removeListener(_updateState);
|
||||
_hideTimer?.cancel();
|
||||
_expandCollapseTimer?.cancel();
|
||||
_initTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final oldController = _chewieController;
|
||||
_chewieController = ChewieController.of(context);
|
||||
controller = chewieController.videoPlayerController;
|
||||
|
||||
if (oldController != chewieController) {
|
||||
_dispose();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
GestureDetector _buildOptionsButton(
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
final options = <OptionItem>[];
|
||||
|
||||
if (chewieController.additionalOptions != null && chewieController.additionalOptions!(context).isNotEmpty) {
|
||||
options.addAll(chewieController.additionalOptions!(context));
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
if (chewieController.optionsBuilder != null) {
|
||||
await chewieController.optionsBuilder!(context, options);
|
||||
} else {
|
||||
await showCupertinoModalPopup<OptionItem>(
|
||||
context: context,
|
||||
semanticsDismissible: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => CupertinoOptionsDialog(
|
||||
options: options,
|
||||
cancelButtonText: chewieController.optionsTranslation?.cancelButtonText,
|
||||
),
|
||||
);
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(left: 4.0, right: 8.0),
|
||||
margin: const EdgeInsets.only(right: 6.0),
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
color: iconColor,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitles(Subtitles subtitles) {
|
||||
if (!_subtitleOn) {
|
||||
return const SizedBox();
|
||||
}
|
||||
if (_subtitlesPosition == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition!);
|
||||
if (currentSubtitle.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
if (chewieController.subtitleBuilder != null) {
|
||||
return chewieController.subtitleBuilder!(
|
||||
context,
|
||||
currentSubtitle.first!.text,
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: marginSize, right: marginSize),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x96000000),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Text(
|
||||
currentSubtitle.first!.text.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar(
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
return SafeArea(
|
||||
bottom: chewieController.isFullScreen,
|
||||
minimum: chewieController.controlsSafeAreaMinimum,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
alignment: Alignment.bottomCenter,
|
||||
margin: EdgeInsets.all(marginSize),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(
|
||||
sigmaX: 10.0,
|
||||
sigmaY: 10.0,
|
||||
),
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: backgroundColor,
|
||||
child: chewieController.isLive
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
_buildPlayPause(controller, iconColor, barHeight),
|
||||
_buildLive(iconColor),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: <Widget>[
|
||||
_buildPlayPause(controller, iconColor, barHeight),
|
||||
_buildPosition(iconColor),
|
||||
_buildProgressBar(),
|
||||
_buildRemaining(iconColor),
|
||||
_buildSubtitleToggle(iconColor, barHeight),
|
||||
if (chewieController.allowPlaybackSpeedChanging)
|
||||
_buildSpeedButton(controller, iconColor, barHeight),
|
||||
if (chewieController.additionalOptions != null &&
|
||||
chewieController.additionalOptions!(context).isNotEmpty)
|
||||
_buildOptionsButton(iconColor, barHeight),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLive(Color iconColor) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Text(
|
||||
'LIVE',
|
||||
style: TextStyle(color: iconColor, fontSize: 12.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildExpandButton(
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
double buttonPadding,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: _onExpandCollapse,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 10.0),
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: EdgeInsets.only(
|
||||
left: buttonPadding,
|
||||
right: buttonPadding,
|
||||
),
|
||||
color: backgroundColor,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
chewieController.isFullScreen
|
||||
? CupertinoIcons.arrow_down_right_arrow_up_left
|
||||
: CupertinoIcons.arrow_up_left_arrow_down_right,
|
||||
color: iconColor,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHitArea() {
|
||||
final bool isFinished = _latestValue.position >= _latestValue.duration;
|
||||
final bool showPlayButton = widget.showPlayButton && !_latestValue.isPlaying && !_dragging;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: _latestValue.isPlaying
|
||||
? _cancelAndRestartTimer
|
||||
: () {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: widget.backgroundColor,
|
||||
iconColor: widget.iconColor,
|
||||
isFinished: isFinished,
|
||||
isPlaying: controller.value.isPlaying,
|
||||
show: showPlayButton,
|
||||
onPressed: _playPause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildMuteButton(
|
||||
VideoPlayerController controller,
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
double buttonPadding,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (_latestValue.volume == 0) {
|
||||
controller.setVolume(_latestVolume ?? 0.5);
|
||||
} else {
|
||||
_latestVolume = controller.value.volume;
|
||||
controller.setVolume(0.0);
|
||||
}
|
||||
},
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 10.0),
|
||||
child: ColoredBox(
|
||||
color: backgroundColor,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: EdgeInsets.only(
|
||||
left: buttonPadding,
|
||||
right: buttonPadding,
|
||||
),
|
||||
child: Icon(
|
||||
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
|
||||
color: iconColor,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildPlayPause(
|
||||
VideoPlayerController controller,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: _playPause,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
child: AnimatedPlayPause(
|
||||
color: widget.iconColor,
|
||||
playing: controller.value.isPlaying,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPosition(Color iconColor) {
|
||||
final position = _latestValue.position;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Text(
|
||||
formatDuration(position),
|
||||
style: TextStyle(
|
||||
color: iconColor,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRemaining(Color iconColor) {
|
||||
final position = _latestValue.duration - _latestValue.position;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Text(
|
||||
'-${formatDuration(position)}',
|
||||
style: TextStyle(color: iconColor, fontSize: 12.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitleToggle(Color iconColor, double barHeight) {
|
||||
if (chewieController.subtitle?.isEmpty ?? true) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: _subtitleToggle,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
margin: const EdgeInsets.only(right: 10.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.subtitles,
|
||||
color: _subtitleOn ? iconColor : Colors.grey[700],
|
||||
size: 16.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _subtitleToggle() {
|
||||
setState(() {
|
||||
_subtitleOn = !_subtitleOn;
|
||||
});
|
||||
}
|
||||
|
||||
GestureDetector _buildSkipBack(Color iconColor, double barHeight) {
|
||||
return GestureDetector(
|
||||
onTap: _skipBack,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
margin: const EdgeInsets.only(left: 10.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.gobackward_15,
|
||||
color: iconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildSkipForward(Color iconColor, double barHeight) {
|
||||
return GestureDetector(
|
||||
onTap: _skipForward,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 8.0,
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
right: 8.0,
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.goforward_15,
|
||||
color: iconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildSpeedButton(
|
||||
VideoPlayerController controller,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
final chosenSpeed = await showCupertinoModalPopup<double>(
|
||||
context: context,
|
||||
semanticsDismissible: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => _PlaybackSpeedDialog(
|
||||
speeds: chewieController.playbackSpeeds,
|
||||
selected: _latestValue.playbackSpeed,
|
||||
),
|
||||
);
|
||||
|
||||
if (chosenSpeed != null) {
|
||||
controller.setPlaybackSpeed(chosenSpeed);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 8.0,
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
right: 8.0,
|
||||
),
|
||||
child: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.skewY(0.0)
|
||||
..rotateX(math.pi)
|
||||
..rotateZ(math.pi * 0.8),
|
||||
child: Icon(
|
||||
Icons.speed,
|
||||
color: iconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopBar(
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
double buttonPadding,
|
||||
) {
|
||||
return Container(
|
||||
height: barHeight,
|
||||
margin: EdgeInsets.only(
|
||||
top: marginSize,
|
||||
right: marginSize,
|
||||
left: marginSize,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (chewieController.allowFullScreen)
|
||||
_buildExpandButton(
|
||||
backgroundColor,
|
||||
iconColor,
|
||||
barHeight,
|
||||
buttonPadding,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _cancelAndRestartTimer() {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
|
||||
_startHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
|
||||
controller.addListener(_updateState);
|
||||
|
||||
_updateState();
|
||||
|
||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
if (chewieController.showControlsOnInitialize) {
|
||||
_initTimer = Timer(const Duration(milliseconds: 200), () {
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onExpandCollapse() {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
|
||||
chewieController.toggleFullScreen();
|
||||
_expandCollapseTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
_cancelAndRestartTimer();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildProgressBar() {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: CupertinoVideoProgressBar(
|
||||
controller,
|
||||
onDragStart: () {
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
});
|
||||
|
||||
_hideTimer?.cancel();
|
||||
},
|
||||
onDragUpdate: () {
|
||||
_hideTimer?.cancel();
|
||||
},
|
||||
onDragEnd: () {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
});
|
||||
|
||||
_startHideTimer();
|
||||
},
|
||||
colors: chewieController.cupertinoProgressColors ??
|
||||
ChewieProgressColors(
|
||||
playedColor: const Color.fromARGB(
|
||||
120,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
handleColor: const Color.fromARGB(
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
bufferedColor: const Color.fromARGB(
|
||||
60,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
backgroundColor: const Color.fromARGB(
|
||||
20,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _playPause() {
|
||||
final isFinished = _latestValue.position >= _latestValue.duration;
|
||||
|
||||
setState(() {
|
||||
if (controller.value.isPlaying) {
|
||||
notifier.hideStuff = false;
|
||||
_hideTimer?.cancel();
|
||||
controller.pause();
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (!controller.value.isInitialized) {
|
||||
controller.initialize().then((_) {
|
||||
controller.play();
|
||||
});
|
||||
} else {
|
||||
if (isFinished) {
|
||||
controller.seekTo(Duration.zero);
|
||||
}
|
||||
controller.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _skipBack() {
|
||||
_cancelAndRestartTimer();
|
||||
final beginning = Duration.zero.inMilliseconds;
|
||||
final skip = (_latestValue.position - const Duration(seconds: 15)).inMilliseconds;
|
||||
controller.seekTo(Duration(milliseconds: math.max(skip, beginning)));
|
||||
}
|
||||
|
||||
void _skipForward() {
|
||||
_cancelAndRestartTimer();
|
||||
final end = _latestValue.duration.inMilliseconds;
|
||||
final skip = (_latestValue.position + const Duration(seconds: 15)).inMilliseconds;
|
||||
controller.seekTo(Duration(milliseconds: math.min(skip, end)));
|
||||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
||||
? ChewieController.defaultHideControlsTimer
|
||||
: chewieController.hideControlsTimer;
|
||||
_hideTimer = Timer(hideControlsTimer, () {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _bufferingTimerTimeout() {
|
||||
_displayBufferingIndicator = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
if (!mounted) return;
|
||||
|
||||
if (chewieController.progressIndicatorDelay != null) {
|
||||
if (controller.value.isBuffering) {
|
||||
_bufferingDisplayTimer ??= Timer(
|
||||
chewieController.progressIndicatorDelay!,
|
||||
_bufferingTimerTimeout,
|
||||
);
|
||||
} else {
|
||||
_bufferingDisplayTimer?.cancel();
|
||||
_bufferingDisplayTimer = null;
|
||||
_displayBufferingIndicator = false;
|
||||
}
|
||||
} else {
|
||||
_displayBufferingIndicator = controller.value.isBuffering;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_latestValue = controller.value;
|
||||
_subtitlesPosition = controller.value.position;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaybackSpeedDialog extends StatelessWidget {
|
||||
const _PlaybackSpeedDialog({
|
||||
Key? key,
|
||||
required List<double> speeds,
|
||||
required double selected,
|
||||
}) : _speeds = speeds,
|
||||
_selected = selected,
|
||||
super(key: key);
|
||||
|
||||
final List<double> _speeds;
|
||||
final double _selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedColor = CupertinoTheme.of(context).primaryColor;
|
||||
|
||||
return CupertinoActionSheet(
|
||||
actions: _speeds
|
||||
.map(
|
||||
(e) => CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(e);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (e == _selected) Icon(Icons.check, size: 20.0, color: selectedColor),
|
||||
Text(e.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,456 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum PressType {
|
||||
longPress,
|
||||
singleClick,
|
||||
}
|
||||
|
||||
enum PreferredPosition {
|
||||
top,
|
||||
bottom,
|
||||
}
|
||||
|
||||
class CustomPopupMenuController extends ChangeNotifier {
|
||||
bool menuIsShowing = false;
|
||||
|
||||
void showMenu() {
|
||||
menuIsShowing = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void hideMenu() {
|
||||
menuIsShowing = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleMenu() {
|
||||
menuIsShowing = !menuIsShowing;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Rect _menuRect = Rect.zero;
|
||||
|
||||
class CopyCustomPopupMenu extends StatefulWidget {
|
||||
const CopyCustomPopupMenu({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.menuBuilder,
|
||||
required this.pressType,
|
||||
this.controller,
|
||||
this.arrowColor = const Color(0xFF4C4C4C),
|
||||
this.showArrow = true,
|
||||
this.barrierColor = Colors.black12,
|
||||
this.arrowSize = 10.0,
|
||||
this.horizontalMargin = 10.0,
|
||||
this.verticalMargin = 10.0,
|
||||
this.position,
|
||||
this.menuOnChange,
|
||||
this.enablePassEvent = true,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final PressType pressType;
|
||||
final bool showArrow;
|
||||
final Color arrowColor;
|
||||
final Color barrierColor;
|
||||
final double horizontalMargin;
|
||||
final double verticalMargin;
|
||||
final double arrowSize;
|
||||
final CustomPopupMenuController? controller;
|
||||
final Widget Function() menuBuilder;
|
||||
final PreferredPosition? position;
|
||||
final void Function(bool)? menuOnChange;
|
||||
|
||||
final bool enablePassEvent;
|
||||
|
||||
@override
|
||||
State<CopyCustomPopupMenu> createState() => _CustomPopupMenuState();
|
||||
}
|
||||
|
||||
class _CustomPopupMenuState extends State<CopyCustomPopupMenu> {
|
||||
RenderBox? _childBox;
|
||||
RenderBox? _parentBox;
|
||||
OverlayEntry? _overlayEntry;
|
||||
CustomPopupMenuController? _controller;
|
||||
bool _canResponse = true;
|
||||
TapDownDetails? _tapDownDetails;
|
||||
|
||||
_showMenu() {
|
||||
Widget arrow = ClipPath(
|
||||
clipper: _ArrowClipper(),
|
||||
child: Container(
|
||||
width: widget.arrowSize,
|
||||
height: widget.arrowSize,
|
||||
color: widget.arrowColor,
|
||||
),
|
||||
);
|
||||
|
||||
final viewInsets = EdgeInsets.fromWindowPadding(
|
||||
WidgetsBinding.instance.window.viewInsets,
|
||||
WidgetsBinding.instance.window.devicePixelRatio,
|
||||
);
|
||||
|
||||
var keyboardHeight = viewInsets.bottom;
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
Widget menu = Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _parentBox!.size.width - 2 * widget.horizontalMargin,
|
||||
minWidth: 0,
|
||||
),
|
||||
child: CustomMultiChildLayout(
|
||||
delegate: _MenuLayoutDelegate(
|
||||
anchorSize: _childBox!.size,
|
||||
anchorOffset: _childBox!.localToGlobal(
|
||||
Offset(-widget.horizontalMargin, 0),
|
||||
),
|
||||
keyboardHeight: keyboardHeight,
|
||||
verticalMargin: widget.verticalMargin,
|
||||
position: widget.position,
|
||||
onTapDownOffset: _tapDownDetails?.globalPosition,
|
||||
),
|
||||
children: <Widget>[
|
||||
if (widget.showArrow)
|
||||
LayoutId(
|
||||
id: _MenuLayoutId.arrow,
|
||||
child: arrow,
|
||||
),
|
||||
if (widget.showArrow)
|
||||
LayoutId(
|
||||
id: _MenuLayoutId.downArrow,
|
||||
child: Transform.rotate(
|
||||
angle: math.pi,
|
||||
child: arrow,
|
||||
),
|
||||
),
|
||||
LayoutId(
|
||||
id: _MenuLayoutId.content,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: widget.menuBuilder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return Listener(
|
||||
behavior: widget.enablePassEvent ? HitTestBehavior.translucent : HitTestBehavior.opaque,
|
||||
onPointerDown: (PointerDownEvent event) {
|
||||
Offset offset = event.localPosition;
|
||||
|
||||
if (_menuRect.contains(Offset(offset.dx - widget.horizontalMargin, offset.dy))) {
|
||||
return;
|
||||
}
|
||||
_controller?.hideMenu();
|
||||
|
||||
_canResponse = false;
|
||||
Future.delayed(const Duration(milliseconds: 300)).then((_) => _canResponse = true);
|
||||
},
|
||||
child: widget.barrierColor == Colors.transparent
|
||||
? menu
|
||||
: Container(
|
||||
color: widget.barrierColor,
|
||||
child: menu,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (_overlayEntry != null) {
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
}
|
||||
|
||||
_hideMenu() {
|
||||
if (_overlayEntry != null) {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
}
|
||||
|
||||
_updateView() {
|
||||
bool menuIsShowing = _controller?.menuIsShowing ?? false;
|
||||
widget.menuOnChange?.call(menuIsShowing);
|
||||
if (menuIsShowing) {
|
||||
_showMenu();
|
||||
} else {
|
||||
_hideMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = widget.controller;
|
||||
_controller ??= CustomPopupMenuController();
|
||||
_controller?.addListener(_updateView);
|
||||
WidgetsBinding.instance.addPostFrameCallback((call) {
|
||||
if (mounted) {
|
||||
_childBox = context.findRenderObject() as RenderBox?;
|
||||
_parentBox = Overlay.of(context).context.findRenderObject() as RenderBox?;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hideMenu();
|
||||
_controller?.removeListener(_updateView);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var child = Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
hoverColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
child: widget.child,
|
||||
onTap: () {
|
||||
if (widget.pressType == PressType.singleClick && _canResponse) {
|
||||
_controller?.showMenu();
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (widget.pressType == PressType.longPress && _canResponse) {
|
||||
_controller?.showMenu();
|
||||
}
|
||||
},
|
||||
onTapDown: (details) {
|
||||
_tapDownDetails = details;
|
||||
},
|
||||
),
|
||||
);
|
||||
if (Platform.isIOS) {
|
||||
return child;
|
||||
} else {
|
||||
bool menuIsShowing = _controller?.menuIsShowing ?? false;
|
||||
return WillPopScope(
|
||||
onWillPop: menuIsShowing
|
||||
? () {
|
||||
_hideMenu();
|
||||
return Future.value(false);
|
||||
}
|
||||
: null,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _MenuLayoutId {
|
||||
arrow,
|
||||
downArrow,
|
||||
content,
|
||||
}
|
||||
|
||||
enum _MenuPosition {
|
||||
bottomLeft,
|
||||
bottomCenter,
|
||||
bottomRight,
|
||||
topLeft,
|
||||
topCenter,
|
||||
topRight,
|
||||
}
|
||||
|
||||
class _MenuLayoutDelegate extends MultiChildLayoutDelegate {
|
||||
_MenuLayoutDelegate({
|
||||
required this.anchorSize,
|
||||
required this.anchorOffset,
|
||||
required this.verticalMargin,
|
||||
required this.keyboardHeight,
|
||||
this.position,
|
||||
this.onTapDownOffset,
|
||||
});
|
||||
|
||||
final Size anchorSize;
|
||||
final Offset anchorOffset;
|
||||
final double verticalMargin;
|
||||
final PreferredPosition? position;
|
||||
final double keyboardHeight;
|
||||
final Offset? onTapDownOffset;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
Size contentSize = Size.zero;
|
||||
Size arrowSize = Size.zero;
|
||||
Offset contentOffset = const Offset(0, 0);
|
||||
Offset arrowOffset = const Offset(0, 0);
|
||||
|
||||
double anchorCenterX = anchorOffset.dx + anchorSize.width / 2;
|
||||
double anchorTopY = anchorOffset.dy;
|
||||
double anchorBottomY = anchorTopY + anchorSize.height;
|
||||
double? touchY = onTapDownOffset?.dy;
|
||||
_MenuPosition menuPosition = _MenuPosition.bottomCenter;
|
||||
|
||||
if (hasChild(_MenuLayoutId.content)) {
|
||||
contentSize = layoutChild(
|
||||
_MenuLayoutId.content,
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
}
|
||||
if (hasChild(_MenuLayoutId.arrow)) {
|
||||
arrowSize = layoutChild(
|
||||
_MenuLayoutId.arrow,
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
}
|
||||
if (hasChild(_MenuLayoutId.downArrow)) {
|
||||
layoutChild(
|
||||
_MenuLayoutId.downArrow,
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
}
|
||||
|
||||
bool isTop = false;
|
||||
|
||||
if (position == null) {
|
||||
isTop = anchorBottomY > (size.height - keyboardHeight) / 2;
|
||||
} else {
|
||||
isTop = position == PreferredPosition.top;
|
||||
}
|
||||
|
||||
double minTopMargin = contentSize.height + arrowSize.height;
|
||||
if (null != touchY && anchorTopY < minTopMargin) {
|
||||
if (touchY < minTopMargin) {
|
||||
isTop = false;
|
||||
if (anchorSize.height > size.height * 0.8) {
|
||||
anchorBottomY = touchY;
|
||||
}
|
||||
} else {
|
||||
anchorTopY = touchY;
|
||||
}
|
||||
}
|
||||
|
||||
if (anchorCenterX - contentSize.width / 2 < 0) {
|
||||
menuPosition = isTop ? _MenuPosition.topLeft : _MenuPosition.bottomLeft;
|
||||
} else if (anchorCenterX + contentSize.width / 2 > size.width) {
|
||||
menuPosition = isTop ? _MenuPosition.topRight : _MenuPosition.bottomRight;
|
||||
} else {
|
||||
menuPosition = isTop ? _MenuPosition.topCenter : _MenuPosition.bottomCenter;
|
||||
}
|
||||
|
||||
switch (menuPosition) {
|
||||
case _MenuPosition.bottomCenter:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorBottomY + verticalMargin,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
anchorCenterX - contentSize.width / 2,
|
||||
anchorBottomY + verticalMargin + arrowSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.bottomLeft:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorBottomY + verticalMargin,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
0,
|
||||
anchorBottomY + verticalMargin + arrowSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.bottomRight:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorBottomY + verticalMargin,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
size.width - contentSize.width,
|
||||
anchorBottomY + verticalMargin + arrowSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.topCenter:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorTopY - verticalMargin - arrowSize.height,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
anchorCenterX - contentSize.width / 2,
|
||||
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.topLeft:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorTopY - verticalMargin - arrowSize.height,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
0,
|
||||
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.topRight:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorTopY - verticalMargin - arrowSize.height,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
size.width - contentSize.width,
|
||||
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasChild(_MenuLayoutId.content)) {
|
||||
positionChild(_MenuLayoutId.content, contentOffset);
|
||||
}
|
||||
|
||||
_menuRect = Rect.fromLTWH(
|
||||
contentOffset.dx,
|
||||
contentOffset.dy,
|
||||
contentSize.width,
|
||||
contentSize.height,
|
||||
);
|
||||
bool isBottom = false;
|
||||
if (_MenuPosition.values.indexOf(menuPosition) < 3) {
|
||||
isBottom = true;
|
||||
}
|
||||
if (hasChild(_MenuLayoutId.arrow)) {
|
||||
positionChild(
|
||||
_MenuLayoutId.arrow,
|
||||
isBottom ? Offset(arrowOffset.dx, arrowOffset.dy + 0.1) : const Offset(-100, 0),
|
||||
);
|
||||
}
|
||||
if (hasChild(_MenuLayoutId.downArrow)) {
|
||||
positionChild(
|
||||
_MenuLayoutId.downArrow,
|
||||
!isBottom ? Offset(arrowOffset.dx, arrowOffset.dy - 0.1) : const Offset(-100, 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _ArrowClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
Path path = Path();
|
||||
path.moveTo(0, size.height);
|
||||
path.lineTo(size.width / 2, size.height / 2);
|
||||
path.lineTo(size.width, size.height);
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<Path> oldClipper) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
enum DialogType {
|
||||
confirm,
|
||||
}
|
||||
|
||||
class CustomDialog extends StatelessWidget {
|
||||
const CustomDialog({
|
||||
Key? key,
|
||||
this.title,
|
||||
this.url,
|
||||
this.content,
|
||||
this.rightText,
|
||||
this.leftText,
|
||||
this.onTapLeft,
|
||||
this.onTapRight,
|
||||
}) : super(key: key);
|
||||
final String? title;
|
||||
final String? url;
|
||||
final String? content;
|
||||
final String? rightText;
|
||||
final String? leftText;
|
||||
final Function()? onTapLeft;
|
||||
final Function()? onTapRight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
child: Container(
|
||||
width: 280.w,
|
||||
color: Styles.c_FFFFFF,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20.w,
|
||||
vertical: 20.h,
|
||||
),
|
||||
child: Text(
|
||||
title ?? '',
|
||||
style: Styles.ts_0C1C33_17sp,
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
color: Styles.c_E8EAEF,
|
||||
height: 0.5.h,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_button(
|
||||
bgColor: Styles.c_FFFFFF,
|
||||
text: leftText ?? StrRes.cancel,
|
||||
textStyle: Styles.ts_0C1C33_17sp,
|
||||
onTap: onTapLeft ?? () => Get.back(result: false),
|
||||
),
|
||||
Container(
|
||||
color: Styles.c_E8EAEF,
|
||||
width: 0.5.w,
|
||||
height: 48.h,
|
||||
),
|
||||
_button(
|
||||
bgColor: Styles.c_FFFFFF,
|
||||
text: rightText ?? StrRes.determine,
|
||||
textStyle: Styles.ts_0089FF_17sp,
|
||||
onTap: onTapRight ?? () => Get.back(result: true),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _button({
|
||||
required Color bgColor,
|
||||
required String text,
|
||||
required TextStyle textStyle,
|
||||
Function()? onTap,
|
||||
}) =>
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
),
|
||||
height: 48.h,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
text,
|
||||
style: textStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ForwardHintDialog extends StatelessWidget {
|
||||
const ForwardHintDialog({
|
||||
Key? key,
|
||||
required this.title,
|
||||
this.checkedList = const [],
|
||||
}) : super(key: key);
|
||||
final String title;
|
||||
final List<dynamic> checkedList;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final list = IMUtils.convertCheckedListToForwardObj(checkedList);
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
|
||||
margin: EdgeInsets.symmetric(horizontal: 36.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
(list.length == 1 ? StrRes.sentTo : StrRes.sentSeparatelyTo).toText
|
||||
..style = Styles.ts_0C1C33_17sp_medium,
|
||||
5.verticalSpace,
|
||||
list.length == 1
|
||||
? Row(
|
||||
children: [
|
||||
AvatarView(
|
||||
url: list.first['faceURL'],
|
||||
text: list.first['nickname'],
|
||||
),
|
||||
10.horizontalSpace,
|
||||
Expanded(
|
||||
child: (list.first['nickname'] ?? '').toText
|
||||
..style = Styles.ts_0C1C33_17sp
|
||||
..maxLines = 1
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
)
|
||||
: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: 120.h),
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
crossAxisSpacing: 10.w,
|
||||
mainAxisSpacing: 0,
|
||||
childAspectRatio: 50.w / 65.h,
|
||||
),
|
||||
itemCount: list.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (_, index) => Column(
|
||||
children: [
|
||||
AvatarView(
|
||||
url: list.elementAt(index)['faceURL'],
|
||||
text: list.elementAt(index)['nickname'],
|
||||
),
|
||||
10.horizontalSpace,
|
||||
(list.elementAt(index)['nickname'] ?? '').toText
|
||||
..style = Styles.ts_8E9AB0_10sp
|
||||
..maxLines = 1
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
5.verticalSpace,
|
||||
title.toText
|
||||
..style = Styles.ts_8E9AB0_14sp
|
||||
..maxLines = 1
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
16.verticalSpace,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
StrRes.cancel.toText
|
||||
..style = Styles.ts_0C1C33_17sp
|
||||
..onTap = () => Get.back(),
|
||||
26.horizontalSpace,
|
||||
StrRes.determine.toText
|
||||
..style = Styles.ts_0089FF_17sp
|
||||
..onTap = () => Get.back(result: true),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class ExpandedText extends StatefulWidget {
|
||||
const ExpandedText({
|
||||
Key? key,
|
||||
required this.text,
|
||||
this.textStyle,
|
||||
this.maxLines = 4,
|
||||
}) : super(key: key);
|
||||
final String text;
|
||||
final TextStyle? textStyle;
|
||||
final int maxLines;
|
||||
|
||||
@override
|
||||
State<ExpandedText> createState() => _ExpandedTextState();
|
||||
}
|
||||
|
||||
class _ExpandedTextState extends State<ExpandedText> {
|
||||
bool _isExpand = false;
|
||||
|
||||
TextStyle get _textStyle => widget.textStyle ?? Styles.ts_0C1C33_17sp;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (_, cons) {
|
||||
final tp = IMUtils.getTextPainter(
|
||||
widget.text,
|
||||
_textStyle,
|
||||
maxLines: widget.maxLines,
|
||||
maxWidth: cons.maxWidth,
|
||||
);
|
||||
return tp.didExceedMaxLines
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_isExpand
|
||||
? Text(widget.text, style: _textStyle)
|
||||
: Text(
|
||||
widget.text,
|
||||
style: _textStyle,
|
||||
maxLines: widget.maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isExpand = !_isExpand;
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 2.h),
|
||||
child: (_isExpand ? StrRes.rollUp : StrRes.fullText).toText
|
||||
..style = Styles.ts_0089FF_17sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(widget.text, style: _textStyle);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:syncfusion_flutter_core/theme.dart';
|
||||
import 'package:syncfusion_flutter_sliders/sliders.dart';
|
||||
|
||||
class FontSizeSlider extends StatelessWidget {
|
||||
const FontSizeSlider({
|
||||
Key? key,
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
}) : super(key: key);
|
||||
final double value;
|
||||
final Function(dynamic value)? onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Styles.c_FFFFFF,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20.w,
|
||||
vertical: 20.h,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildIndicatorLabel(),
|
||||
SfSliderTheme(
|
||||
data: SfSliderThemeData(
|
||||
activeTrackHeight: 1,
|
||||
inactiveTrackHeight: 1,
|
||||
activeTrackColor: Styles.c_8E9AB0_opacity30,
|
||||
inactiveTrackColor: Styles.c_8E9AB0_opacity30,
|
||||
activeTickColor: Styles.c_8E9AB0_opacity30,
|
||||
inactiveTickColor: Styles.c_8E9AB0_opacity30,
|
||||
activeMinorTickColor: Styles.c_8E9AB0_opacity30,
|
||||
inactiveMinorTickColor: Styles.c_8E9AB0_opacity30,
|
||||
thumbColor: Styles.c_FFFFFF,
|
||||
tickOffset: Offset(0, -10.h),
|
||||
),
|
||||
child: SfSlider(
|
||||
min: 0,
|
||||
max: 2,
|
||||
value: value,
|
||||
interval: 1,
|
||||
showTicks: true,
|
||||
showLabels: false,
|
||||
labelFormatterCallback: (actualValue, formattedText) {
|
||||
return '打';
|
||||
},
|
||||
minorTicksPerInterval: 1,
|
||||
labelPlacement: LabelPlacement.onTicks,
|
||||
edgeLabelPlacement: EdgeLabelPlacement.inside,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIndicatorLabel() => Stack(
|
||||
children: [
|
||||
StrRes.little.toText
|
||||
..style = Styles.ts_0C1C33_12sp
|
||||
..onTap = () => onChanged?.call(.0),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: StrRes.standard.toText
|
||||
..style = Styles.ts_0C1C33_17sp
|
||||
..onTap = () => onChanged?.call(1.0),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: StrRes.big.toText
|
||||
..style = Styles.ts_0C1C33_20sp
|
||||
..onTap = () => onChanged?.call(2.0),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
library gesture_x_detector;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:vector_math/vector_math.dart' as vector;
|
||||
|
||||
class XGestureDetector extends StatefulWidget {
|
||||
const XGestureDetector(
|
||||
{super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onMoveUpdate,
|
||||
this.onMoveEnd,
|
||||
this.onMoveStart,
|
||||
this.onScaleStart,
|
||||
this.onScaleUpdate,
|
||||
this.onScaleEnd,
|
||||
this.onDoubleTap,
|
||||
this.onPointerDown,
|
||||
this.onScrollEvent,
|
||||
this.bypassMoveEventAfterLongPress = true,
|
||||
this.bypassTapEventOnDoubleTap = false,
|
||||
this.doubleTapTimeConsider = 250,
|
||||
this.longPressTimeConsider = 350,
|
||||
this.onLongPress,
|
||||
this.onLongPressMove,
|
||||
this.onLongPressEnd,
|
||||
this.behavior = HitTestBehavior.deferToChild,
|
||||
this.longPressMaximumRangeAllowed = 25});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final bool bypassTapEventOnDoubleTap;
|
||||
|
||||
final bool bypassMoveEventAfterLongPress;
|
||||
|
||||
final int doubleTapTimeConsider;
|
||||
|
||||
final TapEventListener? onTap;
|
||||
|
||||
final MoveEventListener? onMoveStart;
|
||||
|
||||
final MoveEventListener? onMoveUpdate;
|
||||
|
||||
final MoveEventListener? onMoveEnd;
|
||||
|
||||
final void Function(Offset initialFocusPoint)? onScaleStart;
|
||||
|
||||
final ScaleEventListener? onScaleUpdate;
|
||||
|
||||
final void Function()? onScaleEnd;
|
||||
|
||||
final TapEventListener? onDoubleTap;
|
||||
|
||||
final TapEventListener? onLongPress;
|
||||
|
||||
final MoveEventListener? onLongPressMove;
|
||||
|
||||
final Function()? onLongPressEnd;
|
||||
|
||||
final Function(ScrollEvent event)? onScrollEvent;
|
||||
|
||||
final int longPressTimeConsider;
|
||||
|
||||
final HitTestBehavior behavior;
|
||||
|
||||
final int longPressMaximumRangeAllowed;
|
||||
|
||||
final Function(PointerDownEvent event)? onPointerDown;
|
||||
|
||||
@override
|
||||
State<XGestureDetector> createState() => _XGestureDetectorState();
|
||||
}
|
||||
|
||||
enum _GestureState { PointerDown, MoveStart, ScaleStart, Scalling, LongPress, Unknown }
|
||||
|
||||
class _XGestureDetectorState extends State<XGestureDetector> {
|
||||
List<_Touch> touches = [];
|
||||
double initialScaleDistance = 1.0;
|
||||
_GestureState state = _GestureState.Unknown;
|
||||
Timer? doubleTapTimer;
|
||||
Timer? longPressTimer;
|
||||
Offset lastTouchUpPos = const Offset(0, 0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
behavior: widget.behavior,
|
||||
onPointerDown: onPointerDown,
|
||||
onPointerUp: onPointerUp,
|
||||
onPointerMove: onPointerMove,
|
||||
onPointerCancel: onPointerUp,
|
||||
onPointerSignal: onPointerSignal,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void onPointerSignal(PointerSignalEvent event) {
|
||||
if (event is PointerScrollEvent) {
|
||||
widget.onScrollEvent?.call(ScrollEvent(event.pointer, event.localPosition, event.position, event.scrollDelta));
|
||||
}
|
||||
}
|
||||
|
||||
void onPointerDown(PointerDownEvent event) {
|
||||
widget.onPointerDown?.call(event);
|
||||
touches.add(_Touch(event.pointer, event.localPosition));
|
||||
|
||||
if (touchCount == 1) {
|
||||
state = _GestureState.PointerDown;
|
||||
startLongPressTimer(TapEvent.from(event));
|
||||
} else if (touchCount == 2) {
|
||||
state = _GestureState.ScaleStart;
|
||||
} else {
|
||||
state = _GestureState.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
void initScaleAndRotate() {
|
||||
initialScaleDistance = (touches[0].currentOffset - touches[1].currentOffset).distance;
|
||||
}
|
||||
|
||||
void onPointerMove(PointerMoveEvent event) {
|
||||
final touch = touches.firstWhere((touch) => touch.id == event.pointer);
|
||||
touch.currentOffset = event.localPosition;
|
||||
cleanupDoubleTimer();
|
||||
|
||||
switch (state) {
|
||||
case _GestureState.LongPress:
|
||||
if (widget.bypassMoveEventAfterLongPress) {
|
||||
widget.onLongPressMove?.call(MoveEvent(event.localPosition, event.position, event.pointer,
|
||||
delta: event.delta, localDelta: event.localDelta));
|
||||
} else {
|
||||
switch2MoveStartState(touch, event);
|
||||
}
|
||||
break;
|
||||
case _GestureState.PointerDown:
|
||||
switch2MoveStartState(touch, event);
|
||||
break;
|
||||
case _GestureState.MoveStart:
|
||||
widget.onMoveUpdate?.call(MoveEvent(event.localPosition, event.position, event.pointer,
|
||||
delta: event.delta, localDelta: event.localDelta));
|
||||
break;
|
||||
case _GestureState.ScaleStart:
|
||||
touch.startOffset = touch.currentOffset;
|
||||
state = _GestureState.Scalling;
|
||||
initScaleAndRotate();
|
||||
if (widget.onScaleStart != null) {
|
||||
final centerOffset = (touches[0].currentOffset + touches[1].currentOffset) / 2;
|
||||
widget.onScaleStart!(centerOffset);
|
||||
}
|
||||
break;
|
||||
case _GestureState.Scalling:
|
||||
if (widget.onScaleUpdate != null) {
|
||||
var rotation = angleBetweenLines(touches[0], touches[1]);
|
||||
final newDistance = (touches[0].currentOffset - touches[1].currentOffset).distance;
|
||||
final centerOffset = (touches[0].currentOffset + touches[1].currentOffset) / 2;
|
||||
|
||||
widget.onScaleUpdate!(ScaleEvent(centerOffset, newDistance / initialScaleDistance, rotation));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
touch.startOffset = touch.currentOffset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void switch2MoveStartState(_Touch touch, PointerMoveEvent event) {
|
||||
state = _GestureState.MoveStart;
|
||||
touch.startOffset = event.localPosition;
|
||||
widget.onMoveStart?.call(MoveEvent(event.localPosition, event.localPosition, event.pointer));
|
||||
}
|
||||
|
||||
double angleBetweenLines(_Touch f, _Touch s) {
|
||||
double angle1 = math.atan2(f.startOffset.dy - s.startOffset.dy, f.startOffset.dx - s.startOffset.dx);
|
||||
double angle2 = math.atan2(f.currentOffset.dy - s.currentOffset.dy, f.currentOffset.dx - s.currentOffset.dx);
|
||||
|
||||
double angle = vector.degrees(angle1 - angle2) % 360;
|
||||
if (angle < -180.0) angle += 360.0;
|
||||
if (angle > 180.0) angle -= 360.0;
|
||||
return vector.radians(angle);
|
||||
}
|
||||
|
||||
void onPointerUp(PointerEvent event) {
|
||||
touches.removeWhere((touch) => touch.id == event.pointer);
|
||||
|
||||
if (state == _GestureState.PointerDown) {
|
||||
if (!widget.bypassTapEventOnDoubleTap || widget.onDoubleTap == null) {
|
||||
callOnTap(TapEvent.from(event));
|
||||
}
|
||||
if (widget.onDoubleTap != null) {
|
||||
final tapEvent = TapEvent.from(event);
|
||||
if (doubleTapTimer == null) {
|
||||
startDoubleTapTimer(tapEvent);
|
||||
} else {
|
||||
cleanupTimer();
|
||||
if ((event.localPosition - lastTouchUpPos).distanceSquared < 200) {
|
||||
widget.onDoubleTap!(tapEvent);
|
||||
} else {
|
||||
startDoubleTapTimer(tapEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (state == _GestureState.ScaleStart || state == _GestureState.Scalling) {
|
||||
state = _GestureState.Unknown;
|
||||
widget.onScaleEnd?.call();
|
||||
} else if (state == _GestureState.MoveStart) {
|
||||
state = _GestureState.Unknown;
|
||||
widget.onMoveEnd?.call(MoveEvent(event.localPosition, event.position, event.pointer));
|
||||
} else if (state == _GestureState.LongPress) {
|
||||
widget.onLongPressEnd?.call();
|
||||
state = _GestureState.Unknown;
|
||||
} else if (state == _GestureState.Unknown && touchCount == 2) {
|
||||
state = _GestureState.ScaleStart;
|
||||
} else {
|
||||
state = _GestureState.Unknown;
|
||||
}
|
||||
|
||||
lastTouchUpPos = event.localPosition;
|
||||
}
|
||||
|
||||
void startLongPressTimer(TapEvent event) {
|
||||
if (widget.onLongPress != null) {
|
||||
if (longPressTimer != null) {
|
||||
longPressTimer!.cancel();
|
||||
longPressTimer = null;
|
||||
}
|
||||
longPressTimer = Timer(Duration(milliseconds: widget.longPressTimeConsider), () {
|
||||
if (touchCount == 1 && touches[0].id == event.pointer && inLongPressRange(touches[0])) {
|
||||
state = _GestureState.LongPress;
|
||||
widget.onLongPress!(event);
|
||||
cleanupTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool inLongPressRange(_Touch touch) {
|
||||
return (touch.currentOffset - touch.startOffset).distanceSquared < widget.longPressMaximumRangeAllowed;
|
||||
}
|
||||
|
||||
void startDoubleTapTimer(TapEvent event) {
|
||||
doubleTapTimer = Timer(Duration(milliseconds: widget.doubleTapTimeConsider), () {
|
||||
state = _GestureState.Unknown;
|
||||
cleanupTimer();
|
||||
if (widget.bypassTapEventOnDoubleTap) {
|
||||
callOnTap(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void cleanupTimer() {
|
||||
cleanupDoubleTimer();
|
||||
if (longPressTimer != null) {
|
||||
longPressTimer!.cancel();
|
||||
longPressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void cleanupDoubleTimer() {
|
||||
if (doubleTapTimer != null) {
|
||||
doubleTapTimer!.cancel();
|
||||
doubleTapTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void callOnTap(TapEvent event) {
|
||||
if (widget.onTap != null) {
|
||||
widget.onTap!(event);
|
||||
}
|
||||
}
|
||||
|
||||
get touchCount => touches.length;
|
||||
}
|
||||
|
||||
class _Touch {
|
||||
int id;
|
||||
Offset startOffset;
|
||||
late Offset currentOffset;
|
||||
|
||||
_Touch(this.id, this.startOffset) {
|
||||
this.currentOffset = startOffset;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class MoveEvent extends TapEvent {
|
||||
final Offset localDelta;
|
||||
|
||||
final Offset delta;
|
||||
|
||||
const MoveEvent(
|
||||
Offset localPos,
|
||||
Offset position,
|
||||
int pointer, {
|
||||
this.localDelta = const Offset(0, 0),
|
||||
this.delta = const Offset(0, 0),
|
||||
}) : super(localPos, position, pointer);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class TapEvent {
|
||||
final int pointer;
|
||||
|
||||
final Offset localPos;
|
||||
|
||||
final Offset position;
|
||||
|
||||
const TapEvent(this.localPos, this.position, this.pointer);
|
||||
|
||||
static from(PointerEvent event) {
|
||||
return TapEvent(event.localPosition, event.position, event.pointer);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ScaleEvent {
|
||||
final Offset focalPoint;
|
||||
|
||||
final double scale;
|
||||
|
||||
final double rotationAngle;
|
||||
|
||||
const ScaleEvent(this.focalPoint, this.scale, this.rotationAngle);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ScrollEvent {
|
||||
final int pointer;
|
||||
|
||||
final Offset localPos;
|
||||
|
||||
final Offset position;
|
||||
|
||||
final Offset scrollDelta;
|
||||
|
||||
const ScrollEvent(this.pointer, this.localPos, this.position, this.scrollDelta);
|
||||
}
|
||||
|
||||
typedef ScaleEventListener = void Function(ScaleEvent event);
|
||||
|
||||
typedef TapEventListener = void Function(TapEvent event);
|
||||
|
||||
typedef MoveEventListener = void Function(MoveEvent event);
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Copyright (c) 2015-2019 StoneHui
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
library gesture_zoom_box;
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef DoubleCallback = void Function(double v);
|
||||
|
||||
class GestureZoomBox extends StatefulWidget {
|
||||
final double maxScale;
|
||||
final double doubleTapScale;
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final DoubleCallback? onScaleListener;
|
||||
final Duration duration;
|
||||
|
||||
const GestureZoomBox(
|
||||
{Key? key,
|
||||
this.maxScale = 5.0,
|
||||
this.doubleTapScale = 2.0,
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
this.onScaleListener})
|
||||
: assert(maxScale >= 1.0),
|
||||
assert(doubleTapScale >= 1.0 && doubleTapScale <= maxScale),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _GestureZoomBoxState();
|
||||
}
|
||||
}
|
||||
|
||||
class _GestureZoomBoxState extends State<GestureZoomBox> with TickerProviderStateMixin {
|
||||
AnimationController? _scaleAnimController;
|
||||
|
||||
AnimationController? _offsetAnimController;
|
||||
|
||||
ScaleUpdateDetails? _firstScaleUpdateDetails;
|
||||
|
||||
ScaleUpdateDetails? _latestScaleUpdateDetails;
|
||||
|
||||
double _scale = 1.0;
|
||||
|
||||
Offset _offset = Offset.zero;
|
||||
|
||||
Offset? _doubleTapPosition;
|
||||
|
||||
bool _isScaling = false;
|
||||
bool _isDragging = false;
|
||||
|
||||
double _maxDragOver = 100;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.identity()
|
||||
..translate(_offset.dx, _offset.dy)
|
||||
..scale(_scale, _scale),
|
||||
child: Listener(
|
||||
onPointerUp: _onPointerUp,
|
||||
child: GestureDetector(
|
||||
onTap: widget.onPressed,
|
||||
onDoubleTap: _onDoubleTap,
|
||||
onScaleStart: _onScaleStart,
|
||||
onScaleUpdate: _onScaleUpdate,
|
||||
onScaleEnd: _onScaleEnd,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scaleAnimController?.dispose();
|
||||
_offsetAnimController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_onPointerUp(PointerUpEvent event) {
|
||||
_doubleTapPosition = event.localPosition;
|
||||
}
|
||||
|
||||
_onDoubleTap() {
|
||||
double targetScale = _scale == 1.0 ? widget.doubleTapScale : 1.0;
|
||||
_animationScale(targetScale);
|
||||
if (targetScale == 1.0) {
|
||||
_animationOffset(Offset.zero);
|
||||
}
|
||||
}
|
||||
|
||||
_onScaleStart(ScaleStartDetails details) {
|
||||
_scaleAnimController?.stop();
|
||||
_offsetAnimController?.stop();
|
||||
_isScaling = false;
|
||||
_isDragging = false;
|
||||
_firstScaleUpdateDetails = null;
|
||||
_latestScaleUpdateDetails = null;
|
||||
}
|
||||
|
||||
_onScaleUpdate(ScaleUpdateDetails details) {
|
||||
if (_firstScaleUpdateDetails == null) {
|
||||
_firstScaleUpdateDetails = details;
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
if (details.scale != 1.0) {
|
||||
_scaling(details);
|
||||
} else {
|
||||
_dragging(details);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_scaling(ScaleUpdateDetails details) {
|
||||
if (_isDragging) {
|
||||
return;
|
||||
}
|
||||
final latestScaleUpdateDetails = _latestScaleUpdateDetails, size = context.size;
|
||||
_isScaling = true;
|
||||
if (latestScaleUpdateDetails == null || size == null) {
|
||||
_latestScaleUpdateDetails = details;
|
||||
return;
|
||||
}
|
||||
|
||||
double scaleIncrement = details.scale - latestScaleUpdateDetails.scale;
|
||||
if (details.scale < 1.0 && _scale > 1.0) {
|
||||
scaleIncrement *= _scale;
|
||||
}
|
||||
if (_scale < 1.0 && scaleIncrement < 0) {
|
||||
scaleIncrement *= (_scale - 0.5);
|
||||
} else if (_scale > widget.maxScale && scaleIncrement > 0) {
|
||||
scaleIncrement *= (2.0 - (_scale - widget.maxScale));
|
||||
}
|
||||
_scale = max(_scale + scaleIncrement, 0.0);
|
||||
|
||||
double scaleOffsetX = size.width * (_scale - 1.0) / 2;
|
||||
double scaleOffsetY = size.height * (_scale - 1.0) / 2;
|
||||
|
||||
double scalePointDX = (details.localFocalPoint.dx + scaleOffsetX - _offset.dx) / _scale;
|
||||
double scalePointDY = (details.localFocalPoint.dy + scaleOffsetY - _offset.dy) / _scale;
|
||||
|
||||
_offset += Offset(
|
||||
(size.width / 2 - scalePointDX) * scaleIncrement,
|
||||
(size.height / 2 - scalePointDY) * scaleIncrement,
|
||||
);
|
||||
|
||||
_latestScaleUpdateDetails = details;
|
||||
}
|
||||
|
||||
_dragging(ScaleUpdateDetails details) {
|
||||
if (_isScaling) {
|
||||
return;
|
||||
}
|
||||
final latestScaleUpdateDetails = _latestScaleUpdateDetails, size = context.size;
|
||||
_isDragging = true;
|
||||
if (latestScaleUpdateDetails == null || size == null) {
|
||||
_latestScaleUpdateDetails = details;
|
||||
return;
|
||||
}
|
||||
|
||||
double offsetXIncrement = (details.localFocalPoint.dx - latestScaleUpdateDetails.localFocalPoint.dx) * _scale;
|
||||
double offsetYIncrement = (details.localFocalPoint.dy - latestScaleUpdateDetails.localFocalPoint.dy) * _scale;
|
||||
|
||||
double scaleOffsetX = (size.width * _scale - MediaQuery.of(context).size.width) / 2;
|
||||
if (scaleOffsetX <= 0) {
|
||||
offsetXIncrement = 0;
|
||||
} else if (_offset.dx > scaleOffsetX) {
|
||||
offsetXIncrement *= (_maxDragOver - (_offset.dx - scaleOffsetX)) / _maxDragOver;
|
||||
} else if (_offset.dx < -scaleOffsetX) {
|
||||
offsetXIncrement *= (_maxDragOver - (-scaleOffsetX - _offset.dx)) / _maxDragOver;
|
||||
}
|
||||
|
||||
double scaleOffsetY = (size.height * _scale - MediaQuery.of(context).size.height) / 2;
|
||||
if (scaleOffsetY <= 0) {
|
||||
offsetYIncrement = 0;
|
||||
} else if (_offset.dy > scaleOffsetY) {
|
||||
offsetYIncrement *= (_maxDragOver - (_offset.dy - scaleOffsetY)) / _maxDragOver;
|
||||
} else if (_offset.dy < -scaleOffsetY) {
|
||||
offsetYIncrement *= (_maxDragOver - (-scaleOffsetY - _offset.dy)) / _maxDragOver;
|
||||
}
|
||||
|
||||
_offset += Offset(offsetXIncrement, offsetYIncrement);
|
||||
|
||||
_latestScaleUpdateDetails = details;
|
||||
}
|
||||
|
||||
_onScaleEnd(ScaleEndDetails details) {
|
||||
final size = context.size;
|
||||
|
||||
if (size == null) {
|
||||
return;
|
||||
}
|
||||
widget.onScaleListener?.call(_scale);
|
||||
|
||||
if (_scale < 1.0) {
|
||||
_animationScale(1.0);
|
||||
} else if (_scale > widget.maxScale) {
|
||||
_animationScale(widget.maxScale);
|
||||
}
|
||||
if (_scale <= 1.0) {
|
||||
_animationOffset(Offset.zero);
|
||||
} else if (_isDragging) {
|
||||
double realScale = _scale > widget.maxScale ? widget.maxScale : _scale;
|
||||
double targetOffsetX = _offset.dx, targetOffsetY = _offset.dy;
|
||||
|
||||
double scaleOffsetX = (size.width * realScale - MediaQuery.of(context).size.width) / 2;
|
||||
if (scaleOffsetX <= 0) {
|
||||
targetOffsetX = 0;
|
||||
} else if (_offset.dx > scaleOffsetX) {
|
||||
targetOffsetX = scaleOffsetX;
|
||||
} else if (_offset.dx < -scaleOffsetX) {
|
||||
targetOffsetX = -scaleOffsetX;
|
||||
}
|
||||
|
||||
double scaleOffsetY = (size.height * realScale - MediaQuery.of(context).size.height) / 2;
|
||||
if (scaleOffsetY < 0) {
|
||||
targetOffsetY = 0;
|
||||
} else if (_offset.dy > scaleOffsetY) {
|
||||
targetOffsetY = scaleOffsetY;
|
||||
} else if (_offset.dy < -scaleOffsetY) {
|
||||
targetOffsetY = -scaleOffsetY;
|
||||
}
|
||||
if (_offset.dx != targetOffsetX || _offset.dy != targetOffsetY) {
|
||||
_animationOffset(Offset(targetOffsetX, targetOffsetY));
|
||||
} else {
|
||||
double duration = (widget.duration.inSeconds + widget.duration.inMilliseconds / 1000);
|
||||
Offset targetOffset = _offset + details.velocity.pixelsPerSecond * duration;
|
||||
targetOffsetX = targetOffset.dx;
|
||||
if (targetOffsetX > scaleOffsetX) {
|
||||
targetOffsetX = scaleOffsetX;
|
||||
} else if (targetOffsetX < -scaleOffsetX) {
|
||||
targetOffsetX = -scaleOffsetX;
|
||||
}
|
||||
|
||||
targetOffsetY = targetOffset.dy;
|
||||
if (targetOffsetY > scaleOffsetY) {
|
||||
targetOffsetY = scaleOffsetY;
|
||||
} else if (targetOffsetY < -scaleOffsetY) {
|
||||
targetOffsetY = -scaleOffsetY;
|
||||
}
|
||||
|
||||
_animationOffset(Offset(targetOffsetX, targetOffsetY));
|
||||
}
|
||||
}
|
||||
|
||||
_isScaling = false;
|
||||
_isDragging = false;
|
||||
_latestScaleUpdateDetails = null;
|
||||
}
|
||||
|
||||
_animationScale(double targetScale) {
|
||||
_scaleAnimController?.dispose();
|
||||
final scaleAnimController = _scaleAnimController = AnimationController(vsync: this, duration: widget.duration);
|
||||
Animation anim = Tween<double>(begin: _scale, end: targetScale).animate(scaleAnimController);
|
||||
anim.addListener(() {
|
||||
setState(() {
|
||||
_scaling(ScaleUpdateDetails(
|
||||
focalPoint: _doubleTapPosition!,
|
||||
localFocalPoint: _doubleTapPosition!,
|
||||
scale: anim.value,
|
||||
horizontalScale: anim.value,
|
||||
verticalScale: anim.value,
|
||||
));
|
||||
});
|
||||
});
|
||||
anim.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_onScaleEnd(ScaleEndDetails());
|
||||
}
|
||||
});
|
||||
scaleAnimController.forward();
|
||||
}
|
||||
|
||||
_animationOffset(Offset targetOffset) {
|
||||
_offsetAnimController?.dispose();
|
||||
final offsetAnimController = _offsetAnimController = AnimationController(vsync: this, duration: widget.duration);
|
||||
Animation anim = offsetAnimController.drive(Tween<Offset>(begin: _offset, end: targetOffset));
|
||||
anim.addListener(() {
|
||||
setState(() {
|
||||
_offset = anim.value;
|
||||
});
|
||||
});
|
||||
offsetAnimController.fling();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:openim_common/openim_common.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 H5Container extends StatefulWidget {
|
||||
const H5Container({super.key, required this.url, this.title});
|
||||
|
||||
final String url;
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
State<H5Container> createState() => _H5ContainerState();
|
||||
}
|
||||
|
||||
class _H5ContainerState extends State<H5Container> {
|
||||
late final WebViewController _controller;
|
||||
|
||||
double progress = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Logger.print('H5Container: ${widget.url}');
|
||||
|
||||
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(
|
||||
'Toaster',
|
||||
onMessageReceived: (JavaScriptMessage message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message.message)),
|
||||
);
|
||||
},
|
||||
)
|
||||
..loadRequest(Uri.parse('https://openim.io'));
|
||||
|
||||
if (!Platform.isMacOS) {
|
||||
controller.setBackgroundColor(const Color(0x80000000));
|
||||
}
|
||||
|
||||
if (controller.platform is AndroidWebViewController) {
|
||||
AndroidWebViewController.enableDebugging(true);
|
||||
(controller.platform as AndroidWebViewController).setMediaPlaybackRequiresUserGesture(false);
|
||||
}
|
||||
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Logger.print('H5Container: ${widget.url}');
|
||||
return Scaffold(
|
||||
appBar: widget.title != null ? TitleBar.back(title: widget.title) : null,
|
||||
body: Stack(
|
||||
children: [
|
||||
WebViewWidget(controller: _controller),
|
||||
progress < 1.0
|
||||
? LinearProgressIndicator(
|
||||
value: progress,
|
||||
color: Colors.blue,
|
||||
)
|
||||
: const SizedBox(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
import 'dart:async';
|
||||
|
||||
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';
|
||||
|
||||
enum InputBoxType {
|
||||
phone,
|
||||
account,
|
||||
password,
|
||||
verificationCode,
|
||||
invitationCode,
|
||||
}
|
||||
|
||||
class InputBox extends StatefulWidget {
|
||||
const InputBox.phone({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.code,
|
||||
this.onAreaCode,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.labelStyle,
|
||||
this.textStyle,
|
||||
this.codeStyle,
|
||||
this.hintStyle,
|
||||
this.formatHintStyle,
|
||||
this.hintText,
|
||||
this.formatHintText,
|
||||
this.margin,
|
||||
this.inputFormatters,
|
||||
this.keyBoardType,
|
||||
}) : obscureText = false,
|
||||
type = InputBoxType.phone,
|
||||
arrowColor = null,
|
||||
clearBtnColor = null,
|
||||
onSendVerificationCode = null;
|
||||
|
||||
InputBox.account({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.code,
|
||||
this.onAreaCode,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.labelStyle,
|
||||
this.textStyle,
|
||||
this.codeStyle,
|
||||
this.hintStyle,
|
||||
this.formatHintStyle,
|
||||
this.hintText,
|
||||
this.formatHintText,
|
||||
this.margin,
|
||||
this.inputFormatters,
|
||||
this.keyBoardType,
|
||||
}) : obscureText = false,
|
||||
type = InputBoxType.account,
|
||||
arrowColor = null,
|
||||
clearBtnColor = null,
|
||||
onSendVerificationCode = null;
|
||||
|
||||
const InputBox.password({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.labelStyle,
|
||||
this.textStyle,
|
||||
this.hintStyle,
|
||||
this.formatHintStyle,
|
||||
this.hintText,
|
||||
this.formatHintText,
|
||||
this.margin,
|
||||
this.inputFormatters,
|
||||
this.keyBoardType,
|
||||
}) : obscureText = true,
|
||||
type = InputBoxType.password,
|
||||
codeStyle = null,
|
||||
code = '',
|
||||
arrowColor = null,
|
||||
clearBtnColor = null,
|
||||
onSendVerificationCode = null,
|
||||
onAreaCode = null;
|
||||
|
||||
const InputBox.verificationCode({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.onSendVerificationCode,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.labelStyle,
|
||||
this.textStyle,
|
||||
this.hintStyle,
|
||||
this.formatHintStyle,
|
||||
this.hintText,
|
||||
this.formatHintText,
|
||||
this.margin,
|
||||
this.inputFormatters,
|
||||
this.keyBoardType,
|
||||
}) : obscureText = false,
|
||||
type = InputBoxType.verificationCode,
|
||||
code = '',
|
||||
codeStyle = null,
|
||||
onAreaCode = null,
|
||||
arrowColor = null,
|
||||
clearBtnColor = null;
|
||||
|
||||
const InputBox.invitationCode({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.labelStyle,
|
||||
this.textStyle,
|
||||
this.formatHintStyle,
|
||||
this.hintStyle,
|
||||
this.hintText,
|
||||
this.formatHintText,
|
||||
this.margin,
|
||||
this.inputFormatters,
|
||||
this.keyBoardType,
|
||||
}) : obscureText = false,
|
||||
type = InputBoxType.invitationCode,
|
||||
code = '',
|
||||
codeStyle = null,
|
||||
onAreaCode = null,
|
||||
onSendVerificationCode = null,
|
||||
arrowColor = null,
|
||||
clearBtnColor = null;
|
||||
|
||||
const InputBox({
|
||||
Key? key,
|
||||
required this.label,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.labelStyle,
|
||||
this.textStyle,
|
||||
this.hintStyle,
|
||||
this.codeStyle,
|
||||
this.formatHintStyle,
|
||||
this.code = '+86',
|
||||
this.hintText,
|
||||
this.formatHintText,
|
||||
this.arrowColor,
|
||||
this.clearBtnColor,
|
||||
this.obscureText = false,
|
||||
this.type = InputBoxType.account,
|
||||
this.onAreaCode,
|
||||
this.onSendVerificationCode,
|
||||
this.margin,
|
||||
this.inputFormatters,
|
||||
this.keyBoardType,
|
||||
}) : super(key: key);
|
||||
final TextStyle? labelStyle;
|
||||
final TextStyle? textStyle;
|
||||
final TextStyle? hintStyle;
|
||||
final TextStyle? codeStyle;
|
||||
final TextStyle? formatHintStyle;
|
||||
final String code;
|
||||
final String label;
|
||||
final String? hintText;
|
||||
final String? formatHintText;
|
||||
final Color? arrowColor;
|
||||
final Color? clearBtnColor;
|
||||
final bool obscureText;
|
||||
final TextEditingController? controller;
|
||||
final FocusNode? focusNode;
|
||||
final InputBoxType type;
|
||||
final Function()? onAreaCode;
|
||||
final Future<bool> Function()? onSendVerificationCode;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final TextInputType? keyBoardType;
|
||||
|
||||
@override
|
||||
State<InputBox> createState() => _InputBoxState();
|
||||
}
|
||||
|
||||
class _InputBoxState extends State<InputBox> {
|
||||
late bool _obscureText;
|
||||
bool _showClearBtn = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_obscureText = widget.obscureText;
|
||||
widget.controller?.addListener(_onChanged);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _onChanged() {
|
||||
setState(() {
|
||||
_showClearBtn = widget.controller!.text.isNotEmpty;
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleEye() {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: widget.margin,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.label,
|
||||
style: widget.labelStyle ?? Styles.ts_8E9AB0_12sp,
|
||||
),
|
||||
6.verticalSpace,
|
||||
Container(
|
||||
height: 42.h,
|
||||
padding: EdgeInsets.only(left: 12.w, right: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Styles.c_E8EAEF, width: 1),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.type == InputBoxType.phone || widget.onAreaCode != null) _areaCodeView,
|
||||
_textField,
|
||||
_clearBtn,
|
||||
_eyeBtn,
|
||||
if (widget.type == InputBoxType.verificationCode)
|
||||
VerifyCodedButton(
|
||||
onTapCallback: widget.onSendVerificationCode,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (null != widget.formatHintText)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5.h),
|
||||
child: widget.formatHintText!.toText..style = (widget.formatHintStyle ?? Styles.ts_8E9AB0_12sp),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _textField => Expanded(
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
keyboardType: _textInputType,
|
||||
textInputAction: TextInputAction.next,
|
||||
style: widget.textStyle ?? Styles.ts_0C1C33_17sp,
|
||||
autofocus: false,
|
||||
obscureText: _obscureText,
|
||||
focusNode: widget.focusNode,
|
||||
inputFormatters: [
|
||||
if (widget.type == InputBoxType.phone || widget.type == InputBoxType.verificationCode)
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
|
||||
if (null != widget.inputFormatters) ...widget.inputFormatters!,
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
hintStyle: widget.hintStyle ?? Styles.ts_8E9AB0_17sp,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _areaCodeView => GestureDetector(
|
||||
onTap: widget.onAreaCode,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.code,
|
||||
style: widget.codeStyle ?? Styles.ts_0C1C33_17sp,
|
||||
),
|
||||
8.horizontalSpace,
|
||||
ImageRes.downArrow.toImage
|
||||
..width = 8.49.w
|
||||
..height = 8.49.h,
|
||||
Container(
|
||||
width: 1.w,
|
||||
height: 26.h,
|
||||
margin: EdgeInsets.symmetric(horizontal: 14.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_E8EAEF,
|
||||
borderRadius: BorderRadius.circular(2.r),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _clearBtn => Visibility(
|
||||
visible: _showClearBtn,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
widget.controller?.clear();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: ImageRes.clearText.toImage
|
||||
..width = 24.w
|
||||
..height = 24.h,
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _eyeBtn => Visibility(
|
||||
visible: widget.type == InputBoxType.password,
|
||||
child: GestureDetector(
|
||||
onTap: _toggleEye,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: (_obscureText ? ImageRes.eyeClose.toImage : ImageRes.eyeOpen.toImage)
|
||||
..width = 24.w
|
||||
..height = 24.h,
|
||||
),
|
||||
);
|
||||
|
||||
TextInputType? get _textInputType {
|
||||
if (widget.keyBoardType != null) {
|
||||
return widget.keyBoardType;
|
||||
}
|
||||
TextInputType? keyboardType;
|
||||
switch (widget.type) {
|
||||
case InputBoxType.phone:
|
||||
keyboardType = TextInputType.phone;
|
||||
break;
|
||||
case InputBoxType.account:
|
||||
keyboardType = TextInputType.text;
|
||||
break;
|
||||
case InputBoxType.password:
|
||||
keyboardType = TextInputType.text;
|
||||
break;
|
||||
case InputBoxType.verificationCode:
|
||||
keyboardType = TextInputType.number;
|
||||
break;
|
||||
case InputBoxType.invitationCode:
|
||||
keyboardType = TextInputType.text;
|
||||
break;
|
||||
}
|
||||
return keyboardType;
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyCodedButton extends StatefulWidget {
|
||||
final int seconds;
|
||||
|
||||
final Future<bool> Function()? onTapCallback;
|
||||
|
||||
const VerifyCodedButton({
|
||||
Key? key,
|
||||
this.seconds = 60,
|
||||
required this.onTapCallback,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<VerifyCodedButton> createState() => _VerifyCodedButtonState();
|
||||
}
|
||||
|
||||
class _VerifyCodedButtonState extends State<VerifyCodedButton> {
|
||||
Timer? _timer;
|
||||
late int _seconds;
|
||||
bool _firstTime = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_seconds = widget.seconds;
|
||||
}
|
||||
|
||||
void _start() {
|
||||
_firstTime = false;
|
||||
_timer = Timer.periodic(1.seconds, (timer) {
|
||||
if (!mounted) return;
|
||||
if (_seconds == 0) {
|
||||
_cancel();
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
_seconds--;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
void _cancel() {
|
||||
if (null != _timer) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
if (_seconds != widget.seconds) {
|
||||
_seconds = widget.seconds;
|
||||
}
|
||||
_cancel();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _restart() {
|
||||
_reset();
|
||||
_start();
|
||||
}
|
||||
|
||||
bool get _isEnabled => _seconds == 0 || _firstTime;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => (_isEnabled ? StrRes.sendVerificationCode : '${_seconds}S').toText
|
||||
..style = Styles.ts_0089FF_17sp
|
||||
..onTap = () {
|
||||
if (_isEnabled) {
|
||||
widget.onTapCallback?.call().then((start) {
|
||||
if (start) _restart();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class LoadingView {
|
||||
static final LoadingView singleton = LoadingView._();
|
||||
|
||||
factory LoadingView() => singleton;
|
||||
|
||||
LoadingView._();
|
||||
|
||||
OverlayState? _overlayState;
|
||||
OverlayEntry? _overlayEntry;
|
||||
bool _isVisible = false;
|
||||
|
||||
OverlayState? _progressOverlayState;
|
||||
OverlayEntry? _progressOverlayEntry;
|
||||
bool isProgressVisible = false;
|
||||
|
||||
Future<T> wrap<T>({
|
||||
required Future<T> Function() asyncFunction,
|
||||
bool showing = true,
|
||||
}) async {
|
||||
await Future.delayed(1.milliseconds);
|
||||
if (showing) show();
|
||||
T data;
|
||||
try {
|
||||
data = await asyncFunction();
|
||||
} on Exception catch (_) {
|
||||
rethrow;
|
||||
} finally {
|
||||
dismiss();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void show() async {
|
||||
if (_isVisible) return;
|
||||
_overlayState = Overlay.of(Get.overlayContext!);
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) => Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: SpinKitCircle(color: Styles.c_0089FF),
|
||||
),
|
||||
),
|
||||
);
|
||||
_isVisible = true;
|
||||
_overlayState?.insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
dismiss() async {
|
||||
if (!_isVisible && !isProgressVisible) return;
|
||||
_overlayEntry?.remove();
|
||||
_progressOverlayEntry?.remove();
|
||||
_isVisible = false;
|
||||
isProgressVisible = false;
|
||||
}
|
||||
|
||||
void progress(Stream<double> stream) async {
|
||||
_progressOverlayState = Overlay.of(Get.overlayContext!);
|
||||
_progressOverlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) => GestureDetector(
|
||||
onTap: dismiss,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
color: const Color.fromARGB(0, 37, 33, 33),
|
||||
child: Center(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Styles.c_0C1C33,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CupertinoActivityIndicator(
|
||||
color: Colors.white,
|
||||
radius: 20,
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: stream,
|
||||
builder: (BuildContext context, AsyncSnapshot<double> snapshot) {
|
||||
if (!snapshot.hasData) return Container();
|
||||
final progress = snapshot.data ?? 0.0;
|
||||
return Text('${(progress * 100).toStringAsFixed(1)}%',
|
||||
style: const TextStyle(color: Colors.white));
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
isProgressVisible = true;
|
||||
_progressOverlayState?.insert(_progressOverlayEntry!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:map_launcher/map_launcher.dart' as ml;
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
class MapView extends StatelessWidget {
|
||||
const MapView({
|
||||
Key? key,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.address1,
|
||||
required this.address2,
|
||||
}) : super(key: key);
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final String address1;
|
||||
final String address2;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: TitleBar.back(title: StrRes.location),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCenter: LatLng(latitude, longitude),
|
||||
initialZoom: 15.0,
|
||||
maxZoom: 18.0,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
|
||||
userAgentPackageName: '',
|
||||
),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
point: LatLng(latitude, longitude),
|
||||
child: Icon(
|
||||
Icons.location_on_sharp,
|
||||
color: Styles.c_FF381F,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 22.w, vertical: 10.h),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
address1.toText..style = Styles.ts_0C1C33_17sp_semibold,
|
||||
8.verticalSpace,
|
||||
address2.toText..style = Styles.ts_8E9AB0_14sp,
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: _openMapSheet,
|
||||
child: Container(
|
||||
width: 35.w,
|
||||
height: 35.w,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.map, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_openMapSheet() async {
|
||||
final availableMaps = await ml.MapLauncher.installedMaps;
|
||||
Get.bottomSheet(
|
||||
BottomSheetView(
|
||||
items: availableMaps
|
||||
.map((e) => SheetItem(
|
||||
label: _mapLabel(e),
|
||||
onTap: () async {
|
||||
_launcherMap(e);
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _mapLabel(ml.AvailableMap map) {
|
||||
if (map.mapType == ml.MapType.google) {
|
||||
return StrRes.googleMap;
|
||||
} else if (map.mapType == ml.MapType.apple) {
|
||||
return StrRes.appleMap;
|
||||
} else if (map.mapType == ml.MapType.baidu) {
|
||||
return StrRes.baiduMap;
|
||||
} else if (map.mapType == ml.MapType.amap) {
|
||||
return StrRes.amapMap;
|
||||
} else if (map.mapType == ml.MapType.tencent) {
|
||||
return StrRes.tencentMap;
|
||||
}
|
||||
return map.mapName;
|
||||
}
|
||||
|
||||
_launcherMap(ml.AvailableMap map) async {
|
||||
await ml.MapLauncher.showMarker(
|
||||
mapType: map.mapType,
|
||||
coords: ml.Coords(latitude, longitude),
|
||||
title: address1,
|
||||
description: address2,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:chewie/src/center_play_button.dart';
|
||||
import 'package:chewie/src/chewie_player.dart';
|
||||
import 'package:chewie/src/chewie_progress_colors.dart';
|
||||
import 'package:chewie/src/helpers/utils.dart';
|
||||
import 'package:chewie/src/material/material_progress_bar.dart';
|
||||
import 'package:chewie/src/material/widgets/options_dialog.dart';
|
||||
import 'package:chewie/src/material/widgets/playback_speed_dialog.dart';
|
||||
import 'package:chewie/src/models/option_item.dart';
|
||||
import 'package:chewie/src/models/subtitle_model.dart';
|
||||
import 'package:chewie/src/notifiers/index.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class CustomMaterialControls extends StatefulWidget {
|
||||
const CustomMaterialControls({
|
||||
this.showPlayButton = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool showPlayButton;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _MaterialControlsState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MaterialControlsState extends State<CustomMaterialControls> with SingleTickerProviderStateMixin {
|
||||
late PlayerNotifier notifier;
|
||||
late VideoPlayerValue _latestValue;
|
||||
double? _latestVolume;
|
||||
Timer? _hideTimer;
|
||||
Timer? _initTimer;
|
||||
late var _subtitlesPosition = Duration.zero;
|
||||
bool _subtitleOn = false;
|
||||
Timer? _showAfterExpandCollapseTimer;
|
||||
bool _dragging = false;
|
||||
bool _displayTapped = false;
|
||||
Timer? _bufferingDisplayTimer;
|
||||
bool _displayBufferingIndicator = false;
|
||||
|
||||
final barHeight = 48.0 * 1.5;
|
||||
final marginSize = 5.0;
|
||||
|
||||
late VideoPlayerController controller;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
ChewieController get chewieController => _chewieController!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notifier = Provider.of<PlayerNotifier>(context, listen: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_latestValue.hasError) {
|
||||
return chewieController.errorBuilder?.call(
|
||||
context,
|
||||
chewieController.videoPlayerController.value.errorDescription!,
|
||||
) ??
|
||||
const Center(
|
||||
child: Icon(
|
||||
Icons.error,
|
||||
color: Colors.white,
|
||||
size: 42,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
onHover: (_) {
|
||||
_cancelAndRestartTimer();
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () => _cancelAndRestartTimer(),
|
||||
child: AbsorbPointer(
|
||||
absorbing: notifier.hideStuff,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_displayBufferingIndicator)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
_buildHitArea(),
|
||||
_buildActionBar(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
if (_subtitleOn)
|
||||
Transform.translate(
|
||||
offset: Offset(
|
||||
0.0,
|
||||
notifier.hideStuff ? barHeight * 0.8 : 0.0,
|
||||
),
|
||||
child: _buildSubtitles(context, chewieController.subtitle!),
|
||||
),
|
||||
_buildBottomBar(context),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispose() {
|
||||
controller.removeListener(_updateState);
|
||||
_hideTimer?.cancel();
|
||||
_initTimer?.cancel();
|
||||
_showAfterExpandCollapseTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final oldController = _chewieController;
|
||||
_chewieController = ChewieController.of(context);
|
||||
controller = chewieController.videoPlayerController;
|
||||
|
||||
if (oldController != chewieController) {
|
||||
_dispose();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
Widget _buildActionBar() {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
width: 1.sw,
|
||||
child: SafeArea(
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87.withOpacity(0.4),
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildSubtitleToggle(),
|
||||
if (chewieController.showOptions) _buildOptionsButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionsButton() {
|
||||
final options = <OptionItem>[
|
||||
OptionItem(
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_onSpeedButtonTap();
|
||||
},
|
||||
iconData: Icons.speed,
|
||||
title: chewieController.optionsTranslation?.playbackSpeedButtonText ?? 'Playback speed',
|
||||
)
|
||||
];
|
||||
|
||||
if (chewieController.additionalOptions != null && chewieController.additionalOptions!(context).isNotEmpty) {
|
||||
options.addAll(chewieController.additionalOptions!(context));
|
||||
}
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
if (chewieController.optionsBuilder != null) {
|
||||
await chewieController.optionsBuilder!(context, options);
|
||||
} else {
|
||||
await showModalBottomSheet<OptionItem>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => OptionsDialog(
|
||||
options: options,
|
||||
cancelButtonText: chewieController.optionsTranslation?.cancelButtonText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
|
||||
if (!_subtitleOn) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition);
|
||||
if (currentSubtitle.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
if (chewieController.subtitleBuilder != null) {
|
||||
return chewieController.subtitleBuilder!(
|
||||
context,
|
||||
currentSubtitle.first!.text,
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(marginSize),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x96000000),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Text(
|
||||
currentSubtitle.first!.text.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AnimatedOpacity _buildBottomBar(
|
||||
BuildContext context,
|
||||
) {
|
||||
final iconColor = Theme.of(context).textTheme.labelLarge!.color;
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: barHeight + (chewieController.isFullScreen ? 10.0 : 0),
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
bottom: !chewieController.isFullScreen ? 10.0 : 0,
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: chewieController.isFullScreen,
|
||||
minimum: chewieController.controlsSafeAreaMinimum,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
if (chewieController.isLive) const Expanded(child: Text('LIVE')) else _buildPosition(iconColor),
|
||||
if (chewieController.allowMuting) _buildMuteButton(controller),
|
||||
const Spacer(),
|
||||
if (chewieController.allowFullScreen) _buildExpandButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: chewieController.isFullScreen ? 15.0 : 0,
|
||||
),
|
||||
if (!chewieController.isLive)
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildProgressBar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildMuteButton(
|
||||
VideoPlayerController controller,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (_latestValue.volume == 0) {
|
||||
controller.setVolume(_latestVolume ?? 0.5);
|
||||
} else {
|
||||
_latestVolume = controller.value.volume;
|
||||
controller.setVolume(0.0);
|
||||
}
|
||||
},
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRect(
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
),
|
||||
child: Icon(
|
||||
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildExpandButton() {
|
||||
return GestureDetector(
|
||||
onTap: _onExpandCollapse,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: barHeight + (chewieController.isFullScreen ? 15.0 : 0),
|
||||
margin: const EdgeInsets.only(right: 12.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
chewieController.isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHitArea() {
|
||||
final bool isFinished = _latestValue.position >= _latestValue.duration;
|
||||
final bool showPlayButton = widget.showPlayButton && !_dragging && !notifier.hideStuff;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (_latestValue.isPlaying) {
|
||||
if (_displayTapped) {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
}
|
||||
} else {
|
||||
_playPause();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: Colors.black54,
|
||||
iconColor: Colors.white,
|
||||
isFinished: isFinished,
|
||||
isPlaying: controller.value.isPlaying,
|
||||
show: showPlayButton,
|
||||
onPressed: _playPause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSpeedButtonTap() async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
final chosenSpeed = await showModalBottomSheet<double>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => PlaybackSpeedDialog(
|
||||
speeds: chewieController.playbackSpeeds,
|
||||
selected: _latestValue.playbackSpeed,
|
||||
),
|
||||
);
|
||||
|
||||
if (chosenSpeed != null) {
|
||||
controller.setPlaybackSpeed(chosenSpeed);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPosition(Color? iconColor) {
|
||||
final position = _latestValue.position;
|
||||
final duration = _latestValue.duration;
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: '${formatDuration(position)} ',
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: '/ ${formatDuration(duration)}',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.white.withOpacity(.75),
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
)
|
||||
],
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitleToggle() {
|
||||
if (chewieController.subtitle?.isEmpty ?? true) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: _onSubtitleTap,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
),
|
||||
child: Icon(
|
||||
_subtitleOn ? Icons.closed_caption : Icons.closed_caption_off_outlined,
|
||||
color: _subtitleOn ? Colors.white : Colors.grey[700],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubtitleTap() {
|
||||
setState(() {
|
||||
_subtitleOn = !_subtitleOn;
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelAndRestartTimer() {
|
||||
_hideTimer?.cancel();
|
||||
_startHideTimer();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
_displayTapped = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
|
||||
controller.addListener(_updateState);
|
||||
|
||||
_updateState();
|
||||
|
||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
if (chewieController.showControlsOnInitialize) {
|
||||
_initTimer = Timer(const Duration(milliseconds: 200), () {
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onExpandCollapse() {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
|
||||
chewieController.toggleFullScreen();
|
||||
_showAfterExpandCollapseTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
_cancelAndRestartTimer();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _playPause() {
|
||||
final isFinished = _latestValue.position >= _latestValue.duration;
|
||||
|
||||
setState(() {
|
||||
if (controller.value.isPlaying) {
|
||||
notifier.hideStuff = false;
|
||||
_hideTimer?.cancel();
|
||||
controller.pause();
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (!controller.value.isInitialized) {
|
||||
controller.initialize().then((_) {
|
||||
controller.play();
|
||||
});
|
||||
} else {
|
||||
if (isFinished) {
|
||||
controller.seekTo(Duration.zero);
|
||||
}
|
||||
controller.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
||||
? ChewieController.defaultHideControlsTimer
|
||||
: chewieController.hideControlsTimer;
|
||||
_hideTimer = Timer(hideControlsTimer, () {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _bufferingTimerTimeout() {
|
||||
_displayBufferingIndicator = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
if (!mounted) return;
|
||||
|
||||
if (chewieController.progressIndicatorDelay != null) {
|
||||
if (controller.value.isBuffering) {
|
||||
_bufferingDisplayTimer ??= Timer(
|
||||
chewieController.progressIndicatorDelay!,
|
||||
_bufferingTimerTimeout,
|
||||
);
|
||||
} else {
|
||||
_bufferingDisplayTimer?.cancel();
|
||||
_bufferingDisplayTimer = null;
|
||||
_displayBufferingIndicator = false;
|
||||
}
|
||||
} else {
|
||||
_displayBufferingIndicator = controller.value.isBuffering;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_latestValue = controller.value;
|
||||
_subtitlesPosition = controller.value.position;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildProgressBar() {
|
||||
return Expanded(
|
||||
child: MaterialVideoProgressBar(
|
||||
controller,
|
||||
onDragStart: () {
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
});
|
||||
|
||||
_hideTimer?.cancel();
|
||||
},
|
||||
onDragEnd: () {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
});
|
||||
|
||||
_startHideTimer();
|
||||
},
|
||||
colors: chewieController.materialProgressColors ??
|
||||
ChewieProgressColors(
|
||||
playedColor: Theme.of(context).colorScheme.secondary,
|
||||
handleColor: Theme.of(context).colorScheme.secondary,
|
||||
bufferedColor: Theme.of(context).colorScheme.background.withOpacity(0.5),
|
||||
backgroundColor: Theme.of(context).disabledColor.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user