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,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;
}