feat(mobile-next): 导入官方 openim-flutter-demo 固定基线 3.8.3-patch.3 并完成双构建
- 上游: github.com/OpenIMSDK/openim-flutter-demo tag 3.8.3-patch.3 commit b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f - 并行目录 mobile-next/ 原样导入(含 LICENSE),不覆盖现网 mobile/ - 锁定 Flutter 3.24.5 / Dart 3.5.4 / JDK 17 / Gradle 7.6.3 / AGP 7.3.1 - 实测 Android debug 与 release 构建均成功(见 docs/mobile-next-baseline-import.md) - 本提交可单独回退:git revert 1db1229(重写前) Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user