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:
编码工程师
2026-08-19 15:24:33 +08:00
co-authored by multica-agent
parent fa8584a73f
commit 8046ec7483
767 changed files with 57658 additions and 0 deletions
@@ -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';
+435
View File
@@ -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": "欢迎使用OpenCorpFlutter版)",
"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": "620位,至少包含数字、字母",
"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,
),
],
),
],
),
);
}
}
@@ -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