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,10 @@
import 'package:get/get.dart';
import 'group_list_logic.dart';
class GroupListBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => GroupListLogic());
}
}
@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart';
import 'package:openim/routes/app_navigator.dart';
import 'package:pull_to_refresh_new/pull_to_refresh.dart';
import '../../../core/controller/im_controller.dart';
import '../../../core/im_callback.dart';
import '../../conversation/conversation_logic.dart';
class GroupListLogic extends GetxController {
final imLoic = Get.find<IMController>();
final iCreateRefreshController = RefreshController(initialRefresh: true);
final iJoinRefreshController = RefreshController(initialRefresh: true);
final iCreateGlobalKey = GlobalKey();
final iJoinGlobalKey = GlobalKey();
final conversationLogic = Get.find<ConversationLogic>();
final index = 0.obs;
final iCreatedList = <GroupInfo>[].obs;
final iJoinedList = <GroupInfo>[].obs;
int iCreatedOffset = 0;
int iJoinedOffset = 0;
int count = 1000;
@override
void onInit() {
imLoic.imSdkStatusPublishSubject.last.then((con) {
if (con.status == IMSdkStatus.syncEnded) {
iCreatedInitial();
iJoinedInitial();
}
});
iCreatedInitial();
iJoinedInitial();
super.onInit();
}
void switchTab(i) {
index.value = i;
}
void iCreatedInitial() async {
iCreatedOffset = 0;
iCreatedList.clear();
final length = await initial(iCreate: true, offset: iCreatedOffset);
iCreateRefreshController.refreshCompleted();
if (length >= count) {
iCreatedOffset += length;
} else {
iCreateRefreshController.loadNoData();
}
}
void iCreatedLoadMore() async {
final length = await loadMore(iCreate: true, offset: iCreatedOffset);
if (length < count) {
iCreateRefreshController.loadNoData();
} else {
iCreateRefreshController.loadComplete();
iCreatedOffset += length;
}
}
void iJoinedInitial() async {
iJoinedOffset = 0;
iJoinedList.clear();
final length = await initial(iCreate: false, offset: iJoinedOffset);
iJoinRefreshController.refreshCompleted();
if (length >= count) {
iJoinedOffset += length;
} else {
iJoinRefreshController.loadNoData();
}
}
void iJoinedLoadMore() async {
final length = await loadMore(iCreate: false, offset: iJoinedOffset);
if (length < count) {
iJoinRefreshController.loadNoData();
} else {
iJoinRefreshController.loadComplete();
iJoinedOffset += length;
}
}
Future<int> initial({bool iCreate = true, int offset = 0}) async {
final list = await OpenIM.iMManager.groupManager.getJoinedGroupListPage(offset: offset, count: count);
int iCreatedCount = 0;
int iJoinedCount = 0;
if (iCreate) {
final result = list.where((e) => e.ownerUserID == OpenIM.iMManager.userID);
iCreatedList.addAll(result);
iCreatedCount = result.length;
} else {
final result = list.where((e) => e.ownerUserID != OpenIM.iMManager.userID);
iJoinedList.addAll(result);
iJoinedCount = result.length;
}
return iCreate ? iCreatedCount : iJoinedCount;
}
Future<int> loadMore({bool iCreate = true, int offset = 0}) async {
final list = await OpenIM.iMManager.groupManager.getJoinedGroupListPage(offset: offset, count: count);
int iCreatedCount = 0;
int iJoinedCount = 0;
if (iCreate) {
final result = list.where((e) => e.ownerUserID == OpenIM.iMManager.userID);
iCreatedList.addAll(result);
iCreatedCount = result.length;
} else {
final result = list.where((e) => e.ownerUserID != OpenIM.iMManager.userID);
iJoinedList.addAll(result);
iJoinedCount = result.length;
}
return iCreate ? iCreatedCount : iJoinedCount;
}
void toGroupChat(GroupInfo info) {
conversationLogic.toChat(
offUntilHome: false,
groupID: info.groupID,
nickname: info.groupName,
faceURL: info.faceURL,
sessionType: info.sessionType,
);
}
void searchGroup() => AppNavigator.startSearchGroup();
}
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:openim_common/openim_common.dart';
import 'package:pull_to_refresh_new/pull_to_refresh.dart';
import 'package:sprintf/sprintf.dart';
import 'group_list_logic.dart';
class GroupListPage extends StatelessWidget {
final logic = Get.find<GroupListLogic>();
GroupListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: TitleBar.back(title: StrRes.myGroup),
backgroundColor: Styles.c_F8F9FA,
body: Column(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: logic.searchGroup,
child: Container(
color: Styles.c_FFFFFF,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
child: const SearchBox(),
),
),
Obx(
() => CustomTabBar(
labels: [StrRes.iCreatedGroup, StrRes.iJoinedGroup],
index: logic.index.value,
onTabChanged: (i) => logic.switchTab(i),
showUnderline: true,
),
),
Expanded(
child: Obx(
() => logic.index.value == 0 ? _buildICreatedListView() : _buildIJoinedListView(),
),
),
],
),
);
}
Widget _buildICreatedListView() => SmartRefresher(
key: logic.iCreateGlobalKey,
controller: logic.iCreateRefreshController,
header: IMViews.buildHeader(30),
footer: IMViews.buildFooter(),
enablePullUp: true,
enablePullDown: true,
onRefresh: logic.iCreatedInitial,
onLoading: logic.iCreatedLoadMore,
child: ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: logic.iCreatedList.length,
itemBuilder: (_, index) => _buildItemView(logic.iCreatedList[index]),
),
);
Widget _buildIJoinedListView() => SmartRefresher(
key: logic.iJoinGlobalKey,
controller: logic.iJoinRefreshController,
header: IMViews.buildHeader(30),
footer: IMViews.buildFooter(),
enablePullUp: true,
enablePullDown: true,
onRefresh: logic.iJoinedInitial,
onLoading: logic.iJoinedLoadMore,
child: ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: logic.iJoinedList.length,
itemBuilder: (_, index) => _buildItemView(logic.iJoinedList[index]),
),
);
Widget _buildItemView(GroupInfo info) => Ink(
height: 64.h,
color: Styles.c_FFFFFF,
child: InkWell(
onTap: () => logic.toGroupChat(info),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Row(
children: [
AvatarView(
url: info.faceURL,
text: info.groupName,
isGroup: true,
),
10.horizontalSpace,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
(info.groupName ?? '').toText..style = Styles.ts_0C1C33_17sp,
sprintf(StrRes.nPerson, [info.memberCount]).toText..style = Styles.ts_8E9AB0_14sp,
],
),
],
),
),
),
);
}
@@ -0,0 +1,10 @@
import 'package:get/get.dart';
import 'search_group_logic.dart';
class SearchGroupBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => SearchGroupLogic());
}
}
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart';
import '../../../conversation/conversation_logic.dart';
import '../group_list_logic.dart';
class SearchGroupLogic extends GetxController {
final conversationLogic = Get.find<ConversationLogic>();
final logic = Get.find<GroupListLogic>();
final focusNode = FocusNode();
final searchCtrl = TextEditingController();
final resultList = <GroupInfo>[].obs;
@override
void onInit() {
searchCtrl.addListener(_clearInput);
super.onInit();
}
@override
void onClose() {
focusNode.dispose();
searchCtrl.dispose();
super.onClose();
}
bool get isSearchNotResult => searchCtrl.text.trim().isNotEmpty && resultList.isEmpty;
_clearInput() {
final key = searchCtrl.text.trim();
if (key.isEmpty) {
resultList.clear();
}
}
search() async {
var key = searchCtrl.text.trim();
final result = await OpenIM.iMManager.groupManager.searchGroups(keywordList: [key], isSearchGroupName: true);
resultList.clear();
resultList.addAll(result);
}
void toGroupChat(GroupInfo info) {
conversationLogic.toChat(
offUntilHome: false,
groupID: info.groupID,
nickname: info.groupName,
faceURL: info.faceURL,
sessionType: info.sessionType,
);
}
}
@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:openim_common/openim_common.dart';
import 'package:search_keyword_text/search_keyword_text.dart';
import 'package:sprintf/sprintf.dart';
import 'search_group_logic.dart';
class SearchGroupPage extends StatelessWidget {
final logic = Get.find<SearchGroupLogic>();
SearchGroupPage({super.key});
@override
Widget build(BuildContext context) {
return TouchCloseSoftKeyboard(
child: Scaffold(
appBar: TitleBar.search(
focusNode: logic.focusNode,
controller: logic.searchCtrl,
onSubmitted: (_) => logic.search(),
onCleared: () => logic.focusNode.requestFocus(),
),
backgroundColor: Styles.c_F8F9FA,
body: Obx(() => logic.isSearchNotResult
? _emptyListView
: ListView.builder(
itemCount: logic.resultList.length,
itemBuilder: (_, index) => _buildItemView(logic.resultList[index]),
)),
),
);
}
Widget _buildItemView(GroupInfo info) => Ink(
height: 64.h,
color: Styles.c_FFFFFF,
child: InkWell(
onTap: () => logic.toGroupChat(info),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Row(
children: [
AvatarView(
url: info.faceURL,
text: info.groupName,
isGroup: true,
),
10.horizontalSpace,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SearchKeywordText(
text: info.groupName ?? '',
keyText: logic.searchCtrl.text.trim(),
style: Styles.ts_0C1C33_17sp,
keyStyle: Styles.ts_0089FF_17sp,
),
sprintf(StrRes.nPerson, [info.memberCount]).toText..style = Styles.ts_8E9AB0_14sp,
],
),
],
),
),
),
);
Widget get _emptyListView => SizedBox(
width: 1.sw,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
44.verticalSpace,
StrRes.searchNotFound.toText..style = Styles.ts_8E9AB0_17sp,
],
),
);
}