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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user