feat(create):添加 files
This commit is contained in:
224
flutter_common/lib/calendarcalendar/calendar_choose_widget.dart
Normal file
224
flutter_common/lib/calendarcalendar/calendar_choose_widget.dart
Normal file
@@ -0,0 +1,224 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/calendarcalendar/custom_calendar_range_picker_widget.dart';
|
||||
import 'package:flutter_common/calendarcalendar/custom_date_picker.dart';
|
||||
import 'package:flutter_common/utils/date_utils.dart';
|
||||
import 'package:flutter_common/utils/toast_utils.dart';
|
||||
|
||||
///公用区间日期选选择组件
|
||||
class CalendarChooseWidget extends StatefulWidget {
|
||||
final Function? tapAction;
|
||||
final int? intervalDays; //间隔天数 (包括选中第一天和选中的最后一天)
|
||||
final DateTime? selectedDate; //默认选中日期
|
||||
final int? chooseIndex;
|
||||
final double? fontSize;
|
||||
final DateTimeUtilsType? dateTimeUtilsType;
|
||||
final FontWeight? fontWeight;
|
||||
final bool? onlyShow;
|
||||
final Color? textColor;
|
||||
final bool? isScafforrd;
|
||||
|
||||
const CalendarChooseWidget({
|
||||
super.key,
|
||||
this.tapAction,
|
||||
this.intervalDays,
|
||||
this.selectedDate,
|
||||
this.chooseIndex = 0,
|
||||
this.fontSize,
|
||||
this.onlyShow,
|
||||
this.fontWeight,
|
||||
this.dateTimeUtilsType,
|
||||
this.textColor,
|
||||
this.isScafforrd,
|
||||
});
|
||||
|
||||
@override
|
||||
_CalendarChooseWidgetState createState() => _CalendarChooseWidgetState();
|
||||
}
|
||||
|
||||
class _CalendarChooseWidgetState extends State<CalendarChooseWidget> {
|
||||
///开始时间
|
||||
DateTime? startTime = DateTime.now();
|
||||
|
||||
///结束时间
|
||||
DateTime? endTime;
|
||||
|
||||
///选择的时间区间
|
||||
DateTime? rangeStartTime = DateTime.now();
|
||||
DateTime? rangeEndTime;
|
||||
|
||||
///日期选择方法
|
||||
onDateSelected(DateTime? startDate, DateTime? endDate) {
|
||||
setState(() {
|
||||
rangeStartTime = startDate;
|
||||
rangeEndTime = endDate;
|
||||
});
|
||||
}
|
||||
|
||||
///确定按钮
|
||||
onConfirm() {
|
||||
if (widget.intervalDays != null && rangeEndTime != null) {
|
||||
var difference = rangeEndTime!.difference(rangeStartTime!);
|
||||
if (difference.inDays + 1 > widget.intervalDays!) {
|
||||
ToastUtils.showToast(msg: "时间差不能大于${widget.intervalDays}天");
|
||||
} else {
|
||||
changeDate();
|
||||
}
|
||||
} else {
|
||||
changeDate();
|
||||
}
|
||||
}
|
||||
|
||||
///把选中的时间数据赋值给initialStartDate、initialEndDate,并且返回选中的时间
|
||||
changeDate() {
|
||||
setState(() {
|
||||
startTime = rangeStartTime;
|
||||
endTime = rangeEndTime;
|
||||
});
|
||||
widget.tapAction
|
||||
?.call({"startTime": startTime, "endTime": endTime ?? startTime});
|
||||
// Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
///日期显示
|
||||
String get dealTimeString {
|
||||
String? time = "";
|
||||
if (endTime == null) {
|
||||
time = DateTimeUtils.dateTimeUtilsTool(
|
||||
dateTime: startTime.toString(),
|
||||
dateTimeUtilsType:
|
||||
widget.dateTimeUtilsType ?? DateTimeUtilsType.yearMonthDay,
|
||||
);
|
||||
} else if (endTime == startTime) {
|
||||
time = DateTimeUtils.dateTimeUtilsTool(
|
||||
dateTime: startTime.toString(),
|
||||
dateTimeUtilsType:
|
||||
widget.dateTimeUtilsType ?? DateTimeUtilsType.yearMonthDay,
|
||||
);
|
||||
} else {
|
||||
time = "${DateTimeUtils.dateTimeUtilsTool(
|
||||
dateTime: startTime.toString(),
|
||||
dateTimeUtilsType:
|
||||
widget.dateTimeUtilsType ?? DateTimeUtilsType.yearMonthDay,
|
||||
)} - ${DateTimeUtils.dateTimeUtilsTool(
|
||||
dateTime: endTime.toString(),
|
||||
dateTimeUtilsType:
|
||||
widget.dateTimeUtilsType ?? DateTimeUtilsType.yearMonthDay,
|
||||
)}";
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
///日历弹窗
|
||||
onTapDate() {
|
||||
if (widget.chooseIndex == 1) {
|
||||
ToastUtils.showBottomSheet(
|
||||
context: context,
|
||||
title: '选择时间',
|
||||
height: MediaQuery.of(context).size.height / 2,
|
||||
isShowConfirm: true,
|
||||
contentWidget: CustomDatePicker(
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(DateTime.now().year - 2),
|
||||
lastDate: DateTime(DateTime.now().year + 2),
|
||||
onDateChanged: (dateTime) {
|
||||
rangeStartTime = dateTime;
|
||||
rangeEndTime = dateTime;
|
||||
},
|
||||
),
|
||||
onConfirm: onConfirm,
|
||||
);
|
||||
} else {
|
||||
ToastUtils.showBottomSheet(
|
||||
context: context,
|
||||
title: '选择时间',
|
||||
height: MediaQuery.of(context).size.height / 2,
|
||||
isShowConfirm: true,
|
||||
contentWidget: CustomCalendarRangePickerWidget(
|
||||
firstDate: DateTime(DateTime.now().year - 2),
|
||||
lastDate: DateTime(DateTime.now().year + 2),
|
||||
initialStartDate: startTime,
|
||||
initialEndDate: endTime,
|
||||
selectedDateDecoration: BoxDecoration(
|
||||
color: const Color(0xff4D6FD5),
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
rangeBackgroundColor: const Color(0xffEDF0FF),
|
||||
weekendTextStyle: const TextStyle(
|
||||
color: Color(0xff4D6FD5),
|
||||
fontSize: 12,
|
||||
),
|
||||
weekTextStyle: const TextStyle(
|
||||
color: Color(0xff333333),
|
||||
fontSize: 12,
|
||||
),
|
||||
yearTextStyle: const TextStyle(
|
||||
color: Color(0xff333333),
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
onDateSelected: onDateSelected,
|
||||
),
|
||||
onConfirm: onConfirm,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.selectedDate != null) {
|
||||
startTime = widget.selectedDate;
|
||||
rangeStartTime = widget.selectedDate;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.isScafforrd == true
|
||||
? Scaffold(
|
||||
body: InkWell(
|
||||
onTap: widget.onlyShow == true ? null : onTapDate,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
dealTimeString,
|
||||
style: TextStyle(
|
||||
color: widget.textColor ?? const Color(0xff1A1A1A),
|
||||
fontSize: widget.fontSize ?? 16,
|
||||
fontWeight: widget.fontWeight,
|
||||
),
|
||||
),
|
||||
widget.onlyShow == true
|
||||
? const SizedBox()
|
||||
: const Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
size: 15,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: InkWell(
|
||||
onTap: widget.onlyShow == true ? null : onTapDate,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
dealTimeString,
|
||||
style: TextStyle(
|
||||
color: widget.textColor ?? const Color(0xff1A1A1A),
|
||||
fontSize: widget.fontSize ?? 16,
|
||||
fontWeight: widget.fontWeight,
|
||||
),
|
||||
),
|
||||
widget.onlyShow == true
|
||||
? const SizedBox()
|
||||
: const Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
size: 15,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1343
flutter_common/lib/calendarcalendar/custom_date_picker.dart
Normal file
1343
flutter_common/lib/calendarcalendar/custom_date_picker.dart
Normal file
File diff suppressed because it is too large
Load Diff
1093
flutter_common/lib/calendarcalendar/custom_multiple_choose_date_picker.dart
Executable file
1093
flutter_common/lib/calendarcalendar/custom_multiple_choose_date_picker.dart
Executable file
File diff suppressed because it is too large
Load Diff
1344
flutter_common/lib/calendarcalendar/custom_single_choose_date_picker.dart
Executable file
1344
flutter_common/lib/calendarcalendar/custom_single_choose_date_picker.dart
Executable file
File diff suppressed because it is too large
Load Diff
326
flutter_common/lib/http/http_utils.dart
Executable file
326
flutter_common/lib/http/http_utils.dart
Executable file
@@ -0,0 +1,326 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:ui';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import '../utils/toast_utils.dart';
|
||||
|
||||
///请求方式-枚举类型
|
||||
enum MethodType { get, post, delete, put, patch }
|
||||
|
||||
class HttpUtils {
|
||||
static final HttpUtils _instance = HttpUtils._internal(baseUrl: '');
|
||||
|
||||
factory HttpUtils() => _instance;
|
||||
|
||||
late Dio dio;
|
||||
|
||||
HttpUtils._internal({required String baseUrl}) {
|
||||
BaseOptions options = BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
connectTimeout: const Duration(milliseconds: 15000),
|
||||
receiveTimeout: const Duration(milliseconds: 15000),
|
||||
responseType: ResponseType.json,
|
||||
validateStatus: (status) {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
dio = Dio(options);
|
||||
|
||||
/// 添加请求日志
|
||||
dio.interceptors.add(LogInterceptor(
|
||||
requestBody: true,
|
||||
responseHeader: false,
|
||||
responseBody: false,
|
||||
error: false,
|
||||
));
|
||||
|
||||
/// 添加拦截器
|
||||
dio.interceptors.add(InterceptorsWrapper(
|
||||
onResponse: (response, handler) {
|
||||
/// 打印返回值
|
||||
const bool inProduction = bool.fromEnvironment("dart.vm.product");
|
||||
if (!inProduction) {
|
||||
log(
|
||||
jsonEncode(response.data),
|
||||
name: 'Response Text: ',
|
||||
);
|
||||
}
|
||||
|
||||
/// 对接口抛出异常的检查
|
||||
_apiVerify(response);
|
||||
return handler.next(response);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
///统一请求入口
|
||||
static Future<NetResult> request(
|
||||
String baseUrl,
|
||||
String path, {
|
||||
String? token,
|
||||
MethodType method = MethodType.get,
|
||||
Map<String, String>? headers,
|
||||
Map<String, dynamic>? body,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
bool isShowLoading = false,
|
||||
CancelToken? cancelToken,
|
||||
VoidCallback? unLoginAction,
|
||||
}) async {
|
||||
///开始网络请求
|
||||
late NetResult netResult;
|
||||
Dio dio = HttpUtils._internal(baseUrl: baseUrl).dio;
|
||||
// String? token1 = token ?? '';
|
||||
Map<String, dynamic> headerMap = {};
|
||||
if (headers != null) headerMap.addAll(headers);
|
||||
headerMap['token'] = token;
|
||||
headerMap['Authorization'] = token;
|
||||
Options options = Options(
|
||||
method: method.name,
|
||||
headers: headerMap,
|
||||
);
|
||||
CancelToken newCancelToken = cancelToken ?? CancelToken();
|
||||
try {
|
||||
if (isShowLoading) {
|
||||
await EasyLoading.show(
|
||||
// status: 'loading...',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await dio.request(
|
||||
path,
|
||||
options: options,
|
||||
queryParameters: queryParameters,
|
||||
data: body,
|
||||
cancelToken: newCancelToken,
|
||||
);
|
||||
// if (response.headers["authorization"] != null) {
|
||||
// LocalStore.setValue(
|
||||
// key: StoreValue.tokenKey,
|
||||
// value: response.headers["authorization"]![0]);
|
||||
// }
|
||||
if (isShowLoading) EasyLoading.dismiss();
|
||||
netResult = NetResult(
|
||||
headers: response.headers,
|
||||
result: response.data,
|
||||
statusCode: response.statusCode ?? 0,
|
||||
error: _apiIsSucceed(response),
|
||||
);
|
||||
} on DioError catch (error) {
|
||||
if (isShowLoading) EasyLoading.dismiss();
|
||||
_formatError(error);
|
||||
netResult = NetResult(
|
||||
errorMeg: error.message ?? '服务器出现错误',
|
||||
);
|
||||
}
|
||||
return netResult;
|
||||
}
|
||||
|
||||
///下载
|
||||
static Future<NetResult> download(
|
||||
String path,
|
||||
String savePath, {
|
||||
ProgressCallback? onReceiveProgress,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
CancelToken? cancelToken,
|
||||
bool deleteOnError = true,
|
||||
String lengthHeader = Headers.contentLengthHeader,
|
||||
data,
|
||||
Options? options,
|
||||
}) async {
|
||||
///开始网络请求
|
||||
late NetResult netResult;
|
||||
try {
|
||||
Response response = await Dio().download(
|
||||
path,
|
||||
savePath,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
queryParameters: queryParameters,
|
||||
cancelToken: cancelToken,
|
||||
deleteOnError: deleteOnError,
|
||||
lengthHeader: lengthHeader,
|
||||
data: data,
|
||||
options: options,
|
||||
);
|
||||
netResult = NetResult(
|
||||
headers: response.headers,
|
||||
result: 'Download successful',
|
||||
statusCode: response.statusCode ?? 0,
|
||||
error: response.statusCode != 200,
|
||||
);
|
||||
} on DioError catch (error) {
|
||||
_formatError(error);
|
||||
netResult = NetResult(
|
||||
errorMeg: error.message ?? '服务器出现错误',
|
||||
result: 'Download failure',
|
||||
);
|
||||
}
|
||||
return netResult;
|
||||
}
|
||||
|
||||
///判断接口是否成功
|
||||
static bool _apiIsSucceed(Response response) {
|
||||
if (response.data != null && response.data != '') {
|
||||
Map<String, dynamic> map = response.data;
|
||||
return map['resultCode'] == 'OK' ? false : true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
///接口校验
|
||||
static void _apiVerify(Response response, {VoidCallback? unLoginAction}) {
|
||||
if (response.data != null && response.data != '') {
|
||||
///判断返回值是否是Map
|
||||
if (response.data is Map) {
|
||||
Map<String, dynamic> map = response.data;
|
||||
String resultCode = map['resultCode'];
|
||||
|
||||
if (resultCode.toUpperCase() == "FAIL") {
|
||||
throw DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
response: response,
|
||||
type: DioExceptionType.unknown,
|
||||
error: response.data,
|
||||
message: map['resultMsg'],
|
||||
);
|
||||
}
|
||||
|
||||
if (resultCode.toUpperCase() == "UNLOGIN") {
|
||||
unLoginAction?.call();
|
||||
return;
|
||||
}
|
||||
} else if (response.data is String) {
|
||||
throw DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
response: response,
|
||||
type: DioExceptionType.unknown,
|
||||
error: response.data,
|
||||
message: response.data,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
response: response,
|
||||
type: DioExceptionType.unknown,
|
||||
error: response.data,
|
||||
message: response.data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Dio库error统一处理
|
||||
*/
|
||||
static void _formatError(DioException error) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.cancel:
|
||||
break;
|
||||
case DioExceptionType.connectionTimeout:
|
||||
ToastUtils.showToast(msg: '网络连接超时');
|
||||
break;
|
||||
case DioExceptionType.sendTimeout:
|
||||
ToastUtils.showToast(msg: '网络请求超时');
|
||||
break;
|
||||
case DioExceptionType.receiveTimeout:
|
||||
ToastUtils.showToast(msg: '服务响应超时');
|
||||
break;
|
||||
case DioExceptionType.unknown:
|
||||
ToastUtils.showToast(msg: error.message);
|
||||
break;
|
||||
case DioExceptionType.badResponse:
|
||||
try {
|
||||
int? errCode = error.response!.statusCode;
|
||||
String? errMsg = error.response!.statusMessage;
|
||||
switch (errCode) {
|
||||
case 400:
|
||||
ToastUtils.errorToast(errorText: '请求语法错误');
|
||||
break;
|
||||
case 401:
|
||||
ToastUtils.errorToast(errorText: '没有权限');
|
||||
break;
|
||||
case 403:
|
||||
ToastUtils.errorToast(errorText: '服务器拒绝执行');
|
||||
break;
|
||||
case 404:
|
||||
ToastUtils.errorToast(errorText: '无法连接服务器');
|
||||
break;
|
||||
case 405:
|
||||
ToastUtils.errorToast(errorText: '请求方法被禁止');
|
||||
break;
|
||||
case 500:
|
||||
ToastUtils.errorToast(errorText: '服务器出现问题');
|
||||
break;
|
||||
case 502:
|
||||
ToastUtils.errorToast(errorText: '无效的请求');
|
||||
break;
|
||||
case 503:
|
||||
ToastUtils.errorToast(errorText: '服务器挂了');
|
||||
break;
|
||||
case 505:
|
||||
ToastUtils.errorToast(errorText: '不支持HTTP协议请求');
|
||||
break;
|
||||
default:
|
||||
ToastUtils.errorToast(errorText: errMsg ?? '未知错误');
|
||||
break;
|
||||
}
|
||||
} on Exception catch (_) {
|
||||
ToastUtils.errorToast(errorText: '未知错误');
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ToastUtils.errorToast(errorText: error.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 自定义Dio返回类
|
||||
class NetResult {
|
||||
dynamic result;
|
||||
|
||||
dynamic headers;
|
||||
|
||||
int total;
|
||||
|
||||
bool error;
|
||||
|
||||
int statusCode;
|
||||
|
||||
String errorMeg;
|
||||
|
||||
bool get success => !error;
|
||||
|
||||
NetResult({
|
||||
this.result,
|
||||
this.headers,
|
||||
this.statusCode = -1,
|
||||
this.error = true,
|
||||
this.total = 0,
|
||||
this.errorMeg = '',
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NetResult:statusCode: $statusCode \n'
|
||||
'NetResult: headers: $headers \n'
|
||||
'NetResult: error: $error \n'
|
||||
'NetResult: total: $total \n'
|
||||
'NetResult: errorMeg: $errorMeg \n'
|
||||
'NetResult: result: ${result.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 自定义Dio取消请求处理类
|
||||
class CustomCancelToken {
|
||||
static CancelToken cancelToken = CancelToken();
|
||||
|
||||
static cancel() {
|
||||
cancelToken.cancel('取消请求');
|
||||
cancelToken = CancelToken();
|
||||
}
|
||||
}
|
||||
45
flutter_common/lib/kola/colors.dart
Normal file
45
flutter_common/lib/kola/colors.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class JHYJColors {
|
||||
///背景色
|
||||
static const Color backgroundColor = Color(0xffF5F6F7);
|
||||
|
||||
// Color(0xffF0F4F8)
|
||||
static const Color themeColor = Color(0xffF3C5BB);
|
||||
|
||||
//JHYJColors.themeTextColor
|
||||
static const Color themeTextColor = Color(0xff1A1A1A);
|
||||
|
||||
static const Color appBarColor = Color(0xff4B67F8);
|
||||
|
||||
static const Color contentGreyColor = Color(0xffAAAAAA);
|
||||
|
||||
///7
|
||||
static const Color content7Color = Color(0xff777777);
|
||||
static const Color contentF5Color = Color(0xfff5f5f5);
|
||||
static const Color contentCCCColor = Color(0xffCCCCCC);
|
||||
static const Color contentRedColor = Color(0xffE62222);
|
||||
|
||||
static const Color redTextColor = Color(0xffF84B4B);
|
||||
static const Color contentE62222 = Color(0xffE62222);
|
||||
static const Color content443F4E = Color(0xff443F4E);
|
||||
static const Color contentFF7A3B = Color(0xffFF7A3B);
|
||||
static const Color content4572E2 = Color(0xff4572E2);
|
||||
static const Color contentF4F5FF = Color(0xffF4F5FF);
|
||||
static const Color content46A5FF = Color(0xff46A5FF);
|
||||
static const Color contentE0D0EA = Color(0xffE0D0EA);
|
||||
static const Color contentD5F3F4 = Color(0xffD5F3F4);
|
||||
static const Color content56C1D2 = Color(0xff56C1D2);
|
||||
|
||||
|
||||
|
||||
//内容影响力颜色
|
||||
static const List<Color> contentColor = [
|
||||
Color(0xff0152C7),
|
||||
Color(0xffF84B4B),
|
||||
Color(0xffD25DD6),
|
||||
Color(0xff5ED3E5),
|
||||
Color(0xff4B67F8),
|
||||
Color(0xff5CD1F7),
|
||||
];
|
||||
}
|
||||
240
flutter_common/lib/kola/goods_edite_title_widget.dart
Normal file
240
flutter_common/lib/kola/goods_edite_title_widget.dart
Normal file
@@ -0,0 +1,240 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/customer.dart';
|
||||
import 'package:flutter_common/value/string.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
///商品文字标题
|
||||
class GoodsEditeTitleWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final TextStyle? style;
|
||||
final String? hint; //灰色提示
|
||||
final bool? isRed; //是否有红点
|
||||
final Widget? rightChild;
|
||||
final Widget? contentChild;
|
||||
|
||||
const GoodsEditeTitleWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.hint,
|
||||
this.isRed,
|
||||
this.rightChild,
|
||||
this.contentChild,
|
||||
this.style,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
key: Key(BaseStringValue.cellKeyString(string: title)),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: style ??
|
||||
CustomerTextStyle(
|
||||
customerFontSize: 30.sp,
|
||||
customerFontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
isRed == true
|
||||
? Text(
|
||||
' * ',
|
||||
textAlign: TextAlign.start,
|
||||
style: CustomerTextStyle(
|
||||
customerColor: Colors.red,
|
||||
customerFontSize: 20.sp,
|
||||
customerFontWeight: FontWeight.bold,
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
Text(
|
||||
hint ?? '',
|
||||
textAlign: TextAlign.start,
|
||||
style: CustomerTextStyle(
|
||||
customerColor: const Color(0xff777777),
|
||||
customerFontSize: 24.sp,
|
||||
),
|
||||
),
|
||||
const Expanded(child: SizedBox()),
|
||||
rightChild ?? const SizedBox(),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 16.h, bottom: 16.h),
|
||||
child: contentChild ?? const SizedBox(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
class GoodsEditeContentWidget extends StatelessWidget {
|
||||
final Widget? child;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final double? borderRadius;
|
||||
final double? height;
|
||||
final Color? backColor;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const GoodsEditeContentWidget({
|
||||
super.key,
|
||||
this.child,
|
||||
this.padding,
|
||||
this.borderRadius,
|
||||
this.height,
|
||||
this.backColor,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: height,
|
||||
padding: padding ?? const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: backColor ?? Colors.white,
|
||||
borderRadius: BorderRadius.circular(borderRadius?.h ?? 30.h),
|
||||
),
|
||||
child: child ?? const SizedBox(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///输入框
|
||||
class GoodsEditeTextFiled extends StatefulWidget {
|
||||
final String? hint;
|
||||
final TextInputType? keyboardType;
|
||||
final String? prefixText;
|
||||
final String? suffixText;
|
||||
final int? maxLines;
|
||||
final Function? onChanged;
|
||||
final String? content;
|
||||
final double? borderRadius;
|
||||
final double? height;
|
||||
final bool? obscureText;
|
||||
final bool? readOnly;
|
||||
final Color? backColor;
|
||||
final int? maxNumberText;
|
||||
final bool? hadOver; //是否必须输入才返回,默认是false
|
||||
final TextInputAction? textInputAction;
|
||||
final TextAlign? textAlign;
|
||||
|
||||
const GoodsEditeTextFiled({
|
||||
super.key,
|
||||
this.hint,
|
||||
this.keyboardType,
|
||||
this.prefixText,
|
||||
this.suffixText,
|
||||
this.maxLines,
|
||||
this.onChanged,
|
||||
this.content,
|
||||
this.borderRadius,
|
||||
this.obscureText = false,
|
||||
this.height,
|
||||
this.readOnly = false,
|
||||
this.backColor,
|
||||
this.maxNumberText,
|
||||
this.hadOver = false,
|
||||
this.textInputAction,
|
||||
this.textAlign,
|
||||
});
|
||||
|
||||
@override
|
||||
State<GoodsEditeTextFiled> createState() => _GoodsEditeTextFiledState();
|
||||
}
|
||||
|
||||
class _GoodsEditeTextFiledState extends State<GoodsEditeTextFiled> {
|
||||
TextEditingController controller = TextEditingController();
|
||||
FocusNode focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
controller = TextEditingController(text: widget.content);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GoodsEditeContentWidget(
|
||||
padding: EdgeInsets.zero,
|
||||
backColor: widget.backColor,
|
||||
borderRadius: widget.borderRadius,
|
||||
height: widget.height,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
TextField(
|
||||
textAlign: widget.textAlign ?? TextAlign.start,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
maxLength: widget.maxNumberText,
|
||||
// textInputAction: TextInputAction.done,
|
||||
maxLines: widget.maxLines,
|
||||
style: CustomerTextStyle(
|
||||
customerFontSize: 28.sp,
|
||||
),
|
||||
readOnly: widget.readOnly ?? false,
|
||||
obscureText: widget.obscureText ?? false,
|
||||
keyboardType: widget.keyboardType ?? TextInputType.text,
|
||||
textInputAction: widget.textInputAction ?? TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(
|
||||
///设置边框四个角的弧度
|
||||
// borderRadius: BorderRadius.all(
|
||||
// Radius.circular(
|
||||
// 12.5,
|
||||
// ),
|
||||
// ),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
counterText: '',
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
hintText: widget.hint ?? '',
|
||||
hintStyle: CustomerTextStyle(
|
||||
customerColor: const Color(0xffAAAAAA),
|
||||
customerFontSize: 28.sp,
|
||||
),
|
||||
prefixText: widget.prefixText ?? ' ',
|
||||
prefixStyle: CustomerTextStyle(
|
||||
customerFontSize: 28.sp,
|
||||
),
|
||||
),
|
||||
onEditingComplete: () {
|
||||
widget.onChanged?.call(controller.text);
|
||||
},
|
||||
onSubmitted: widget.hadOver == true
|
||||
? (value) {
|
||||
focusNode.unfocus();
|
||||
widget.onChanged?.call(value);
|
||||
}
|
||||
: (value) {
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged: widget.hadOver == true
|
||||
? null
|
||||
: (value) => widget.onChanged?.call(value),
|
||||
),
|
||||
widget.maxNumberText == null
|
||||
? const SizedBox()
|
||||
: Padding(
|
||||
padding: EdgeInsets.only(bottom: 20.h, right: 30.w),
|
||||
child: Text(
|
||||
'${controller.text.length}/${widget.maxNumberText}',
|
||||
style: CustomerTextStyle(
|
||||
customerFontSize: 28.sp,
|
||||
customerColor: const Color(0xffCCCCCC),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
86
flutter_common/lib/lineline/dash_line.dart
Executable file
86
flutter_common/lib/lineline/dash_line.dart
Executable file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///@Desc 虚线
|
||||
class DashLine extends StatelessWidget {
|
||||
final Color? color; // 虚线颜色
|
||||
final Axis direction; // 虚线方向
|
||||
final double? dottedLength; //虚线长度
|
||||
|
||||
const DashLine({
|
||||
Key? key,
|
||||
this.color,
|
||||
this.direction = Axis.horizontal,
|
||||
this.dottedLength,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: DashedLinePainter(
|
||||
color: color ?? const Color(0xff36C2FD),
|
||||
direction: direction,
|
||||
dottedLength: dottedLength,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashedLinePainter extends CustomPainter {
|
||||
final Color color;
|
||||
final double width;
|
||||
final Axis direction; //方向
|
||||
final double? dottedLength; //虚线长度
|
||||
|
||||
DashedLinePainter({
|
||||
this.width = 1,
|
||||
this.color = Colors.black,
|
||||
this.direction = Axis.horizontal,
|
||||
this.dottedLength,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Paint paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = width
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..isAntiAlias = true;
|
||||
Path dottedPath = _createDashedPath(
|
||||
Path()
|
||||
..moveTo(0, 0)
|
||||
..lineTo(
|
||||
direction == Axis.horizontal ? size.width : 0,
|
||||
direction == Axis.horizontal ? 0 : size.height,
|
||||
),
|
||||
length: dottedLength,
|
||||
);
|
||||
canvas.drawPath(dottedPath, paint);
|
||||
}
|
||||
|
||||
Path _createDashedPath(Path path,{double? length}) {
|
||||
Path targetPath = Path();
|
||||
double dottedLength = length ?? 10;
|
||||
double dottedGap = length ?? 10;
|
||||
for (var metric in path.computeMetrics()) {
|
||||
double distance = 0;
|
||||
bool isDrawDotted = true;
|
||||
while (distance < metric.length) {
|
||||
if (isDrawDotted) {
|
||||
targetPath.addPath(
|
||||
metric.extractPath(distance, distance + dottedLength),
|
||||
Offset.zero,
|
||||
);
|
||||
distance += dottedLength;
|
||||
} else {
|
||||
distance += dottedGap;
|
||||
}
|
||||
isDrawDotted = !isDrawDotted;
|
||||
}
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(DashedLinePainter oldDelegate) => false;
|
||||
}
|
||||
61
flutter_common/lib/lineline/time_line.dart
Executable file
61
flutter_common/lib/lineline/time_line.dart
Executable file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'dash_line.dart';
|
||||
|
||||
///@Desc 时间线
|
||||
class TimeLine extends StatelessWidget {
|
||||
final bool isLast;
|
||||
final Widget child;
|
||||
|
||||
const TimeLine({
|
||||
Key? key,
|
||||
required this.isLast,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDot(),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDot() {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Color(0xff36C2FD),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isLast)
|
||||
const SizedBox.shrink()
|
||||
else
|
||||
const Expanded(
|
||||
child: DashLine(
|
||||
color: Color(0xff36C2FD),
|
||||
direction: Axis.vertical,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
251
flutter_common/lib/rank_list/customer_rank_list.dart
Normal file
251
flutter_common/lib/rank_list/customer_rank_list.dart
Normal file
@@ -0,0 +1,251 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/customer.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class CustomerRankListModel {
|
||||
final List<CustomerRankListContent>? content;
|
||||
final String? detailTitle;
|
||||
final List<CustomerRankDetailContent>? detail;
|
||||
|
||||
CustomerRankListModel({
|
||||
this.detailTitle,
|
||||
this.content,
|
||||
this.detail,
|
||||
});
|
||||
}
|
||||
|
||||
class CustomerRankListContent {
|
||||
final String? content;
|
||||
final TextAlign? textAlign;
|
||||
|
||||
CustomerRankListContent({
|
||||
this.content,
|
||||
this.textAlign,
|
||||
});
|
||||
}
|
||||
|
||||
class CustomerRankDetailContent {
|
||||
final String? content;
|
||||
final String? label;
|
||||
|
||||
CustomerRankDetailContent({
|
||||
this.content,
|
||||
this.label,
|
||||
});
|
||||
}
|
||||
|
||||
class CustomerRankListWidget extends StatelessWidget {
|
||||
// final String? title;
|
||||
final Widget? titleWidget;
|
||||
final Color? titleBackgroundColor;
|
||||
final Color? contentBackgroundColor;
|
||||
final List<CustomerRankListContent> titleList;
|
||||
final List<CustomerRankListModel>? contentList;
|
||||
final double? titleHeight;
|
||||
final double? contentHeight;
|
||||
final Function? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final double? titleFontSize;
|
||||
final double? contentFontSize;
|
||||
|
||||
const CustomerRankListWidget({
|
||||
super.key,
|
||||
required this.titleList,
|
||||
this.contentList,
|
||||
this.titleHeight,
|
||||
this.contentHeight,
|
||||
this.onTap,
|
||||
this.padding,
|
||||
this.titleWidget,
|
||||
this.titleBackgroundColor = Colors.transparent,
|
||||
this.contentBackgroundColor = Colors.transparent,
|
||||
this.titleFontSize = 34,
|
||||
this.contentFontSize = 30,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
padding: padding ?? EdgeInsets.symmetric(horizontal: 60.w),
|
||||
child: Column(
|
||||
children: [
|
||||
titleWidget ?? const SizedBox(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 16.h),
|
||||
child: detailWidget(
|
||||
content: titleList,
|
||||
isTitle: true,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.only(
|
||||
top: 30.h,
|
||||
bottom: 16.h,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
CustomerRankListModel? model = contentList?[index];
|
||||
return Column(
|
||||
children: [
|
||||
detailWidget(
|
||||
content: model?.content ?? [],
|
||||
isTitle: false,
|
||||
onTap: () => onTap?.call(model?.content?[1]),
|
||||
),
|
||||
model?.detail == null
|
||||
? const SizedBox()
|
||||
: contentDetailListWidget(
|
||||
detailTitle: model?.detailTitle,
|
||||
detail: model?.detail,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, index) {
|
||||
return Divider(
|
||||
height: 0.5,
|
||||
color: const Color(0xff399FFF).withOpacity(0.05),
|
||||
);
|
||||
},
|
||||
itemCount: contentList?.length ?? 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
dealContentData({
|
||||
required Map<String, dynamic> content,
|
||||
required int index,
|
||||
}) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Widget detailWidget({
|
||||
required List<CustomerRankListContent?> content,
|
||||
required bool isTitle,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height:
|
||||
isTitle == true ? titleHeight?.h ?? 30.h : contentHeight?.h ?? 40.h,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isTitle != true ? contentBackgroundColor : titleBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(10.h),
|
||||
),
|
||||
child: Row(
|
||||
children: content
|
||||
.map(
|
||||
(e) => Expanded(
|
||||
child: e?.content?.contains('assets/') == true
|
||||
? Image.asset(
|
||||
e?.content ?? '',
|
||||
width: contentHeight,
|
||||
height: contentHeight,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
: e?.content?.contains('http') == true
|
||||
? CustomerImagesNetworking(
|
||||
imageUrl: e?.content ?? '',
|
||||
width: contentHeight,
|
||||
height: contentHeight,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
: detailText(
|
||||
e?.content ?? '',
|
||||
isTitle: isTitle,
|
||||
textAlign: e?.textAlign,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget contentDetailListWidget({
|
||||
String? detailTitle,
|
||||
List<CustomerRankDetailContent>? detail,
|
||||
}) {
|
||||
return detail?.isEmpty == true
|
||||
? const SizedBox()
|
||||
: CustomerContainer(
|
||||
color: const Color(0xfff5F5F5),
|
||||
borderRadius: 30,
|
||||
margin: EdgeInsets.only(left: 60.w, right: 60.w, bottom: 20.h),
|
||||
padding: EdgeInsets.all(30.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${detailTitle}TOP10',
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30.h,),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (_, index) {
|
||||
CustomerRankDetailContent? detailContent = detail?[index];
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
detailContent?.label ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: contentFontSize,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
detailContent?.content ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: contentFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, index) {
|
||||
return SizedBox(height: 20.h);
|
||||
},
|
||||
itemCount: detail?.length ?? 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
detailText(
|
||||
text, {
|
||||
bool? isTitle,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: textAlign == TextAlign.start ? 60.w : 0,
|
||||
right: textAlign == TextAlign.end ? 60.w : 0,
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: textAlign ?? TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isTitle == true ? titleFontSize : contentFontSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
142
flutter_common/lib/upload_image/down_load_image_tool.dart
Normal file
142
flutter_common/lib/upload_image/down_load_image_tool.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_common/utils/toast_utils.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
|
||||
|
||||
// import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class DownLoadImageTool {
|
||||
|
||||
// 请求照片库权限
|
||||
static Future<bool> requestPhotoPermission() async {
|
||||
// 检查当前权限状态
|
||||
var status = await Permission.photos.status;
|
||||
|
||||
if (status.isDenied) {
|
||||
// 请求权限
|
||||
status = await Permission.photos.request();
|
||||
|
||||
// 如果用户拒绝了权限,可以显示一个解释
|
||||
if (status.isPermanentlyDenied) {
|
||||
// 打开应用设置,让用户手动启用权限
|
||||
await openAppSettings();
|
||||
}
|
||||
|
||||
if (status.isDenied) {
|
||||
// 打开应用设置,让用户手动启用权限
|
||||
await openAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
return status.isGranted;
|
||||
}
|
||||
|
||||
// 或者请求存储权限(适用于Android)
|
||||
static Future<bool> requestStoragePermission() async {
|
||||
if (Platform.isAndroid) {
|
||||
// 对于Android 13及以上版本
|
||||
if (await DeviceInfoPlugin().androidInfo.then((info) => info.version.sdkInt) >= 33) {
|
||||
var status = await Permission.photos.request();
|
||||
if(status == PermissionStatus.denied){
|
||||
await requestPhotoPermission();
|
||||
}
|
||||
return status.isGranted;
|
||||
} else {
|
||||
// 对于Android 13以下版本
|
||||
var status = await Permission.storage.request();
|
||||
return status.isGranted;
|
||||
}
|
||||
} else {
|
||||
// iOS使用照片权限
|
||||
return await requestPhotoPermission();
|
||||
}
|
||||
}
|
||||
|
||||
///保存到相册
|
||||
static Future<dynamic> savePhoto({
|
||||
required String imageUrl,
|
||||
bool? isHideLoading,
|
||||
}) async {
|
||||
//获取保存相册权限,如果没有,则申请改权限
|
||||
bool permition = await requestStoragePermission();
|
||||
if (permition) {
|
||||
var result = imageRequest(
|
||||
imageUrl: imageUrl,
|
||||
isHideLoading: isHideLoading,
|
||||
);
|
||||
return result;
|
||||
} else {
|
||||
//重新请求--第一次请求权限时,保存方法不会走,需要重新调一次
|
||||
ToastUtils.showToast(msg: '请打开手机相册权限');
|
||||
// savePhoto(imageUrl: imageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<dynamic> imageRequest({
|
||||
required String imageUrl,
|
||||
bool? isHideLoading,
|
||||
}) async {
|
||||
if (isHideLoading == true) {
|
||||
} else {
|
||||
await EasyLoading.show(
|
||||
// status: 'loading...',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
}
|
||||
|
||||
var response = await Dio().get(
|
||||
imageUrl,
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
);
|
||||
if (isHideLoading == true) {
|
||||
} else {
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
|
||||
final result = await ImageGallerySaverPlus.saveImage(
|
||||
Uint8List.fromList(response.data),
|
||||
quality: 60,
|
||||
name: "hello",
|
||||
isReturnImagePathOfIOS: true,
|
||||
);
|
||||
print('=result ============ $result');
|
||||
return result;
|
||||
}
|
||||
|
||||
///fetchImageAsUint8List
|
||||
static Future<Uint8List?> fetchImageAsUint8List(String imageUrl) async {
|
||||
await EasyLoading.show(
|
||||
// status: 'loading...',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
try {
|
||||
var response = await Dio().get(
|
||||
imageUrl,
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
);
|
||||
// final response = await http.get(Uri.parse(imageUrl));
|
||||
if (response.statusCode == 200) {
|
||||
EasyLoading.dismiss();
|
||||
return response.data;
|
||||
} else {
|
||||
// debugPrint('Failed to load image: ${response.statusCode}');
|
||||
EasyLoading.dismiss();
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
// debugPrint('Error fetching image: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
flutter_common/lib/upload_image/download_local_tool.dart
Normal file
93
flutter_common/lib/upload_image/download_local_tool.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
|
||||
// import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class DownloadLocalTool {
|
||||
/// 拿到存储路径
|
||||
/// 使用getTemporaryDirectory()方法可以获取应用程序的临时目录,该目录用于存储应用程序的临时数据。这个目录在应用程序退出后会被清空
|
||||
Future<String> getTemporaryDirectoryString() async {
|
||||
final directory = await getTemporaryDirectory();
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
/// 使用getApplicationDocumentsDirectory()方法可以获取应用程序的文档目录,该目录用于存储应用程序的私有数据。
|
||||
Future<String> getApplicationDocumentsDirectoryString() async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
/// 使用getExternalStorageDirectory()方法可以获取设备的外部存储目录,该目录用于存储应用程序的公共数据。需要注意的是,在某些设备上,外部存储目录可能是不可用的。
|
||||
Future<String> getExternalStorageDirectoryString() async {
|
||||
final directory = await getExternalStorageDirectory();
|
||||
return directory?.path ?? "";
|
||||
}
|
||||
|
||||
/// 创建对文件位置的引用
|
||||
Future<File> localFile(String fileName) async {
|
||||
final path = await getApplicationDocumentsDirectoryString();
|
||||
return File('$path/$fileName');
|
||||
}
|
||||
|
||||
///save
|
||||
Future<dynamic> saveNetworkVideoFile({
|
||||
required String fileName,
|
||||
required String fileUrl,
|
||||
}) async {
|
||||
final path = await getApplicationDocumentsDirectoryString();
|
||||
String savePath = "$path/$fileName ";
|
||||
// String fileUrl =
|
||||
// "https://s3.cn-north-1.amazonaws.com.cn/mtab.kezaihui.com/video/ForBiggerBlazes.mp4";
|
||||
//
|
||||
await Dio().download(fileUrl, savePath, onReceiveProgress: (count, total) {
|
||||
print("${(count / total * 100).toStringAsFixed(0)}%");
|
||||
});
|
||||
final result = await ImageGallerySaverPlus.saveFile(savePath);
|
||||
return result;
|
||||
// print(result);
|
||||
}
|
||||
|
||||
///save
|
||||
Future<dynamic> saveNetworkVideoFileExternalStorageDirectory({
|
||||
required String fileName,
|
||||
required String fileUrl,
|
||||
}) async {
|
||||
final path = await getApplicationDocumentsDirectoryString();
|
||||
String savePath = "$path/$fileName ";
|
||||
// String fileUrl =
|
||||
// "https://s3.cn-north-1.amazonaws.com.cn/mtab.kezaihui.com/video/ForBiggerBlazes.mp4";
|
||||
//
|
||||
await Dio().download(fileUrl, savePath, onReceiveProgress: (count, total) {
|
||||
print("${(count / total * 100).toStringAsFixed(0)}%");
|
||||
});
|
||||
final result = await ImageGallerySaverPlus.saveFile(savePath);
|
||||
return result;
|
||||
// print(result);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// // 将数据写入文件
|
||||
// Future<File> writeCounter(int counter) async {
|
||||
// final file = await _localFile;
|
||||
// // Write the file
|
||||
// return file.writeAsString('$counter');
|
||||
// }
|
||||
//
|
||||
// // 从文件中读取数据
|
||||
// Future<int> readCounter() async {
|
||||
// try {
|
||||
// final file = await _localFile;
|
||||
// // Read the file
|
||||
// String contents = await file.readAsString();
|
||||
//
|
||||
// return int.parse(contents);
|
||||
// } catch (e) {
|
||||
// // If we encounter an error, return 0
|
||||
// return 0;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
102
flutter_common/lib/upload_image/look_images_widget.dart
Normal file
102
flutter_common/lib/upload_image/look_images_widget.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:photo_view/photo_view_gallery.dart';
|
||||
|
||||
class LookImagesTool {
|
||||
static lookImages({
|
||||
required List<String> listData,
|
||||
int? currentPage,
|
||||
}) async {
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (_) {
|
||||
return LookImagesWidget(
|
||||
listData: listData,
|
||||
currentPage: currentPage,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class LookImagesWidget extends StatefulWidget {
|
||||
final List<String> listData;
|
||||
final int? currentPage;
|
||||
|
||||
const LookImagesWidget({
|
||||
super.key,
|
||||
required this.listData,
|
||||
this.currentPage,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LookImagesWidget> createState() => _LookImagesWidgetState();
|
||||
}
|
||||
|
||||
class _LookImagesWidgetState extends State<LookImagesWidget> {
|
||||
List listData = [];
|
||||
late int currentPage;
|
||||
late int initialPage = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
listData = widget.listData;
|
||||
if (widget.currentPage == null) {
|
||||
initialPage = 0;
|
||||
currentPage = 0;
|
||||
} else {
|
||||
// initialPage = 0;
|
||||
currentPage = widget.currentPage ?? 0;
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Stack(
|
||||
children: [
|
||||
Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(
|
||||
child: PhotoViewGallery.builder(
|
||||
itemCount: listData.length,
|
||||
pageController: PageController(initialPage: currentPage),
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
currentPage = index;
|
||||
});
|
||||
},
|
||||
builder: (_, index) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: NetworkImage(
|
||||
listData[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
//图片张数指示器
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 20,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"${currentPage + 1}/${listData.length}",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
274
flutter_common/lib/upload_image/ossUtil.dart
Executable file
274
flutter_common/lib/upload_image/ossUtil.dart
Executable file
@@ -0,0 +1,274 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:ui';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import "package:dio/dio.dart";
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_common/utils/toast_utils.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
|
||||
|
||||
// import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class UploadOss {
|
||||
/*
|
||||
* @params file 要上传的文件对象
|
||||
* @params rootDir 阿里云oss设置的根目录文件夹名字
|
||||
* @param fileType 文件类型例如jpg,mp4等
|
||||
* @param callback 回调函数我这里用于传cancelToken,方便后期关闭请求
|
||||
* @param onSendProgress 上传的进度事件
|
||||
*/
|
||||
|
||||
static Future<String> upload(
|
||||
String path, {
|
||||
String rootDir = "moment",
|
||||
required String fileType,
|
||||
required String oSSAccessKeyId,
|
||||
required String policy,
|
||||
required String callback,
|
||||
required String signature,
|
||||
required String ossDirectory,
|
||||
required String ossHost,
|
||||
}) async {
|
||||
// 生成oss的路径和文件名我这里目前设置的是moment/20201229/test.mp4
|
||||
String pathName = "$rootDir/${getDate()}/app-${getRandom(12)}.$fileType";
|
||||
|
||||
// 请求参数的form对象
|
||||
FormData formdata = FormData.fromMap({
|
||||
'OSSAccessKeyId': oSSAccessKeyId,
|
||||
'policy': policy,
|
||||
'callback': callback,
|
||||
'signature': signature,
|
||||
'key': '$ossDirectory$pathName',
|
||||
//上传后的文件名
|
||||
'success_action_status': '200',
|
||||
'file': MultipartFile.fromFileSync(
|
||||
path,
|
||||
contentType: fileType == 'mp4' ? null : DioMediaType("image", "jpg"),
|
||||
filename: "${getRandom(12)}.$fileType",
|
||||
),
|
||||
});
|
||||
await EasyLoading.show(
|
||||
// status: 'loading...',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
Dio dio = Dio();
|
||||
dio.options.responseType = ResponseType.plain;
|
||||
// dio.options.method = 'put';
|
||||
// dio.options.contentType = "multipart/form-data;image/jpg";
|
||||
try {
|
||||
// 发送请求
|
||||
Response response = await dio.post(
|
||||
ossHost,
|
||||
data: formdata,
|
||||
options: Options(
|
||||
contentType: "multipart/form-data;image/jpg",
|
||||
headers: {'Content-Type': 'multipart/form-data;image/jpg'},
|
||||
),
|
||||
);
|
||||
print("response ===== $response");
|
||||
EasyLoading.dismiss();
|
||||
// 成功后返回文件访问路径
|
||||
return "$ossHost/$ossDirectory$pathName";
|
||||
} on DioError catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
print("e.message ===== ${e.message}");
|
||||
print("e.data ===== ${e.response?.data}");
|
||||
// print("e.headers ===== ${e.response?.headers}");
|
||||
// print("e.extra ===== ${e.response?.extra}");
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 生成固定长度的随机字符串
|
||||
* */
|
||||
static String getRandom(int num) {
|
||||
String alphabet = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM";
|
||||
String left = "";
|
||||
for (var i = 0; i < num; i++) {
|
||||
// right = right + (min + (Random().nextInt(max - min))).toString();
|
||||
left = left + alphabet[Random().nextInt(alphabet.length)];
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
/// 获取日期
|
||||
static String getDate() {
|
||||
DateTime now = DateTime.now();
|
||||
return "${now.year}${now.month}${now.day}";
|
||||
}
|
||||
}
|
||||
|
||||
class UploadWidgetOss {
|
||||
static Future<String> uploadWidgetImage(
|
||||
GlobalKey globalKey, {
|
||||
required String oSSAccessKeyId,
|
||||
required String policy,
|
||||
required String callback,
|
||||
required String signature,
|
||||
required String ossDirectory,
|
||||
required String ossHost,
|
||||
}) async {
|
||||
///通过globalkey将Widget保存为ui.Image
|
||||
ui.Image _image = await getImageFromWidget(globalKey);
|
||||
|
||||
///异步将这张图片保存在手机内部存储目录下
|
||||
String? localImagePath = await saveImageByUIImage(_image, isEncode: false);
|
||||
|
||||
///保存完毕后关闭当前页面并将保存的图片路径返回到上一个页面
|
||||
String? url = await UploadOss.upload(
|
||||
localImagePath,
|
||||
fileType: "png",
|
||||
oSSAccessKeyId: oSSAccessKeyId,
|
||||
policy: policy,
|
||||
callback: callback,
|
||||
signature: signature,
|
||||
ossDirectory: ossDirectory,
|
||||
ossHost: ossHost,
|
||||
);
|
||||
return url;
|
||||
}
|
||||
|
||||
// 将一个Widget转为image.Image对象
|
||||
static Future<ui.Image> getImageFromWidget(GlobalKey globalKey) async {
|
||||
// globalKey为需要图像化的widget的key
|
||||
RenderRepaintBoundary? boundary =
|
||||
globalKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
|
||||
// 转换为图像
|
||||
ui.Image img = await boundary!.toImage(pixelRatio: 2);
|
||||
return img;
|
||||
}
|
||||
|
||||
///将指定的文件保存到目录空间中。
|
||||
///[image] 这里是使用的ui包下的Image
|
||||
///[picName] 保存到本地的文件(图片)文件名,如test_image
|
||||
///[endFormat]保存到本地的文件(图片)文件格式,如png,
|
||||
///[isReplace]当本地存在同名的文件(图片)时,true就是替换
|
||||
///[isEncode]对保存的文件(图片)进行编码
|
||||
/// 最终保存到本地的文件 (图片)的名称为 picName.endFormat
|
||||
static Future<String> saveImageByUIImage(ui.Image image,
|
||||
{String? picName,
|
||||
String endFormat = "png",
|
||||
bool isReplace = true,
|
||||
bool isEncode = true}) async {
|
||||
///获取本地磁盘路径
|
||||
/*
|
||||
* 在Android平台中获取的是/data/user/0/com.studyyoun.flutterbookcode/app_flutter
|
||||
* 此方法在在iOS平台获取的是Documents路径
|
||||
*/
|
||||
Directory appDocDir = await getApplicationDocumentsDirectory();
|
||||
String appDocPath = appDocDir.path;
|
||||
|
||||
///拼接目录
|
||||
if (picName == null || picName.trim().isEmpty) {
|
||||
///当用户没有指定picName时,取当前的时间命名
|
||||
picName = "${DateTime.now().millisecond.toString()}.$endFormat";
|
||||
} else {
|
||||
picName = "$picName.$endFormat";
|
||||
}
|
||||
|
||||
if (isEncode) {
|
||||
///对保存的图片名字加密
|
||||
picName = md5.convert(utf8.encode(picName)).toString();
|
||||
}
|
||||
|
||||
appDocPath = "$appDocPath/$picName";
|
||||
|
||||
///校验图片是否存在
|
||||
var file = File(appDocPath);
|
||||
bool exist = await file.exists();
|
||||
if (exist) {
|
||||
if (isReplace) {
|
||||
///如果图片存在就进行删除替换
|
||||
///如果新的图片加载失败,那么旧的图片也被删除了
|
||||
await file.delete();
|
||||
} else {
|
||||
///如果图片存在就不进行下载
|
||||
return "";
|
||||
}
|
||||
}
|
||||
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
|
||||
Uint8List pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
///将Uint8List的数据格式保存
|
||||
await File(appDocPath).writeAsBytes(pngBytes);
|
||||
|
||||
return appDocPath;
|
||||
}
|
||||
|
||||
//申请存本地相册权限
|
||||
static Future<bool> getPormiation() async {
|
||||
if (Platform.isIOS) {
|
||||
var status = await Permission.photos.status;
|
||||
if (status.isDenied) {
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.photos,
|
||||
].request();
|
||||
// saveImage(globalKey);
|
||||
}
|
||||
return status.isGranted;
|
||||
} else {
|
||||
var status = await Permission.storage.status;
|
||||
if (status.isDenied) {
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.storage,
|
||||
].request();
|
||||
}
|
||||
return status.isGranted;
|
||||
}
|
||||
}
|
||||
|
||||
///保存到相册
|
||||
static void savePhoto(GlobalKey globalKey) async {
|
||||
RenderRepaintBoundary? boundary =
|
||||
globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary?;
|
||||
|
||||
double dpr = ui.window.devicePixelRatio; // 获取当前设备的像素比
|
||||
var image = await boundary!.toImage(pixelRatio: dpr);
|
||||
// 将image转化成byte
|
||||
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
|
||||
//获取保存相册权限,如果没有,则申请改权限
|
||||
bool permition = await getPormiation();
|
||||
|
||||
var status = await Permission.photos.status;
|
||||
if (permition) {
|
||||
if (Platform.isIOS) {
|
||||
if (status.isGranted) {
|
||||
Uint8List images = byteData!.buffer.asUint8List();
|
||||
final result = await ImageGallerySaverPlus.saveImage(images,
|
||||
quality: 60, name: "hello");
|
||||
// EasyLoading.showToast("保存成功");
|
||||
ToastUtils.showToast(msg: "保存成功");
|
||||
}
|
||||
if (status.isDenied) {
|
||||
print("IOS拒绝");
|
||||
}
|
||||
} else {
|
||||
//安卓
|
||||
if (status.isGranted) {
|
||||
print("Android已授权");
|
||||
Uint8List images = byteData!.buffer.asUint8List();
|
||||
final result =
|
||||
await ImageGallerySaverPlus.saveImage(images, quality: 60);
|
||||
if (result != null) {
|
||||
// EasyLoading.showToast("保存成功");
|
||||
ToastUtils.showToast(msg: "保存成功");
|
||||
} else {
|
||||
print('error');
|
||||
// toast("保存失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//重新请求--第一次请求权限时,保存方法不会走,需要重新调一次
|
||||
savePhoto(globalKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
375
flutter_common/lib/upload_image/upload_image.dart
Normal file
375
flutter_common/lib/upload_image/upload_image.dart
Normal file
@@ -0,0 +1,375 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/customer.dart';
|
||||
import 'package:flutter_common/utils/toast_utils.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
// import 'package:images_picker/images_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import 'ossUtil.dart';
|
||||
|
||||
class UploadImages extends StatefulWidget {
|
||||
final Function? chooseImages; // List<String>
|
||||
final int? max; //最大照片数量
|
||||
final int? hNumber; //一排最大几个
|
||||
final bool? onlyShow; //仅展示,不操作
|
||||
final BoxFit? fit;
|
||||
final List<String>? imagesList; //图片数组
|
||||
final String oSSAccessKeyId;
|
||||
final String policy;
|
||||
final String callback;
|
||||
final String signature;
|
||||
final String ossDirectory;
|
||||
final String ossHost;
|
||||
final Widget carmaWidget;
|
||||
final Function? oneTap; //点击的哪一个
|
||||
final Function? deleteTap; //删除一个后
|
||||
|
||||
const UploadImages({
|
||||
super.key,
|
||||
this.chooseImages,
|
||||
this.max = 9,
|
||||
this.onlyShow = false,
|
||||
this.imagesList,
|
||||
required this.oSSAccessKeyId,
|
||||
required this.policy,
|
||||
required this.callback,
|
||||
required this.signature,
|
||||
required this.ossDirectory,
|
||||
required this.ossHost,
|
||||
required this.carmaWidget,
|
||||
this.hNumber = 3,
|
||||
this.fit,
|
||||
this.oneTap,
|
||||
this.deleteTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UploadImages> createState() => _UploadImagesState();
|
||||
}
|
||||
|
||||
class _UploadImagesState extends State<UploadImages> {
|
||||
List<String> imagesList = [];
|
||||
bool isMax = false; //是否达到最大值
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// print("widget.maxwidget.max ============ ${widget.max}");
|
||||
if (widget.imagesList != null) {
|
||||
imagesList = widget.imagesList ?? [];
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
///数组的数量
|
||||
setImageListLength() {
|
||||
// print("widget.maxwidget.max ============ ${widget.max}");
|
||||
// print("imagesList.length ============ ${imagesList.length}");
|
||||
if (imagesList.length < (widget.max?.toInt() ?? 9)) {
|
||||
isMax = false;
|
||||
return imagesList.length + 1;
|
||||
} else if (imagesList.length == (widget.max?.toInt() ?? 9)) {
|
||||
isMax = true;
|
||||
return imagesList.length;
|
||||
} else {
|
||||
isMax = true;
|
||||
return 9;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MasonryGridView.count(
|
||||
padding: const EdgeInsets.only(top: 0),
|
||||
crossAxisCount: 3,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: setImageListLength(),
|
||||
itemBuilder: (_, int index) {
|
||||
if (isMax == true) {
|
||||
String? imageUrl = imagesList[index];
|
||||
return cellDeleteWidget(
|
||||
index: index,
|
||||
child: GestureDetector(
|
||||
onTap: () => widget.oneTap?.call(index),
|
||||
child: imageNetworkWidget(
|
||||
imageUrl: imageUrl,
|
||||
index: index,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (index == setImageListLength() - 1) {
|
||||
return widget.onlyShow == true
|
||||
? const SizedBox()
|
||||
: uploadWidget(context);
|
||||
} else {
|
||||
String? imageUrl = imagesList[index];
|
||||
return cellDeleteWidget(
|
||||
index: index,
|
||||
child: imageNetworkWidget(
|
||||
imageUrl: imageUrl,
|
||||
index: index,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
mainAxisSpacing: 15,
|
||||
crossAxisSpacing: 8,
|
||||
);
|
||||
}
|
||||
|
||||
Widget imageNetworkWidget({
|
||||
required String imageUrl,
|
||||
required int index,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: () => widget.oneTap?.call(index),
|
||||
child: CustomerImagesNetworking(
|
||||
imageUrl: imageUrl,
|
||||
fit: widget.fit ?? BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///cell 总样式
|
||||
Widget cellWidget({required Widget child}) {
|
||||
return SizedBox(
|
||||
key: Key(cellKeyString()),
|
||||
width: (Get.width - 32 - 32) / (widget.hNumber ?? 3),
|
||||
height: (Get.width - 32 - 32) / (widget.hNumber ?? 3),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
///cell key 随机数+时间
|
||||
String cellKeyString({String? string}) {
|
||||
var random = Random();
|
||||
int randomNumber = random.nextInt(10000); // 生成0到10000000000000之间的随机整数
|
||||
return '$randomNumber + $string + ${DateTime.now().toString()}';
|
||||
}
|
||||
|
||||
///有删除样式的cell
|
||||
Widget cellDeleteWidget({
|
||||
required Widget child,
|
||||
required int index,
|
||||
}) {
|
||||
return Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
cellWidget(child: child),
|
||||
widget.onlyShow == true
|
||||
? const SizedBox()
|
||||
: GestureDetector(
|
||||
onTap: () {
|
||||
imagesList.removeAt(index);
|
||||
widget.deleteTap?.call(imagesList);
|
||||
setState(() {});
|
||||
},
|
||||
child: ClipOval(
|
||||
child: Container(
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 25.w,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
///上传的Widget
|
||||
Widget uploadWidget(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (Platform.isIOS) {
|
||||
await chooseCamera(
|
||||
context: context,
|
||||
max: widget.max,
|
||||
);
|
||||
} else {
|
||||
await chooseCamera(
|
||||
context: context,
|
||||
max: widget.max,
|
||||
);
|
||||
// bool b = await requestPermission(context);
|
||||
// if (b == true) {
|
||||
// await chooseCamera(
|
||||
// context: context,
|
||||
// max: widget.max,
|
||||
// );
|
||||
// }
|
||||
}
|
||||
},
|
||||
child: cellWidget(
|
||||
child: widget.carmaWidget,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 动态申请权限,需要区分android和ios,很多时候它两配置权限时各自的名称不同
|
||||
/// 此处以保存图片需要的配置为例
|
||||
Future<bool> requestPermission(context) async {
|
||||
late PermissionStatus status;
|
||||
// 1、读取系统权限的弹框
|
||||
if (Platform.isIOS) {
|
||||
status = await Permission.photosAddOnly.request();
|
||||
} else {
|
||||
status = await Permission.camera.request();
|
||||
}
|
||||
// 2、假如你点not allow后,下次点击不会在出现系统权限的弹框(系统权限的弹框只会出现一次),
|
||||
// 这时候需要你自己写一个弹框,然后去打开app权限的页面
|
||||
|
||||
if (status != PermissionStatus.granted) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return CupertinoAlertDialog(
|
||||
title: const Text('您需要去打开权限'),
|
||||
content: const Text('请打开您的相机或者相册权限'),
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
child: const Text('取消'),
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('确定'),
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
// 打开手机上该app权限的页面
|
||||
openAppSettings();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
///
|
||||
Future<void> chooseCamera({
|
||||
required BuildContext context,
|
||||
int? max,
|
||||
}) async {
|
||||
//
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return CupertinoActionSheet(
|
||||
title: const Text('上传图片'),
|
||||
message: Text('请选择上传方式\n相册最多${max ?? 9}张'),
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('拍照上传'),
|
||||
onPressed: () {
|
||||
openCamera();
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('相册'),
|
||||
onPressed: () {
|
||||
openGallery();
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
isDefaultAction: true,
|
||||
child: const Text('取消'),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
openCamera() async {
|
||||
XFile? file = await ImagePicker().pickImage(
|
||||
source: ImageSource.camera,
|
||||
);
|
||||
if (file == null) {
|
||||
// Get.back();
|
||||
} else {
|
||||
String imgPath = await saveNetworkImg(
|
||||
file,
|
||||
);
|
||||
imagesList.add(imgPath);
|
||||
widget.chooseImages?.call(imagesList);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
openGallery() async {
|
||||
int number = (widget.max ?? 9) - imagesList.length;
|
||||
// List<Media>? images =
|
||||
// await ImagesPicker.pick(count: number, pickType: PickType.image);
|
||||
List<String> list = [];
|
||||
List<XFile>? images = await ImagePicker().pickMultiImage(limit: number,);
|
||||
if (images.isEmpty != true) {
|
||||
for (var element in images) {
|
||||
String path = await saveNetworkImgGallery(
|
||||
element.path,
|
||||
);
|
||||
list.add(path);
|
||||
}
|
||||
imagesList.addAll(list);
|
||||
widget.chooseImages?.call(imagesList);
|
||||
setState(() {});
|
||||
} else {
|
||||
ToastUtils.showToast(msg: "请选择图片");
|
||||
}
|
||||
}
|
||||
|
||||
// 保存网络图片
|
||||
Future<String> saveNetworkImg(XFile file) async {
|
||||
// print("file.path ===== ${file.path}");
|
||||
String string = await UploadOss.upload(
|
||||
file.path,
|
||||
fileType: "jpg",
|
||||
oSSAccessKeyId: widget.oSSAccessKeyId,
|
||||
ossHost: widget.ossHost,
|
||||
ossDirectory: widget.ossDirectory,
|
||||
policy: widget.policy,
|
||||
callback: widget.callback,
|
||||
signature: widget.signature,
|
||||
);
|
||||
return string;
|
||||
}
|
||||
|
||||
// 保存网络图片
|
||||
Future<String> saveNetworkImgGallery(String path) async {
|
||||
String string = await UploadOss.upload(
|
||||
path,
|
||||
fileType: "jpg",
|
||||
oSSAccessKeyId: widget.oSSAccessKeyId,
|
||||
ossHost: widget.ossHost,
|
||||
ossDirectory: widget.ossDirectory,
|
||||
policy: widget.policy,
|
||||
callback: widget.callback,
|
||||
signature: widget.signature,
|
||||
);
|
||||
return string;
|
||||
}
|
||||
}
|
||||
341
flutter_common/lib/upload_image/upload_images_tool.dart
Normal file
341
flutter_common/lib/upload_image/upload_images_tool.dart
Normal file
@@ -0,0 +1,341 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_common/upload_image/ossUtil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
// import 'package:images_picker/images_picker.dart';
|
||||
// import 'package:images_picker/images_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class UploadImagesTool {
|
||||
static uploadImagesTool({
|
||||
String? oSSAccessKeyId,
|
||||
String? policy,
|
||||
String? callback,
|
||||
String? signature,
|
||||
String? ossDirectory,
|
||||
String? ossHost,
|
||||
required BuildContext context,
|
||||
Function? chooseImagesTap,
|
||||
int? max,
|
||||
bool? isVideo,
|
||||
Widget? isAddOtherWidget,
|
||||
}) async {
|
||||
await chooseCamera(
|
||||
context: context,
|
||||
max: max ?? 9,
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
isVideo: isVideo,
|
||||
isAddOtherWidget: isAddOtherWidget,
|
||||
chooseImages: (list) => chooseImagesTap?.call(list),
|
||||
);
|
||||
}
|
||||
|
||||
/// 动态申请权限,需要区分android和ios,很多时候它两配置权限时各自的名称不同
|
||||
/// 此处以保存图片需要的配置为例
|
||||
static Future<bool> requestPermission(context) async {
|
||||
late PermissionStatus status;
|
||||
// 1、读取系统权限的弹框
|
||||
if (Platform.isIOS) {
|
||||
status = await Permission.photosAddOnly.request();
|
||||
} else {
|
||||
status = await Permission.camera.request();
|
||||
}
|
||||
// 2、假如你点not allow后,下次点击不会在出现系统权限的弹框(系统权限的弹框只会出现一次),
|
||||
// 这时候需要你自己写一个弹框,然后去打开app权限的页面
|
||||
|
||||
if (status != PermissionStatus.granted) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return CupertinoAlertDialog(
|
||||
title: const Text('您需要去打开权限'),
|
||||
content: const Text('请打开您的相机或者相册权限'),
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
child: const Text('取消'),
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('确定'),
|
||||
onPressed: () {
|
||||
openAppSettings();
|
||||
Navigator.pop(ctx);
|
||||
// 打开手机上该app权限的页面
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
///
|
||||
static Future<void> chooseCamera({
|
||||
required BuildContext context,
|
||||
int? max,
|
||||
String? oSSAccessKeyId,
|
||||
String? policy,
|
||||
String? callback,
|
||||
String? signature,
|
||||
String? ossDirectory,
|
||||
String? ossHost,
|
||||
Function? chooseImages,
|
||||
bool? isVideo,
|
||||
Widget? isAddOtherWidget,
|
||||
}) async {
|
||||
//
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return isVideo == true
|
||||
? CupertinoActionSheet(
|
||||
title: const Text('上传视频'),
|
||||
message: Text('请选择视频'),
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('视频库'),
|
||||
onPressed: () {
|
||||
openGallery(
|
||||
max: max,
|
||||
isVideo: isVideo,
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
chooseImages: (list) => chooseImages?.call(list),
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
isDefaultAction: true,
|
||||
child: const Text('取消'),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
)
|
||||
: CupertinoActionSheet(
|
||||
title: const Text('上传图片'),
|
||||
message: Text('请选择上传方式\n相册最多${max ?? 9}张'),
|
||||
actions: isAddOtherWidget != null
|
||||
? <Widget>[
|
||||
isAddOtherWidget,
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('拍照上传'),
|
||||
onPressed: () {
|
||||
openCamera(
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
chooseImages: (list) =>
|
||||
chooseImages?.call(list),
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('相册'),
|
||||
onPressed: () {
|
||||
openGallery(
|
||||
max: max,
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
chooseImages: (list) =>
|
||||
chooseImages?.call(list),
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
]
|
||||
: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('拍照上传'),
|
||||
onPressed: () {
|
||||
openCamera(
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
chooseImages: (list) =>
|
||||
chooseImages?.call(list),
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('相册'),
|
||||
onPressed: () {
|
||||
openGallery(
|
||||
max: max,
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
chooseImages: (list) =>
|
||||
chooseImages?.call(list),
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
isDefaultAction: true,
|
||||
child: const Text('取消'),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
static openCamera({
|
||||
Function? chooseImages,
|
||||
String? oSSAccessKeyId,
|
||||
String? policy,
|
||||
String? callback,
|
||||
String? signature,
|
||||
String? ossDirectory,
|
||||
String? ossHost,
|
||||
}) async {
|
||||
XFile? file = await ImagePicker().pickImage(
|
||||
source: ImageSource.camera,
|
||||
);
|
||||
if (file == null) {
|
||||
Get.back();
|
||||
} else {
|
||||
String imgPath = await saveNetworkImg(
|
||||
file,
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
);
|
||||
chooseImages?.call([imgPath]);
|
||||
}
|
||||
}
|
||||
|
||||
static openGallery({
|
||||
Function? chooseImages,
|
||||
String? oSSAccessKeyId,
|
||||
String? policy,
|
||||
String? callback,
|
||||
String? signature,
|
||||
String? ossDirectory,
|
||||
String? ossHost,
|
||||
int? max,
|
||||
bool? isVideo,
|
||||
}) async {
|
||||
if (isVideo == true) {
|
||||
XFile? video = await ImagePicker().pickVideo(source: ImageSource.gallery);
|
||||
String path = await saveNetworkImgGallery(
|
||||
video?.path ?? '',
|
||||
fileType: 'mp4',
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
);
|
||||
chooseImages?.call([path]);
|
||||
print('video path ============ $path');
|
||||
} else {
|
||||
List<XFile>? images = await ImagePicker().pickMultiImage();
|
||||
List<String> list = [];
|
||||
for (var element in images) {
|
||||
String path = await saveNetworkImgGallery(
|
||||
element.path,
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
);
|
||||
list.add(path);
|
||||
}
|
||||
chooseImages?.call(list);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存网络图片
|
||||
static Future<String> saveNetworkImg(
|
||||
XFile file, {
|
||||
String? oSSAccessKeyId,
|
||||
String? policy,
|
||||
String? callback,
|
||||
String? signature,
|
||||
String? ossDirectory,
|
||||
String? ossHost,
|
||||
}) async {
|
||||
// print("file.path ===== ${file.path}");
|
||||
String string = await UploadOss.upload(
|
||||
file.path,
|
||||
fileType: "jpg",
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
);
|
||||
// print("Gallery ===string== $string");
|
||||
return string;
|
||||
}
|
||||
|
||||
// 保存网络图片
|
||||
static Future<String> saveNetworkImgGallery(
|
||||
String path, {
|
||||
String? fileType,
|
||||
String? oSSAccessKeyId,
|
||||
String? policy,
|
||||
String? callback,
|
||||
String? signature,
|
||||
String? ossDirectory,
|
||||
String? ossHost,
|
||||
}) async {
|
||||
String string = await UploadOss.upload(
|
||||
path,
|
||||
fileType: fileType ?? "jpg",
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
ossDirectory: ossDirectory ?? '',
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
);
|
||||
return string;
|
||||
}
|
||||
}
|
||||
53
flutter_common/lib/utils/PermissionUtil.dart
Normal file
53
flutter_common/lib/utils/PermissionUtil.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'dart:io';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class PermissionUtil{
|
||||
/// 安卓权限
|
||||
static List<Permission> androidPermissions = <Permission>[
|
||||
// 在这里添加需要的权限
|
||||
Permission.storage
|
||||
];
|
||||
|
||||
/// ios权限
|
||||
static List<Permission> iosPermissions = <Permission>[
|
||||
// 在这里添加需要的权限
|
||||
Permission.storage
|
||||
];
|
||||
|
||||
static Future<Map<Permission, PermissionStatus>> requestAll() async {
|
||||
if (Platform.isIOS) {
|
||||
return await iosPermissions.request();
|
||||
}
|
||||
return await androidPermissions.request();
|
||||
}
|
||||
|
||||
static Future<Map<Permission, PermissionStatus>> request(
|
||||
Permission permission) async {
|
||||
final List<Permission> permissions = <Permission>[permission];
|
||||
return await permissions.request();
|
||||
}
|
||||
|
||||
static bool isDenied(Map<Permission, PermissionStatus> result) {
|
||||
var isDenied = false;
|
||||
result.forEach((key, value) {
|
||||
if (value == PermissionStatus.denied) {
|
||||
isDenied = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
return isDenied;
|
||||
}
|
||||
|
||||
|
||||
/// 检查权限
|
||||
static Future<bool> checkGranted(Permission permission) async {
|
||||
PermissionStatus storageStatus = await permission.status;
|
||||
if (storageStatus == PermissionStatus.granted) {
|
||||
//已授权
|
||||
return true;
|
||||
} else {
|
||||
//拒绝授权
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
flutter_common/lib/utils/custom_dialog.dart
Executable file
155
flutter_common/lib/utils/custom_dialog.dart
Executable file
@@ -0,0 +1,155 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
///@Author ouyangyan
|
||||
///@Time 2023/3/1 13:43
|
||||
///自定义对话框
|
||||
class CustomDialog extends Dialog {
|
||||
final String title; //标题
|
||||
final String content; //内容
|
||||
final String cancelText; //"取消" 按钮文字
|
||||
final String confirmText; //"确定" 按钮文字
|
||||
final VoidCallback cancelCall; //取消按钮回调
|
||||
final VoidCallback confirmCall; //确定按钮回调
|
||||
final TextStyle? cancelTextStyle; //"取消" 按钮文字
|
||||
final TextStyle? confirmTextStyle; //"确定" 按钮文字
|
||||
|
||||
const CustomDialog({
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.cancelText,
|
||||
required this.confirmText,
|
||||
required this.cancelCall,
|
||||
required this.confirmCall,
|
||||
this.cancelTextStyle,
|
||||
this.confirmTextStyle,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 45),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
decoration: const ShapeDecoration(
|
||||
color: Color(0xffffffff),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Text(
|
||||
title,
|
||||
textWidthBasis: TextWidthBasis.parent,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff222222),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 20,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: Text(
|
||||
content,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff999999),
|
||||
fontSize: 13,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 5,
|
||||
),
|
||||
),
|
||||
lineWidget(height: 0.5),
|
||||
_bottomButtonGroupWidget(),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///底部按钮组
|
||||
Widget _bottomButtonGroupWidget() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: buttonWidget(
|
||||
title: cancelText,
|
||||
callback: cancelCall,
|
||||
textStyle: cancelTextStyle ??
|
||||
const TextStyle(
|
||||
color: Color(0xff888888),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
lineWidget(width: 0.5, height: 50),
|
||||
Expanded(
|
||||
child: buttonWidget(
|
||||
title: confirmText,
|
||||
callback: confirmCall,
|
||||
textStyle: confirmTextStyle ??
|
||||
const TextStyle(
|
||||
color: Color(0xffFF4D1A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
///按钮共用
|
||||
Widget buttonWidget({
|
||||
required String title,
|
||||
required TextStyle textStyle,
|
||||
required VoidCallback callback,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: callback,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: double.infinity,
|
||||
alignment: Alignment.center,
|
||||
child: Text(title, style: textStyle),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///线
|
||||
Widget lineWidget({
|
||||
double? width,
|
||||
double? height,
|
||||
EdgeInsetsGeometry? margin,
|
||||
}) {
|
||||
return Container(
|
||||
width: width,
|
||||
color: const Color(0xffe9e9e9),
|
||||
height: height,
|
||||
margin: margin,
|
||||
);
|
||||
}
|
||||
}
|
||||
250
flutter_common/lib/utils/customer.dart
Normal file
250
flutter_common/lib/utils/customer.dart
Normal file
@@ -0,0 +1,250 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
// import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
|
||||
|
||||
|
||||
///字体样式
|
||||
class CustomerTextStyle extends TextStyle {
|
||||
final Color? customerColor;
|
||||
final double? customerFontSize;
|
||||
final FontWeight? customerFontWeight;
|
||||
final TextDecoration? customerDecoration;
|
||||
final Color? customerDecorationColor;
|
||||
|
||||
const CustomerTextStyle({
|
||||
this.customerFontSize,
|
||||
this.customerDecorationColor,
|
||||
this.customerFontWeight,
|
||||
this.customerDecoration,
|
||||
this.customerColor,
|
||||
}) : super(
|
||||
color: customerColor ?? const Color(0xff333333),
|
||||
fontSize: customerFontSize,
|
||||
fontWeight: customerFontWeight,
|
||||
decoration: customerDecoration,
|
||||
decorationColor: customerDecorationColor,
|
||||
);
|
||||
}
|
||||
|
||||
///基础 Container
|
||||
class CustomerContainer extends StatelessWidget {
|
||||
final Color? color;
|
||||
final double? borderRadius;
|
||||
final Widget? child;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final double? height;
|
||||
final Gradient? gradient;
|
||||
final DecorationImage? image;
|
||||
final VoidCallback? onTap;
|
||||
final List<BoxShadow>? boxShadow;
|
||||
|
||||
const CustomerContainer({
|
||||
super.key,
|
||||
this.color,
|
||||
this.borderRadius,
|
||||
this.child,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.height,
|
||||
this.gradient,
|
||||
this.image,
|
||||
this.onTap,
|
||||
this.boxShadow,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: height,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: color ?? Colors.white,
|
||||
borderRadius: BorderRadius.circular(borderRadius?.h ?? 10.h),
|
||||
gradient: gradient,
|
||||
image: image,
|
||||
boxShadow: boxShadow,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///图片加载
|
||||
class CustomerImagesNetworking extends StatelessWidget {
|
||||
final String imageUrl;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit? fit;
|
||||
final Widget? errorWidget;
|
||||
|
||||
const CustomerImagesNetworking({
|
||||
super.key,
|
||||
required this.imageUrl,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit,
|
||||
this.errorWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Image.network(
|
||||
key: Key(imageUrl),
|
||||
imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
errorBuilder: (_, object, s) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: EdgeInsets.all((width ?? 0) / 2),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/noContainer.png',
|
||||
// width: width /2,
|
||||
// height: width /2,
|
||||
fit: fit ?? BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return imageUrl.contains('http') == true
|
||||
? FadeInImage.memoryNetwork(
|
||||
placeholder: kTransparentImage,
|
||||
width: width,
|
||||
height: height,
|
||||
image: imageUrl,
|
||||
fit: fit,
|
||||
placeholderErrorBuilder:
|
||||
(_, Object object, StackTrace? stackTrace) {
|
||||
return errorWidget ?? const SizedBox();
|
||||
},
|
||||
imageErrorBuilder: (_, Object object, StackTrace? stackTrace) {
|
||||
return errorWidget ?? const SizedBox();
|
||||
},
|
||||
)
|
||||
: errorWidget ?? const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
///money字体样式
|
||||
class CustomerMoneyText extends StatelessWidget {
|
||||
final String money;
|
||||
final double? moneyFontSize;
|
||||
final Color? moneyColor;
|
||||
final FontWeight? moneyFontWeight;
|
||||
final String? unit;
|
||||
final double? unitFontSize;
|
||||
final Color? unitColor;
|
||||
final FontWeight? unitFontWeight;
|
||||
final String? rightUnit;
|
||||
final double? rightUnitFontSize;
|
||||
final Color? rightUnitColor;
|
||||
final FontWeight? rightUnitFontWeight;
|
||||
|
||||
const CustomerMoneyText({
|
||||
super.key,
|
||||
required this.money,
|
||||
this.moneyFontSize,
|
||||
this.moneyColor,
|
||||
this.moneyFontWeight,
|
||||
this.unit,
|
||||
this.unitFontSize,
|
||||
this.unitColor,
|
||||
this.unitFontWeight,
|
||||
this.rightUnit,
|
||||
this.rightUnitFontSize,
|
||||
this.rightUnitColor,
|
||||
this.rightUnitFontWeight,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: unit ?? '',
|
||||
style: CustomerTextStyle(
|
||||
customerColor: unitColor ?? const Color(0xff333333),
|
||||
customerFontSize: unitFontSize ?? 12,
|
||||
customerFontWeight: unitFontWeight ?? FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: money,
|
||||
style: CustomerTextStyle(
|
||||
customerColor: moneyColor ?? const Color(0xff333333),
|
||||
customerFontSize: moneyFontSize ?? 20,
|
||||
customerFontWeight: moneyFontWeight ?? FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: rightUnit ?? '',
|
||||
style: CustomerTextStyle(
|
||||
customerColor: rightUnitColor ?? const Color(0xff333333),
|
||||
customerFontSize: rightUnitFontSize ?? 12,
|
||||
customerFontWeight: rightUnitFontWeight ?? FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///Html widget
|
||||
class CustomerHtmlWidget extends StatelessWidget {
|
||||
final String html;
|
||||
final Function? onTap;
|
||||
|
||||
const CustomerHtmlWidget({
|
||||
super.key,
|
||||
required this.html,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HtmlWidget(html);
|
||||
// return Html(
|
||||
// data: html,
|
||||
// extensions: [
|
||||
// TagExtension(
|
||||
// tagsToExtend: {"flutter"},
|
||||
// child: const FlutterLogo(),
|
||||
// ),
|
||||
// ],
|
||||
// style: {
|
||||
// "p.fancy": Style(
|
||||
// textAlign: TextAlign.center,
|
||||
// // padding: EdgeInsets.all(),
|
||||
// backgroundColor: Colors.grey,
|
||||
// margin: Margins(left: Margin(50, Unit.px), right: Margin.auto()),
|
||||
// width: Width(300, Unit.px),
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// },
|
||||
// );
|
||||
// return Html(
|
||||
// data: html,
|
||||
// style: const TextStyle(
|
||||
// letterSpacing: 1.5,
|
||||
// ),
|
||||
// onTapUrl: (url) {
|
||||
// return onTap?.call(url);
|
||||
// },
|
||||
// );
|
||||
}
|
||||
}
|
||||
106
flutter_common/lib/utils/customer_title_content.dart
Normal file
106
flutter_common/lib/utils/customer_title_content.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/customer.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
///上下结构的文字
|
||||
///title、unit上
|
||||
///content、unit下
|
||||
class CustomerTitleAndContentWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final FontWeight? titleFontWeight;
|
||||
final double? titleFontSize;
|
||||
final Color? titleColor;
|
||||
final String? unitTitle;
|
||||
final FontWeight? unitTitleFontWeight;
|
||||
final double? unitTitleFontSize;
|
||||
final Color? unitTitleColor;
|
||||
final String content;
|
||||
final FontWeight? contentFontWeight;
|
||||
final double? contentFontSize;
|
||||
final Color? contentColor;
|
||||
final String? unitContent;
|
||||
final FontWeight? unitContentFontWeight;
|
||||
final double? unitContentFontSize;
|
||||
final Color? unitContentColor;
|
||||
final CrossAxisAlignment? crossAxisAlignment;
|
||||
final MainAxisAlignment? mainAxisAlignment;
|
||||
final double? spaceHeight;
|
||||
|
||||
const CustomerTitleAndContentWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.titleFontWeight,
|
||||
this.titleColor,
|
||||
this.titleFontSize,
|
||||
this.contentFontWeight,
|
||||
this.contentFontSize,
|
||||
this.contentColor,
|
||||
this.crossAxisAlignment,
|
||||
this.mainAxisAlignment,
|
||||
this.unitContent,
|
||||
this.unitContentFontWeight,
|
||||
this.unitContentFontSize,
|
||||
this.unitContentColor,
|
||||
this.spaceHeight = 10,
|
||||
this.unitTitle,
|
||||
this.unitTitleFontWeight,
|
||||
this.unitTitleFontSize,
|
||||
this.unitTitleColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: mainAxisAlignment ?? MainAxisAlignment.start,
|
||||
crossAxisAlignment: crossAxisAlignment ?? CrossAxisAlignment.center,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: unitTitle,
|
||||
style: CustomerTextStyle(
|
||||
customerFontSize: unitTitleFontSize,
|
||||
customerColor: unitTitleColor ?? const Color(0xff399FFF),
|
||||
customerFontWeight: unitTitleFontWeight ?? FontWeight.normal,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: title,
|
||||
style: CustomerTextStyle(
|
||||
customerFontSize: titleFontSize,
|
||||
customerColor: titleColor ?? const Color(0xff1A1A1A),
|
||||
customerFontWeight: titleFontWeight ?? FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: spaceHeight?.h ?? 30.h),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: content,
|
||||
style: TextStyle(
|
||||
fontSize: contentFontSize,
|
||||
color: contentColor ?? const Color(0xff399FFF),
|
||||
fontWeight: contentFontWeight ?? FontWeight.normal,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: unitContent,
|
||||
style: TextStyle(
|
||||
fontSize: unitContentFontSize,
|
||||
color: unitContentColor ?? const Color(0xff399FFF),
|
||||
fontWeight: unitContentFontWeight ?? FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
368
flutter_common/lib/utils/date_utils.dart
Executable file
368
flutter_common/lib/utils/date_utils.dart
Executable file
@@ -0,0 +1,368 @@
|
||||
import 'package:flustars_flutter3/flustars_flutter3.dart';
|
||||
|
||||
enum DateTimeUtilsType {
|
||||
yearMonthDayHourMinuteSecond, // 2024-01-01 01:01:01
|
||||
yearMonthDayHourMinute, // 2024-01-01 01:01
|
||||
yearMonthDay, // 2024-01-01
|
||||
monthDay, //01-01
|
||||
monthDayPoint, //01.01
|
||||
monthDayWord, //01月01日
|
||||
yearMonthDayPoint, // 2024.01.01
|
||||
yearMonthDayHourMinutePoint, // 2024.01.01
|
||||
yearMonthDayHourMinuteSecondPoint, // 2024.01.01 01:01:01
|
||||
yearMonthDayWord, // 2024年01月01日
|
||||
yearMonthWord, // 2024年01月
|
||||
yearMonth, // 2024-01
|
||||
yearMonthPoint, // 2024.01
|
||||
hourMinuteSecond, // 01:01:01
|
||||
hourMinute, // 01:01
|
||||
monthDayLine, // 01:01
|
||||
monthWord, //01月
|
||||
monthDayHourMinutePoint, //01月
|
||||
}
|
||||
|
||||
///日期处理类
|
||||
class DateTimeUtils {
|
||||
static String dateTimeUtilsTool({
|
||||
DateTimeUtilsType? dateTimeUtilsType,
|
||||
String? dateTime,
|
||||
String nullString = '-- --',
|
||||
}) {
|
||||
if (dateTime == null || dateTime == '') return nullString;
|
||||
DateTime date = DateTime.parse(dateTime).toLocal();
|
||||
switch (dateTimeUtilsType) {
|
||||
case null:
|
||||
return nullString;
|
||||
case DateTimeUtilsType.yearMonthDayHourMinuteSecond:
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}'
|
||||
':${date.second.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDayHourMinute:
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDay:
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthDay:
|
||||
return '${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthDayPoint:
|
||||
return '${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthDayWord:
|
||||
return '${date.month.toString().padLeft(2, '0')}月'
|
||||
'${date.day.toString().padLeft(2, '0')}日';
|
||||
case DateTimeUtilsType.yearMonthDayPoint:
|
||||
return '${date.year.toString()}'
|
||||
'.${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDayHourMinuteSecondPoint:
|
||||
return '${date.year.toString()}'
|
||||
'.${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}'
|
||||
':${date.second.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDayHourMinutePoint:
|
||||
return '${date.year.toString()}'
|
||||
'.${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDayWord:
|
||||
return '${date.year.toString()}'
|
||||
'年${date.month.toString().padLeft(2, '0')}'
|
||||
'月${date.day.toString().padLeft(2, '0')}日';
|
||||
case DateTimeUtilsType.yearMonthWord:
|
||||
return '${date.year.toString()}'
|
||||
'年${date.month.toString().padLeft(2, '0')}月';
|
||||
case DateTimeUtilsType.yearMonth:
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthPoint:
|
||||
return '${date.year.toString()}'
|
||||
'.${date.month.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.hourMinuteSecond:
|
||||
return '${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}'
|
||||
':${date.second.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.hourMinute:
|
||||
return '${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthDayLine:
|
||||
return '${date.month.toString().padLeft(2, '0')}'
|
||||
'/${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthWord:
|
||||
return '${date.month.toString().padLeft(2, '0')}月';
|
||||
case DateTimeUtilsType.monthDayHourMinutePoint:
|
||||
return '${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算两个日期相差多少天?
|
||||
static int differenceTwoTimes({
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
}) {
|
||||
var startDate =
|
||||
startTime == null ? DateTime.now() : DateTime.parse(startTime);
|
||||
var endDate = endTime == null ? DateTime.now() : DateTime.parse(endTime);
|
||||
var days = endDate.difference(startDate).inDays;
|
||||
return days;
|
||||
}
|
||||
|
||||
/// 计算两个日期相差inMinutes?
|
||||
static int differenceTwoInMinutesTimes({
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
}) {
|
||||
var startDate =
|
||||
startTime == null ? DateTime.now() : DateTime.parse(startTime);
|
||||
var endDate = endTime == null ? DateTime.now() : DateTime.parse(endTime);
|
||||
var days = endDate.difference(startDate).inMinutes;
|
||||
return days;
|
||||
}
|
||||
|
||||
|
||||
/// 计算两个日期相差day huor min?
|
||||
static String differenceTwoDayHourTimes({
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
|
||||
}) {
|
||||
|
||||
String xxxxx = '0';
|
||||
var startDate =
|
||||
startTime == null ? DateTime.now() : DateTime.parse(startTime);
|
||||
var endDate = endTime == null ? DateTime.now() : DateTime.parse(endTime);
|
||||
var days = endDate.difference(startDate).inDays;
|
||||
if(days == 0){
|
||||
//days == 0 相当
|
||||
var hours = endDate.difference(startDate).inHours;
|
||||
|
||||
if(hours == 0){
|
||||
var minutes = endDate.difference(startDate).inMinutes;
|
||||
xxxxx = '$minutes分';
|
||||
}else{
|
||||
// xxxxx = hours;
|
||||
var minutes = endDate.difference(startDate).inMinutes;
|
||||
|
||||
xxxxx = '${minutes ~/ 60}小时${minutes % 60}分';
|
||||
}
|
||||
}else{
|
||||
// xxxxx = days;
|
||||
var hours = endDate.difference(startDate).inHours;
|
||||
int divisor = 24; // 除数
|
||||
int quotient = hours ~/ divisor; // 取整除法,得到商
|
||||
int remainder = hours % divisor; // 取余数,得到余数
|
||||
//
|
||||
// print('商: $quotient');
|
||||
// print('余数: $remainder');
|
||||
xxxxx = '$quotient天$remainder小时';
|
||||
}
|
||||
return xxxxx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///获取当前月份
|
||||
static String getCurrentMonth() {
|
||||
DateTime date = DateTime.now();
|
||||
return date.month.toString().padLeft(2, '0');
|
||||
}
|
||||
|
||||
///获取当前的年月日
|
||||
static String getCurrentYMD() {
|
||||
DateTime date = DateTime.now();
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
///判断时间是否在某个时间区间内
|
||||
static bool isTimeInRange({
|
||||
required DateTime startTime,
|
||||
required DateTime endTime,
|
||||
required DateTime dateTime,
|
||||
}) {
|
||||
return dateTime.isAfter(startTime) && dateTime.isBefore(endTime);
|
||||
}
|
||||
|
||||
///获取当月份
|
||||
static String get getMonth {
|
||||
return DateTime.now().month.toString();
|
||||
}
|
||||
|
||||
///获取当年份
|
||||
static String get getYear {
|
||||
return DateTime.now().year.toString();
|
||||
}
|
||||
|
||||
///获取当年份
|
||||
static String getGMTString() {
|
||||
|
||||
return DateTime.now().year.toString();
|
||||
}
|
||||
|
||||
///获取当前属于第几周
|
||||
static String getWeekDay(DateTime dateTime) {
|
||||
List<String> weekday = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||||
return weekday[dateTime.weekday - 1];
|
||||
}
|
||||
|
||||
///根据日期获取某月的第一天和最后一天
|
||||
static Map<String, dynamic> getMonthStartAndMonthEnd(
|
||||
{required DateTime dateTime}) {
|
||||
DateTime monthStart = DateTime(
|
||||
dateTime.year,
|
||||
dateTime.month,
|
||||
1,
|
||||
); // 获取本月第一天的日期时间
|
||||
DateTime monthEnd = DateTime(
|
||||
dateTime.year,
|
||||
dateTime.month + 1,
|
||||
0,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
); // 获取本月最后一天的日期时间(时间为23:59:59)
|
||||
return {
|
||||
'monthStart': monthStart,
|
||||
'monthEnd': monthEnd,
|
||||
};
|
||||
}
|
||||
//
|
||||
// DateTime monthStart = DateTime(
|
||||
// int.parse(time.toString().split('-').first),
|
||||
// int.parse(time.toString().split('-').last),
|
||||
// 1); // 获取本月第一天的日期时间
|
||||
// DateTime monthEnd = DateTime(
|
||||
// int.parse(time.toString().split('-').first),
|
||||
// int.parse(time.toString().split('-').last) + 1,
|
||||
// 0,
|
||||
// 23,
|
||||
// 59,
|
||||
// 59); // 获取本月最后一天的日期时间(时间为23:59:59)
|
||||
/// /
|
||||
}
|
||||
|
||||
class TimeMachineUtil {
|
||||
/// 获取某一年的第一个月的第一天和最后一个月的最后一天
|
||||
static getStartEndYearDate(int iYear) {
|
||||
Map mapDate = {};
|
||||
int yearNow = DateTime.now().year;
|
||||
yearNow = yearNow + iYear;
|
||||
|
||||
String newStartYear = '$yearNow-01-01';
|
||||
String newEndtYear = '${yearNow + 1}-01-00';
|
||||
|
||||
mapDate['startTime'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(turnTimestamp(newStartYear)),
|
||||
format: 'yyyy-MM-dd');
|
||||
mapDate['endTime'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(turnTimestamp(newEndtYear)),
|
||||
format: 'yyyy-MM-dd');
|
||||
|
||||
mapDate['startStamp'] = turnTimestamp(mapDate['startTime'] + ' 00:00:00');
|
||||
mapDate['endStamp'] = turnTimestamp(mapDate['endTime'] + ' 23:59:59');
|
||||
print('某一年初和年末:$mapDate');
|
||||
}
|
||||
|
||||
/// 获得当前日期 未来/过去 第某个月第一天和最后一天时间
|
||||
static Map<String, String> getMonthDate(int iMonth) {
|
||||
//获取当前日期
|
||||
var currentDate = DateTime.now();
|
||||
if (iMonth + currentDate.month > 0) {
|
||||
return timeConversion(
|
||||
iMonth + currentDate.month, (currentDate.year).toString());
|
||||
} else {
|
||||
int beforeYear = (iMonth + currentDate.month) ~/ 12;
|
||||
String yearNew = (currentDate.year + beforeYear - 1).toString();
|
||||
int monthNew = (iMonth + currentDate.month) - beforeYear * 12;
|
||||
return timeConversion(12 + monthNew, yearNew);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, String> timeConversion(int monthTime, String yearTime) {
|
||||
Map<String, String> dateMap = {};
|
||||
dateMap['startDate'] =
|
||||
'$yearTime-${monthTime < 10 ? '0$monthTime' : '$monthTime'}-01';
|
||||
//转时间戳再转时间格式 防止出错
|
||||
dateMap['startDate'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
turnTimestamp(dateMap['startDate'] ?? "")),
|
||||
format: 'yyyy-MM-dd');
|
||||
//某个月结束时间,转时间戳再转
|
||||
String endMonth =
|
||||
'$yearTime-${(monthTime + 1) < 10 ? '0${monthTime + 1}' : (monthTime + 1)}-00';
|
||||
var endMonthTimeStamp = turnTimestamp(endMonth);
|
||||
endMonth = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(endMonthTimeStamp),
|
||||
format: 'yyyy-MM-dd');
|
||||
dateMap['endDate'] = endMonth;
|
||||
//这里是为了公司后台接口 需加时间段的时间戳 但不显示在格式化实践中
|
||||
dateMap['startDateStamp'] =
|
||||
turnTimestamp('${dateMap['startDate']} 00:00:00').toString();
|
||||
dateMap['endDateStamp'] =
|
||||
turnTimestamp('${dateMap['endDate']} 23:59:59').toString();
|
||||
// print('过去未来某个月初月末:$dateMap');
|
||||
return dateMap;
|
||||
}
|
||||
|
||||
/// 转时间戳
|
||||
static int turnTimestamp(String timestamp) {
|
||||
return DateTime.parse(timestamp).millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
/// 当前时间 过去/未来 某个周的周一和周日
|
||||
static Map<String, String> getWeeksDate(int weeks) {
|
||||
Map<String, String> mapTime = {};
|
||||
DateTime now = DateTime.now();
|
||||
int weekday = now.weekday; //今天周几
|
||||
|
||||
var sunDay = getTimestampLatest(false, 7 - weekday + weeks * 7); //周末
|
||||
var monDay = getTimestampLatest(true, -weekday + 1 + weeks * 7); //周一
|
||||
|
||||
mapTime['monDay'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(monDay),
|
||||
format: 'yyyy-MM-dd'); //周一 时间格式化
|
||||
mapTime['sunDay'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(sunDay),
|
||||
format: 'yyyy-MM-dd'); //周一 时间格式化
|
||||
mapTime['monDayStamp'] = '$monDay'; //周一 时间戳
|
||||
mapTime['sunDayStamp'] = '$sunDay'; //周日 时间戳
|
||||
// print('某个周的周一和周日:$mapTime');
|
||||
return mapTime;
|
||||
}
|
||||
|
||||
/// phase : 是零点还是23:59:59
|
||||
static int getTimestampLatest(bool phase, int day) {
|
||||
String newHours;
|
||||
DateTime now = DateTime.now();
|
||||
DateTime sixtyDaysFromNow = now.add(Duration(days: day));
|
||||
String formattedDate =
|
||||
DateUtil.formatDate(sixtyDaysFromNow, format: 'yyyy-MM-dd');
|
||||
if (phase) {
|
||||
newHours = '$formattedDate 00:00:00';
|
||||
} else {
|
||||
newHours = '$formattedDate 23:59:59';
|
||||
}
|
||||
|
||||
DateTime newDate = DateTime.parse(newHours);
|
||||
// String newFormattedDate =
|
||||
// DateUtil.formatDate(newDate, format: 'yyyy-MM-dd HH:mm:ss');
|
||||
int timeStamp = newDate.millisecondsSinceEpoch;
|
||||
// print('时间' + newFormattedDate);
|
||||
return timeStamp;
|
||||
}
|
||||
}
|
||||
47
flutter_common/lib/utils/diolog_alter_view.dart
Normal file
47
flutter_common/lib/utils/diolog_alter_view.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///弹窗widget
|
||||
///ctx showDialog context
|
||||
///padding 位置
|
||||
class ShowDialogWidget extends StatelessWidget {
|
||||
final BuildContext? ctx;
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
const ShowDialogWidget({
|
||||
super.key,
|
||||
this.ctx,
|
||||
required this.child,
|
||||
required this.padding,
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: ctx == null ? null : () => Navigator.pop(ctx!),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
126
flutter_common/lib/utils/file/customer_file.dart
Normal file
126
flutter_common/lib/utils/file/customer_file.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/toast_utils.dart';
|
||||
import 'play/video_play_page.dart';
|
||||
|
||||
enum OpenType {
|
||||
file, //文件
|
||||
image, //图片
|
||||
video, //视频
|
||||
url, //网址
|
||||
}
|
||||
|
||||
class CustomerFile {
|
||||
static Future<void> openTypeFile({
|
||||
required OpenType type,
|
||||
required BuildContext context,
|
||||
required int isLock,
|
||||
required String filePath,
|
||||
String? fileName,
|
||||
}) async {
|
||||
if (filePath == "" || filePath == null) {
|
||||
ToastUtils.showToast(msg: "暂无内容");
|
||||
} else {
|
||||
if (type == OpenType.file) {
|
||||
CustomerAction.openFileAction(
|
||||
context: context,
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
);
|
||||
}
|
||||
if (type == OpenType.video) {
|
||||
CustomerAction.openVideoAction(
|
||||
context: context,
|
||||
videoUrl: filePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///公共方法
|
||||
class CustomerAction {
|
||||
///打开文件
|
||||
static Future<void> openFileAction({
|
||||
required BuildContext context,
|
||||
required String filePath,
|
||||
String? fileName,
|
||||
}) async {
|
||||
if (filePath == "") {
|
||||
ToastUtils.showToast(msg: "暂无文件");
|
||||
} else {
|
||||
if (filePath.contains("http")) {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (BuildContext ctx) {
|
||||
//
|
||||
// // return CustomerPDFPage(
|
||||
// // filePath: filePath,
|
||||
// // );
|
||||
// });
|
||||
} else {
|
||||
ToastUtils.showToast(msg: "地址无效");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static _dealFileName(String filePath) {
|
||||
return "${DateTime.now()}.${filePath.split(".").last}";
|
||||
}
|
||||
|
||||
///打开视频
|
||||
static Future<void> openVideoAction({
|
||||
required BuildContext context,
|
||||
required String videoUrl,
|
||||
}) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: VideoPlayPage(
|
||||
videoUrl: videoUrl,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
///打开网页
|
||||
// static Future<void> openWebview({
|
||||
// required BuildContext context,
|
||||
// required String url,
|
||||
// }) async {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (BuildContext ctx) {
|
||||
// return CustomerWebView(url: url);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
static Future<void> openTypeFile({
|
||||
required OpenType type,
|
||||
required BuildContext context,
|
||||
required int isLock,
|
||||
required String filePath,
|
||||
String? fileName,
|
||||
}) async {
|
||||
if (filePath == "" || filePath == null) {
|
||||
ToastUtils.showToast(msg: "暂无内容");
|
||||
} else {
|
||||
if (type == OpenType.file) {
|
||||
CustomerAction.openFileAction(
|
||||
context: context,
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
);
|
||||
}
|
||||
if (type == OpenType.video) {
|
||||
CustomerAction.openVideoAction(
|
||||
context: context,
|
||||
videoUrl: filePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
flutter_common/lib/utils/file/play/video_play_page.dart
Executable file
87
flutter_common/lib/utils/file/play/video_play_page.dart
Executable file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/file/video/lib/chewie.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
|
||||
|
||||
|
||||
class VideoPlayPage extends StatefulWidget {
|
||||
final String? videoUrl;
|
||||
const VideoPlayPage({super.key, this.videoUrl});
|
||||
|
||||
@override
|
||||
State<VideoPlayPage> createState() => _VideoPlayPageState();
|
||||
}
|
||||
|
||||
class _VideoPlayPageState extends State<VideoPlayPage> {
|
||||
var videoController = VideoPlayerController.network("");
|
||||
var chewieController;
|
||||
//监听视频是否在播放
|
||||
bool isPlaying = false;
|
||||
//视频地址 https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4
|
||||
// String videoUrl =
|
||||
// "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4";
|
||||
// String videoUrl =
|
||||
// "http://static.dongmenshijing.com/upload/20230720/cf742bbf-8a96-46d5-b187-edeffa168556.mp4";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Uri url = Uri.parse(videoUrl);
|
||||
// videoController = VideoPlayerController.networkUrl(url)
|
||||
// ..initialize().then((value) {
|
||||
// setState(() {});
|
||||
// });
|
||||
// videoController = VideoPlayerController.network(
|
||||
// videoUrl,
|
||||
// videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
// )..initialize().then((value) {
|
||||
// setState(() {});
|
||||
// });
|
||||
// videoController.addListener(() {
|
||||
// setState(() {});
|
||||
// });
|
||||
initVideoController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
///初始化
|
||||
initVideoController({String? url}) {
|
||||
videoController = VideoPlayerController.network(
|
||||
widget.videoUrl ?? "",
|
||||
)..initialize().then((_) {
|
||||
setState(() {
|
||||
chewieController = ChewieController(
|
||||
// autoInitialize: true,
|
||||
fullScreenByDefault: true,
|
||||
videoPlayerController: videoController,
|
||||
allowFullScreen: true,
|
||||
// aspectRatio: MediaQuery.of(context).size.height /
|
||||
// MediaQuery.of(context).size.width,
|
||||
autoPlay: true,
|
||||
looping: false,
|
||||
);
|
||||
});
|
||||
});
|
||||
videoController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
videoController.dispose();
|
||||
chewieController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return chewieController == null
|
||||
? Container()
|
||||
: Chewie(
|
||||
controller: chewieController,
|
||||
);
|
||||
}
|
||||
}
|
||||
8
flutter_common/lib/utils/file/video/lib/chewie.dart
Executable file
8
flutter_common/lib/utils/file/video/lib/chewie.dart
Executable file
@@ -0,0 +1,8 @@
|
||||
library chewie;
|
||||
|
||||
export 'src/chewie_player.dart';
|
||||
export 'src/chewie_progress_colors.dart';
|
||||
export 'src/cupertino/cupertino_controls.dart';
|
||||
export 'src/material/material_controls.dart';
|
||||
export 'src/material/material_desktop_controls.dart';
|
||||
export 'src/models/index.dart';
|
||||
57
flutter_common/lib/utils/file/video/lib/src/animated_play_pause.dart
Executable file
57
flutter_common/lib/utils/file/video/lib/src/animated_play_pause.dart
Executable file
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that animates implicitly between a play and a pause icon.
|
||||
class AnimatedPlayPause extends StatefulWidget {
|
||||
const AnimatedPlayPause({
|
||||
Key? key,
|
||||
required this.playing,
|
||||
this.size,
|
||||
this.color,
|
||||
}) : super(key: key);
|
||||
|
||||
final double? size;
|
||||
final bool playing;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => AnimatedPlayPauseState();
|
||||
}
|
||||
|
||||
class AnimatedPlayPauseState extends State<AnimatedPlayPause>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final animationController = AnimationController(
|
||||
vsync: this,
|
||||
value: widget.playing ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AnimatedPlayPause oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.playing != oldWidget.playing) {
|
||||
if (widget.playing) {
|
||||
animationController.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: AnimatedIcon(
|
||||
color: widget.color,
|
||||
size: widget.size,
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: animationController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
flutter_common/lib/utils/file/video/lib/src/center_play_button.dart
Executable file
56
flutter_common/lib/utils/file/video/lib/src/center_play_button.dart
Executable file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'animated_play_pause.dart';
|
||||
|
||||
class CenterPlayButton extends StatelessWidget {
|
||||
const CenterPlayButton({
|
||||
Key? key,
|
||||
required this.backgroundColor,
|
||||
this.iconColor,
|
||||
required this.show,
|
||||
required this.isPlaying,
|
||||
required this.isFinished,
|
||||
this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
final Color backgroundColor;
|
||||
final Color? iconColor;
|
||||
final bool show;
|
||||
final bool isPlaying;
|
||||
final bool isFinished;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: UnconstrainedBox(
|
||||
child: AnimatedOpacity(
|
||||
opacity: show ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
// Always set the iconSize on the IconButton, not on the Icon itself:
|
||||
// https://github.com/flutter/flutter/issues/52980
|
||||
child: IconButton(
|
||||
iconSize: 32,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
icon: isFinished
|
||||
? Icon(Icons.replay, color: iconColor)
|
||||
: AnimatedPlayPause(
|
||||
color: iconColor,
|
||||
playing: isPlaying,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
633
flutter_common/lib/utils/file/video/lib/src/chewie_player.dart
Executable file
633
flutter_common/lib/utils/file/video/lib/src/chewie_player.dart
Executable file
@@ -0,0 +1,633 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
// import 'package:wakelock/wakelock.dart';
|
||||
|
||||
import '../chewie.dart';
|
||||
import 'notifiers/index.dart';
|
||||
import 'player_with_controls.dart';
|
||||
|
||||
typedef ChewieRoutePageBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
ChewieControllerProvider controllerProvider,
|
||||
);
|
||||
|
||||
/// A Video Player with Material and Cupertino skins.
|
||||
///
|
||||
/// `video_player` is pretty low level. Chewie wraps it in a friendly skin to
|
||||
/// make it easy to use!
|
||||
class Chewie extends StatefulWidget {
|
||||
const Chewie({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The [ChewieController]
|
||||
final ChewieController controller;
|
||||
|
||||
@override
|
||||
ChewieState createState() {
|
||||
return ChewieState();
|
||||
}
|
||||
}
|
||||
|
||||
class ChewieState extends State<Chewie> {
|
||||
bool _isFullScreen = false;
|
||||
|
||||
bool get isControllerFullScreen => widget.controller.isFullScreen;
|
||||
late PlayerNotifier notifier;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(listener);
|
||||
notifier = PlayerNotifier.init();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(listener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Chewie oldWidget) {
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
widget.controller.addListener(listener);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (_isFullScreen != isControllerFullScreen) {
|
||||
widget.controller._isFullScreen = _isFullScreen;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> listener() async {
|
||||
if (isControllerFullScreen && !_isFullScreen) {
|
||||
_isFullScreen = isControllerFullScreen;
|
||||
await _pushFullScreenWidget(context);
|
||||
} else if (_isFullScreen) {
|
||||
Navigator.of(
|
||||
context,
|
||||
rootNavigator: widget.controller.useRootNavigator,
|
||||
).pop();
|
||||
_isFullScreen = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChewieControllerProvider(
|
||||
controller: widget.controller,
|
||||
child: ChangeNotifierProvider<PlayerNotifier>.value(
|
||||
value: notifier,
|
||||
builder: (context, w) => const PlayerWithControls(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFullScreenVideo(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
ChewieControllerProvider controllerProvider,
|
||||
) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Container(
|
||||
alignment: Alignment.center,
|
||||
color: Colors.black,
|
||||
child: controllerProvider,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AnimatedWidget _defaultRoutePageBuilder(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
ChewieControllerProvider controllerProvider,
|
||||
) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return _buildFullScreenVideo(context, animation, controllerProvider);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fullScreenRoutePageBuilder(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
final controllerProvider = ChewieControllerProvider(
|
||||
controller: widget.controller,
|
||||
child: ChangeNotifierProvider<PlayerNotifier>.value(
|
||||
value: notifier,
|
||||
builder: (context, w) => const PlayerWithControls(),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.controller.routePageBuilder == null) {
|
||||
return _defaultRoutePageBuilder(
|
||||
context,
|
||||
animation,
|
||||
secondaryAnimation,
|
||||
controllerProvider,
|
||||
);
|
||||
}
|
||||
return widget.controller.routePageBuilder!(
|
||||
context,
|
||||
animation,
|
||||
secondaryAnimation,
|
||||
controllerProvider,
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> _pushFullScreenWidget(BuildContext context) async {
|
||||
final TransitionRoute<void> route = PageRouteBuilder<void>(
|
||||
pageBuilder: _fullScreenRoutePageBuilder,
|
||||
);
|
||||
|
||||
onEnterFullScreen();
|
||||
|
||||
if (!widget.controller.allowedScreenSleep) {
|
||||
WakelockPlus.enable;
|
||||
}
|
||||
|
||||
await Navigator.of(
|
||||
context,
|
||||
rootNavigator: widget.controller.useRootNavigator,
|
||||
).push(route);
|
||||
_isFullScreen = false;
|
||||
widget.controller.exitFullScreen();
|
||||
|
||||
// The wakelock plugins checks whether it needs to perform an action internally,
|
||||
// so we do not need to check Wakelock.isEnabled.
|
||||
WakelockPlus.disable();
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: widget.controller.systemOverlaysAfterFullScreen,
|
||||
);
|
||||
SystemChrome.setPreferredOrientations(
|
||||
widget.controller.deviceOrientationsAfterFullScreen,
|
||||
);
|
||||
}
|
||||
|
||||
void onEnterFullScreen() {
|
||||
final videoWidth = widget.controller.videoPlayerController.value.size.width;
|
||||
final videoHeight =
|
||||
widget.controller.videoPlayerController.value.size.height;
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
|
||||
// if (widget.controller.systemOverlaysOnEnterFullScreen != null) {
|
||||
// /// Optional user preferred settings
|
||||
// SystemChrome.setEnabledSystemUIMode(
|
||||
// SystemUiMode.manual,
|
||||
// overlays: widget.controller.systemOverlaysOnEnterFullScreen,
|
||||
// );
|
||||
// } else {
|
||||
// /// Default behavior
|
||||
// SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
|
||||
// }
|
||||
|
||||
if (widget.controller.deviceOrientationsOnEnterFullScreen != null) {
|
||||
/// Optional user preferred settings
|
||||
SystemChrome.setPreferredOrientations(
|
||||
widget.controller.deviceOrientationsOnEnterFullScreen!,
|
||||
);
|
||||
} else {
|
||||
final isLandscapeVideo = videoWidth > videoHeight;
|
||||
final isPortraitVideo = videoWidth < videoHeight;
|
||||
|
||||
/// Default behavior
|
||||
/// Video w > h means we force landscape
|
||||
if (isLandscapeVideo) {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Video h > w means we force portrait
|
||||
else if (isPortraitVideo) {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Otherwise if h == w (square video)
|
||||
else {
|
||||
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The ChewieController is used to configure and drive the Chewie Player
|
||||
/// Widgets. It provides methods to control playback, such as [pause] and
|
||||
/// [play], as well as methods that control the visual appearance of the player,
|
||||
/// such as [enterFullScreen] or [exitFullScreen].
|
||||
///
|
||||
/// In addition, you can listen to the ChewieController for presentational
|
||||
/// changes, such as entering and exiting full screen mode. To listen for
|
||||
/// changes to the playback, such as a change to the seek position of the
|
||||
/// player, please use the standard information provided by the
|
||||
/// `VideoPlayerController`.
|
||||
class ChewieController extends ChangeNotifier {
|
||||
ChewieController({
|
||||
required this.videoPlayerController,
|
||||
this.optionsTranslation,
|
||||
this.aspectRatio,
|
||||
this.autoInitialize = false,
|
||||
this.autoPlay = false,
|
||||
this.startAt,
|
||||
this.looping = false,
|
||||
this.fullScreenByDefault = false,
|
||||
this.cupertinoProgressColors,
|
||||
this.materialProgressColors,
|
||||
this.placeholder,
|
||||
this.overlay,
|
||||
this.showControlsOnInitialize = true,
|
||||
this.showOptions = true,
|
||||
this.optionsBuilder,
|
||||
this.additionalOptions,
|
||||
this.showControls = true,
|
||||
this.transformationController,
|
||||
this.zoomAndPan = false,
|
||||
this.maxScale = 2.5,
|
||||
this.subtitle,
|
||||
this.subtitleBuilder,
|
||||
this.customControls,
|
||||
this.errorBuilder,
|
||||
this.allowedScreenSleep = true,
|
||||
this.isLive = false,
|
||||
this.allowFullScreen = true,
|
||||
this.allowMuting = true,
|
||||
this.allowPlaybackSpeedChanging = true,
|
||||
this.useRootNavigator = true,
|
||||
this.playbackSpeeds = const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
|
||||
this.systemOverlaysOnEnterFullScreen,
|
||||
this.deviceOrientationsOnEnterFullScreen,
|
||||
this.systemOverlaysAfterFullScreen = SystemUiOverlay.values,
|
||||
this.deviceOrientationsAfterFullScreen = DeviceOrientation.values,
|
||||
this.routePageBuilder,
|
||||
this.progressIndicatorDelay,
|
||||
this.hideControlsTimer = defaultHideControlsTimer,
|
||||
}) : assert(
|
||||
playbackSpeeds.every((speed) => speed > 0),
|
||||
'The playbackSpeeds values must all be greater than 0',
|
||||
) {
|
||||
_initialize();
|
||||
}
|
||||
|
||||
ChewieController copyWith({
|
||||
VideoPlayerController? videoPlayerController,
|
||||
OptionsTranslation? optionsTranslation,
|
||||
double? aspectRatio,
|
||||
bool? autoInitialize,
|
||||
bool? autoPlay,
|
||||
Duration? startAt,
|
||||
bool? looping,
|
||||
bool? fullScreenByDefault,
|
||||
ChewieProgressColors? cupertinoProgressColors,
|
||||
ChewieProgressColors? materialProgressColors,
|
||||
Widget? placeholder,
|
||||
Widget? overlay,
|
||||
bool? showControlsOnInitialize,
|
||||
bool? showOptions,
|
||||
Future<void> Function(BuildContext, List<OptionItem>)? optionsBuilder,
|
||||
List<OptionItem> Function(BuildContext)? additionalOptions,
|
||||
bool? showControls,
|
||||
TransformationController? transformationController,
|
||||
bool? zoomAndPan,
|
||||
double? maxScale,
|
||||
Subtitles? subtitle,
|
||||
Widget Function(BuildContext, dynamic)? subtitleBuilder,
|
||||
Widget? customControls,
|
||||
Widget Function(BuildContext, String)? errorBuilder,
|
||||
bool? allowedScreenSleep,
|
||||
bool? isLive,
|
||||
bool? allowFullScreen,
|
||||
bool? allowMuting,
|
||||
bool? allowPlaybackSpeedChanging,
|
||||
bool? useRootNavigator,
|
||||
Duration? hideControlsTimer,
|
||||
List<double>? playbackSpeeds,
|
||||
List<SystemUiOverlay>? systemOverlaysOnEnterFullScreen,
|
||||
List<DeviceOrientation>? deviceOrientationsOnEnterFullScreen,
|
||||
List<SystemUiOverlay>? systemOverlaysAfterFullScreen,
|
||||
List<DeviceOrientation>? deviceOrientationsAfterFullScreen,
|
||||
Duration? progressIndicatorDelay,
|
||||
Widget Function(
|
||||
BuildContext,
|
||||
Animation<double>,
|
||||
Animation<double>,
|
||||
ChewieControllerProvider,
|
||||
)? routePageBuilder,
|
||||
}) {
|
||||
return ChewieController(
|
||||
videoPlayerController:
|
||||
videoPlayerController ?? this.videoPlayerController,
|
||||
optionsTranslation: optionsTranslation ?? this.optionsTranslation,
|
||||
aspectRatio: aspectRatio ?? this.aspectRatio,
|
||||
autoInitialize: autoInitialize ?? this.autoInitialize,
|
||||
autoPlay: autoPlay ?? this.autoPlay,
|
||||
startAt: startAt ?? this.startAt,
|
||||
looping: looping ?? this.looping,
|
||||
fullScreenByDefault: fullScreenByDefault ?? this.fullScreenByDefault,
|
||||
cupertinoProgressColors:
|
||||
cupertinoProgressColors ?? this.cupertinoProgressColors,
|
||||
materialProgressColors:
|
||||
materialProgressColors ?? this.materialProgressColors,
|
||||
placeholder: placeholder ?? this.placeholder,
|
||||
overlay: overlay ?? this.overlay,
|
||||
showControlsOnInitialize:
|
||||
showControlsOnInitialize ?? this.showControlsOnInitialize,
|
||||
showOptions: showOptions ?? this.showOptions,
|
||||
optionsBuilder: optionsBuilder ?? this.optionsBuilder,
|
||||
additionalOptions: additionalOptions ?? this.additionalOptions,
|
||||
showControls: showControls ?? this.showControls,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
subtitleBuilder: subtitleBuilder ?? this.subtitleBuilder,
|
||||
customControls: customControls ?? this.customControls,
|
||||
errorBuilder: errorBuilder ?? this.errorBuilder,
|
||||
allowedScreenSleep: allowedScreenSleep ?? this.allowedScreenSleep,
|
||||
isLive: isLive ?? this.isLive,
|
||||
allowFullScreen: allowFullScreen ?? this.allowFullScreen,
|
||||
allowMuting: allowMuting ?? this.allowMuting,
|
||||
allowPlaybackSpeedChanging:
|
||||
allowPlaybackSpeedChanging ?? this.allowPlaybackSpeedChanging,
|
||||
useRootNavigator: useRootNavigator ?? this.useRootNavigator,
|
||||
playbackSpeeds: playbackSpeeds ?? this.playbackSpeeds,
|
||||
systemOverlaysOnEnterFullScreen: systemOverlaysOnEnterFullScreen ??
|
||||
this.systemOverlaysOnEnterFullScreen,
|
||||
deviceOrientationsOnEnterFullScreen:
|
||||
deviceOrientationsOnEnterFullScreen ??
|
||||
this.deviceOrientationsOnEnterFullScreen,
|
||||
systemOverlaysAfterFullScreen:
|
||||
systemOverlaysAfterFullScreen ?? this.systemOverlaysAfterFullScreen,
|
||||
deviceOrientationsAfterFullScreen: deviceOrientationsAfterFullScreen ??
|
||||
this.deviceOrientationsAfterFullScreen,
|
||||
routePageBuilder: routePageBuilder ?? this.routePageBuilder,
|
||||
hideControlsTimer: hideControlsTimer ?? this.hideControlsTimer,
|
||||
progressIndicatorDelay:
|
||||
progressIndicatorDelay ?? this.progressIndicatorDelay,
|
||||
);
|
||||
}
|
||||
|
||||
static const defaultHideControlsTimer = Duration(seconds: 3);
|
||||
|
||||
/// If false, the options button in MaterialUI and MaterialDesktopUI
|
||||
/// won't be shown.
|
||||
final bool showOptions;
|
||||
|
||||
/// Pass your translations for the options like:
|
||||
/// - PlaybackSpeed
|
||||
/// - Subtitles
|
||||
/// - Cancel
|
||||
///
|
||||
/// Buttons
|
||||
///
|
||||
/// These are required for the default `OptionItem`'s
|
||||
final OptionsTranslation? optionsTranslation;
|
||||
|
||||
/// Build your own options with default chewieOptions shiped through
|
||||
/// the builder method. Just add your own options to the Widget
|
||||
/// you'll build. If you want to hide the chewieOptions, just leave them
|
||||
/// out from your Widget.
|
||||
final Future<void> Function(
|
||||
BuildContext context,
|
||||
List<OptionItem> chewieOptions,
|
||||
)? optionsBuilder;
|
||||
|
||||
/// Add your own additional options on top of chewie options
|
||||
final List<OptionItem> Function(BuildContext context)? additionalOptions;
|
||||
|
||||
/// Define here your own Widget on how your n'th subtitle will look like
|
||||
Widget Function(BuildContext context, dynamic subtitle)? subtitleBuilder;
|
||||
|
||||
/// Add a List of Subtitles here in `Subtitles.subtitle`
|
||||
Subtitles? subtitle;
|
||||
|
||||
/// The controller for the video you want to play
|
||||
final VideoPlayerController videoPlayerController;
|
||||
|
||||
/// Initialize the Video on Startup. This will prep the video for playback.
|
||||
final bool autoInitialize;
|
||||
|
||||
/// Play the video as soon as it's displayed
|
||||
final bool autoPlay;
|
||||
|
||||
/// Start video at a certain position
|
||||
final Duration? startAt;
|
||||
|
||||
/// Whether or not the video should loop
|
||||
final bool looping;
|
||||
|
||||
/// Wether or not to show the controls when initializing the widget.
|
||||
final bool showControlsOnInitialize;
|
||||
|
||||
/// Whether or not to show the controls at all
|
||||
final bool showControls;
|
||||
|
||||
/// Controller to pass into the [InteractiveViewer] component
|
||||
final TransformationController? transformationController;
|
||||
|
||||
/// Whether or not to allow zooming and panning
|
||||
final bool zoomAndPan;
|
||||
|
||||
/// Max scale when zooming
|
||||
final double maxScale;
|
||||
|
||||
/// Defines customised controls. Check [MaterialControls] or
|
||||
/// [CupertinoControls] for reference.
|
||||
final Widget? customControls;
|
||||
|
||||
/// When the video playback runs into an error, you can build a custom
|
||||
/// error message.
|
||||
final Widget Function(BuildContext context, String errorMessage)?
|
||||
errorBuilder;
|
||||
|
||||
/// The Aspect Ratio of the Video. Important to get the correct size of the
|
||||
/// video!
|
||||
///
|
||||
/// Will fallback to fitting within the space allowed.
|
||||
final double? aspectRatio;
|
||||
|
||||
/// The colors to use for controls on iOS. By default, the iOS player uses
|
||||
/// colors sampled from the original iOS 11 designs.
|
||||
final ChewieProgressColors? cupertinoProgressColors;
|
||||
|
||||
/// The colors to use for the Material Progress Bar. By default, the Material
|
||||
/// player uses the colors from your Theme.
|
||||
final ChewieProgressColors? materialProgressColors;
|
||||
|
||||
/// The placeholder is displayed underneath the Video before it is initialized
|
||||
/// or played.
|
||||
final Widget? placeholder;
|
||||
|
||||
/// A widget which is placed between the video and the controls
|
||||
final Widget? overlay;
|
||||
|
||||
/// Defines if the player will start in fullscreen when play is pressed
|
||||
final bool fullScreenByDefault;
|
||||
|
||||
/// Defines if the player will sleep in fullscreen or not
|
||||
final bool allowedScreenSleep;
|
||||
|
||||
/// Defines if the controls should be shown for live stream video
|
||||
final bool isLive;
|
||||
|
||||
/// Defines if the fullscreen control should be shown
|
||||
final bool allowFullScreen;
|
||||
|
||||
/// Defines if the mute control should be shown
|
||||
final bool allowMuting;
|
||||
|
||||
/// Defines if the playback speed control should be shown
|
||||
final bool allowPlaybackSpeedChanging;
|
||||
|
||||
/// Defines if push/pop navigations use the rootNavigator
|
||||
final bool useRootNavigator;
|
||||
|
||||
/// Defines the [Duration] before the video controls are hidden. By default, this is set to three seconds.
|
||||
final Duration hideControlsTimer;
|
||||
|
||||
/// Defines the set of allowed playback speeds user can change
|
||||
final List<double> playbackSpeeds;
|
||||
|
||||
/// Defines the system overlays visible on entering fullscreen
|
||||
final List<SystemUiOverlay>? systemOverlaysOnEnterFullScreen;
|
||||
|
||||
/// Defines the set of allowed device orientations on entering fullscreen
|
||||
final List<DeviceOrientation>? deviceOrientationsOnEnterFullScreen;
|
||||
|
||||
/// Defines the system overlays visible after exiting fullscreen
|
||||
final List<SystemUiOverlay> systemOverlaysAfterFullScreen;
|
||||
|
||||
/// Defines the set of allowed device orientations after exiting fullscreen
|
||||
final List<DeviceOrientation> deviceOrientationsAfterFullScreen;
|
||||
|
||||
/// Defines a custom RoutePageBuilder for the fullscreen
|
||||
final ChewieRoutePageBuilder? routePageBuilder;
|
||||
|
||||
/// Defines a delay in milliseconds between entering buffering state and displaying the loading spinner. Set null (default) to disable it.
|
||||
final Duration? progressIndicatorDelay;
|
||||
|
||||
static ChewieController of(BuildContext context) {
|
||||
final chewieControllerProvider =
|
||||
context.dependOnInheritedWidgetOfExactType<ChewieControllerProvider>()!;
|
||||
|
||||
return chewieControllerProvider.controller;
|
||||
}
|
||||
|
||||
bool _isFullScreen = false;
|
||||
|
||||
bool get isFullScreen => _isFullScreen;
|
||||
|
||||
bool get isPlaying => videoPlayerController.value.isPlaying;
|
||||
|
||||
Future _initialize() async {
|
||||
await videoPlayerController.setLooping(looping);
|
||||
|
||||
if ((autoInitialize || autoPlay) &&
|
||||
!videoPlayerController.value.isInitialized) {
|
||||
await videoPlayerController.initialize();
|
||||
}
|
||||
|
||||
if (autoPlay) {
|
||||
if (fullScreenByDefault) {
|
||||
enterFullScreen();
|
||||
}
|
||||
|
||||
await videoPlayerController.play();
|
||||
}
|
||||
|
||||
if (startAt != null) {
|
||||
await videoPlayerController.seekTo(startAt!);
|
||||
}
|
||||
|
||||
if (fullScreenByDefault) {
|
||||
videoPlayerController.addListener(_fullScreenListener);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fullScreenListener() async {
|
||||
if (videoPlayerController.value.isPlaying && !_isFullScreen) {
|
||||
enterFullScreen();
|
||||
videoPlayerController.removeListener(_fullScreenListener);
|
||||
}
|
||||
}
|
||||
|
||||
void enterFullScreen() {
|
||||
_isFullScreen = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void exitFullScreen() {
|
||||
_isFullScreen = false;
|
||||
SystemChrome.setPreferredOrientations([
|
||||
// 强制竖屏
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown
|
||||
]);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleFullScreen() {
|
||||
_isFullScreen = !_isFullScreen;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void togglePause() {
|
||||
isPlaying ? pause() : play();
|
||||
}
|
||||
|
||||
Future<void> play() async {
|
||||
await videoPlayerController.play();
|
||||
}
|
||||
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
Future<void> setLooping(bool looping) async {
|
||||
await videoPlayerController.setLooping(looping);
|
||||
}
|
||||
|
||||
Future<void> pause() async {
|
||||
await videoPlayerController.pause();
|
||||
}
|
||||
|
||||
Future<void> seekTo(Duration moment) async {
|
||||
await videoPlayerController.seekTo(moment);
|
||||
}
|
||||
|
||||
Future<void> setVolume(double volume) async {
|
||||
await videoPlayerController.setVolume(volume);
|
||||
}
|
||||
|
||||
void setSubtitle(List<Subtitle> newSubtitle) {
|
||||
subtitle = Subtitles(newSubtitle);
|
||||
}
|
||||
}
|
||||
|
||||
class ChewieControllerProvider extends InheritedWidget {
|
||||
const ChewieControllerProvider({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final ChewieController controller;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ChewieControllerProvider oldWidget) =>
|
||||
controller != oldWidget.controller;
|
||||
}
|
||||
18
flutter_common/lib/utils/file/video/lib/src/chewie_progress_colors.dart
Executable file
18
flutter_common/lib/utils/file/video/lib/src/chewie_progress_colors.dart
Executable file
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class ChewieProgressColors {
|
||||
ChewieProgressColors({
|
||||
Color playedColor = const Color.fromRGBO(255, 0, 0, 0.7),
|
||||
Color bufferedColor = const Color.fromRGBO(30, 30, 200, 0.2),
|
||||
Color handleColor = const Color.fromRGBO(200, 200, 200, 1.0),
|
||||
Color backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5),
|
||||
}) : playedPaint = Paint()..color = playedColor,
|
||||
bufferedPaint = Paint()..color = bufferedColor,
|
||||
handlePaint = Paint()..color = handleColor,
|
||||
backgroundPaint = Paint()..color = backgroundColor;
|
||||
|
||||
final Paint playedPaint;
|
||||
final Paint bufferedPaint;
|
||||
final Paint handlePaint;
|
||||
final Paint backgroundPaint;
|
||||
}
|
||||
955
flutter_common/lib/utils/file/video/lib/src/cupertino/cupertino_controls.dart
Executable file
955
flutter_common/lib/utils/file/video/lib/src/cupertino/cupertino_controls.dart
Executable file
@@ -0,0 +1,955 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
// import 'package:perfect_volume_control/perfect_volume_control.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../chewie.dart';
|
||||
import '../animated_play_pause.dart';
|
||||
import '../center_play_button.dart';
|
||||
import '../helpers/utils.dart';
|
||||
import '../notifiers/index.dart';
|
||||
import 'cupertino_progress_bar.dart';
|
||||
import 'widgets/cupertino_options_dialog.dart';
|
||||
|
||||
class CupertinoControls extends StatefulWidget {
|
||||
const CupertinoControls({
|
||||
required this.backgroundColor,
|
||||
required this.iconColor,
|
||||
this.showPlayButton = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final Color backgroundColor;
|
||||
final Color iconColor;
|
||||
final bool showPlayButton;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _CupertinoControlsState();
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoControlsState extends State<CupertinoControls>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late PlayerNotifier notifier;
|
||||
late VideoPlayerValue _latestValue;
|
||||
double? _latestVolume;
|
||||
Timer? _hideTimer;
|
||||
final marginSize = 5.0;
|
||||
Timer? _expandCollapseTimer;
|
||||
Timer? _initTimer;
|
||||
bool _dragging = false;
|
||||
Duration? _subtitlesPosition;
|
||||
bool _subtitleOn = false;
|
||||
Timer? _bufferingDisplayTimer;
|
||||
bool _displayBufferingIndicator = false;
|
||||
|
||||
late VideoPlayerController controller;
|
||||
|
||||
// We know that _chewieController is set in didChangeDependencies
|
||||
ChewieController get chewieController => _chewieController!;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notifier = Provider.of<PlayerNotifier>(context, listen: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_latestValue.hasError) {
|
||||
return chewieController.errorBuilder != null
|
||||
? chewieController.errorBuilder!(
|
||||
context,
|
||||
chewieController.videoPlayerController.value.errorDescription!,
|
||||
)
|
||||
: const Center(
|
||||
child: Icon(
|
||||
CupertinoIcons.exclamationmark_circle,
|
||||
color: Colors.white,
|
||||
size: 42,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final backgroundColor = widget.backgroundColor;
|
||||
final iconColor = widget.iconColor;
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
final barHeight = orientation == Orientation.portrait ? 30.0 : 47.0;
|
||||
final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0;
|
||||
double volumeStart = 0.0; //开始手指位置
|
||||
double volume = 0.0; //开始声音
|
||||
double _brightness = 0.0; //屏幕亮度
|
||||
double volumeDouble = 0.018;
|
||||
|
||||
return MouseRegion(
|
||||
onHover: (_) => _cancelAndRestartTimer(),
|
||||
child: GestureDetector(
|
||||
onTap: () => _cancelAndRestartTimer(),
|
||||
onDoubleTap: _playPause,
|
||||
onLongPress: () {
|
||||
///长按快进
|
||||
///
|
||||
|
||||
printInfo(info: "长按快进");
|
||||
},
|
||||
onLongPressStart: (details) {
|
||||
controller.setPlaybackSpeed(3);
|
||||
},
|
||||
onLongPressEnd: (details) {
|
||||
controller.setPlaybackSpeed(1);
|
||||
},
|
||||
onVerticalDragCancel: () {
|
||||
print("onVerticalDragCancel");
|
||||
},
|
||||
// onVerticalDragDown: (details) {
|
||||
// print(
|
||||
// "onVerticalDragDown---${details.globalPosition}---${details.localPosition}");
|
||||
// },
|
||||
onVerticalDragEnd: (details) {
|
||||
// print(
|
||||
// "onVerticalDragEnd---${details.velocity}---${details.primaryVelocity}");
|
||||
},
|
||||
onVerticalDragStart: (details) async {
|
||||
volumeStart = details.localPosition.dy;
|
||||
// PerfectVolumeControl.getVolume();
|
||||
// volume = await PerfectVolumeControl.volume;
|
||||
_brightness = await ScreenBrightness().current;
|
||||
print("volumevolume---volume$volume---_brightness $_brightness");
|
||||
},
|
||||
onVerticalDragUpdate: (details) async {
|
||||
double midle = MediaQuery.of(context).size.width / 2;
|
||||
double width = details.localPosition.dx;
|
||||
print("====== volume $volume");
|
||||
if (midle > width) {
|
||||
if (details.delta.direction > 0) {
|
||||
_brightness =
|
||||
_brightness - volumeDouble * details.primaryDelta!.obs.value;
|
||||
if (_brightness <= 0) {
|
||||
_brightness = 0;
|
||||
}
|
||||
print("====== 左边向下滑 $_brightness");
|
||||
} else {
|
||||
_brightness =
|
||||
_brightness - volumeDouble * details.primaryDelta!.obs.value;
|
||||
if (_brightness >= 1) {
|
||||
_brightness = 1;
|
||||
}
|
||||
print("====== 左边向上滑 $_brightness");
|
||||
}
|
||||
await ScreenBrightness().setScreenBrightness(_brightness);
|
||||
} else {
|
||||
if (details.delta.direction > 0) {
|
||||
volume = volume - volumeDouble * details.primaryDelta!.obs.value;
|
||||
if (volume <= 0) {
|
||||
volume = 0;
|
||||
}
|
||||
print("====== 右边向下滑 $volume");
|
||||
} else {
|
||||
print("====== 右边向上滑 之前 $volume");
|
||||
volume = volume - volumeDouble * details.primaryDelta!.obs.value;
|
||||
if (volume >= 1) {
|
||||
volume = 1;
|
||||
}
|
||||
print("====== 右边向上滑 $volume");
|
||||
}
|
||||
// PerfectVolumeControl.setVolume(volume);
|
||||
}
|
||||
|
||||
// print(
|
||||
// "高度====== ${details.localPosition.dy} /250 ${details.localPosition.dy / 250}");
|
||||
//
|
||||
// print(
|
||||
// "onVerticalDragUpdate---${details.globalPosition}---${details.localPosition}---${details.delta}");
|
||||
},
|
||||
child: AbsorbPointer(
|
||||
absorbing: notifier.hideStuff,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_displayBufferingIndicator)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
_buildHitArea(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
_buildTopBar(
|
||||
backgroundColor,
|
||||
iconColor,
|
||||
barHeight,
|
||||
buttonPadding,
|
||||
),
|
||||
const Spacer(),
|
||||
if (_subtitleOn)
|
||||
Transform.translate(
|
||||
offset: Offset(
|
||||
0.0,
|
||||
notifier.hideStuff ? barHeight * 0.8 : 0.0,
|
||||
),
|
||||
child: _buildSubtitles(chewieController.subtitle!),
|
||||
),
|
||||
_buildBottomBar(backgroundColor, iconColor, barHeight),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispose() {
|
||||
controller.removeListener(_updateState);
|
||||
_hideTimer?.cancel();
|
||||
_expandCollapseTimer?.cancel();
|
||||
_initTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final oldController = _chewieController;
|
||||
_chewieController = ChewieController.of(context);
|
||||
controller = chewieController.videoPlayerController;
|
||||
|
||||
if (oldController != chewieController) {
|
||||
_dispose();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
GestureDetector _buildOptionsButton(
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
final options = <OptionItem>[];
|
||||
|
||||
if (chewieController.additionalOptions != null &&
|
||||
chewieController.additionalOptions!(context).isNotEmpty) {
|
||||
options.addAll(chewieController.additionalOptions!(context));
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
if (chewieController.optionsBuilder != null) {
|
||||
await chewieController.optionsBuilder!(context, options);
|
||||
} else {
|
||||
await showCupertinoModalPopup<OptionItem>(
|
||||
context: context,
|
||||
semanticsDismissible: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => CupertinoOptionsDialog(
|
||||
options: options,
|
||||
cancelButtonText:
|
||||
chewieController.optionsTranslation?.cancelButtonText,
|
||||
),
|
||||
);
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(left: 4.0, right: 8.0),
|
||||
margin: const EdgeInsets.only(right: 6.0),
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
color: iconColor,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitles(Subtitles subtitles) {
|
||||
if (!_subtitleOn) {
|
||||
return const SizedBox();
|
||||
}
|
||||
if (_subtitlesPosition == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition!);
|
||||
if (currentSubtitle.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
if (chewieController.subtitleBuilder != null) {
|
||||
return chewieController.subtitleBuilder!(
|
||||
context,
|
||||
currentSubtitle.first!.text,
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: marginSize, right: marginSize),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x96000000),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Text(
|
||||
currentSubtitle.first!.text.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar(
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
return SafeArea(
|
||||
bottom: chewieController.isFullScreen,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
alignment: Alignment.bottomCenter,
|
||||
margin: EdgeInsets.all(marginSize),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(
|
||||
sigmaX: 10.0,
|
||||
sigmaY: 10.0,
|
||||
),
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: backgroundColor,
|
||||
child: chewieController.isLive
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
_buildPlayPause(controller, iconColor, barHeight),
|
||||
_buildLive(iconColor),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: <Widget>[
|
||||
// _buildSkipBack(iconColor, barHeight),
|
||||
_buildPlayPause(controller, iconColor, barHeight),
|
||||
// _buildSkipForward(iconColor, barHeight),
|
||||
_buildPosition(iconColor),
|
||||
_buildProgressBar(),
|
||||
_buildRemaining(iconColor),
|
||||
_buildSubtitleToggle(iconColor, barHeight),
|
||||
// if (chewieController.allowPlaybackSpeedChanging)
|
||||
// _buildSpeedButton(controller, iconColor, barHeight),
|
||||
if (chewieController.additionalOptions != null &&
|
||||
chewieController
|
||||
.additionalOptions!(context).isNotEmpty)
|
||||
_buildOptionsButton(iconColor, barHeight),
|
||||
// if (chewieController.allowFullScreen)
|
||||
// _buildExpandButton(
|
||||
// backgroundColor,
|
||||
// iconColor,
|
||||
// barHeight,
|
||||
// MediaQuery.of(context).orientation ==
|
||||
// Orientation.portrait
|
||||
// ? 16.0
|
||||
// : 24.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLive(Color iconColor) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Text(
|
||||
'LIVE',
|
||||
style: TextStyle(color: iconColor, fontSize: 12.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildExpandButton(
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
double buttonPadding, {
|
||||
bool? showRight,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: _onExpandCollapse,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 10.0),
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: EdgeInsets.only(
|
||||
left: buttonPadding,
|
||||
right: buttonPadding,
|
||||
),
|
||||
color: backgroundColor,
|
||||
child: Center(
|
||||
child: showRight == true
|
||||
? const Icon(
|
||||
Icons.keyboard_arrow_left,
|
||||
size: 22,
|
||||
color: Colors.white,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.fullscreen,
|
||||
size: 22,
|
||||
color: Colors.white,
|
||||
)
|
||||
// 'Image.asset(
|
||||
// Assets.imagesFullScreen,
|
||||
// width: 22,
|
||||
// height: 22,
|
||||
// ),
|
||||
// child: Icon(
|
||||
// chewieController.isFullScreen
|
||||
// ? CupertinoIcons.arrow_down_right_arrow_up_left
|
||||
// : CupertinoIcons.arrow_up_left_arrow_down_right,
|
||||
// color: iconColor,
|
||||
// size: 16,
|
||||
// ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHitArea() {
|
||||
final bool isFinished = _latestValue.position >= _latestValue.duration;
|
||||
final bool showPlayButton =
|
||||
widget.showPlayButton && !_latestValue.isPlaying && !_dragging;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: _latestValue.isPlaying
|
||||
? _cancelAndRestartTimer
|
||||
: () {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: widget.backgroundColor,
|
||||
iconColor: widget.iconColor,
|
||||
isFinished: isFinished,
|
||||
isPlaying: controller.value.isPlaying,
|
||||
show: showPlayButton,
|
||||
onPressed: _playPause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildMuteButton(
|
||||
VideoPlayerController controller,
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
double buttonPadding,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (_latestValue.volume == 0) {
|
||||
controller.setVolume(_latestVolume ?? 0.5);
|
||||
} else {
|
||||
_latestVolume = controller.value.volume;
|
||||
controller.setVolume(0.0);
|
||||
}
|
||||
},
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 10.0),
|
||||
child: ColoredBox(
|
||||
color: backgroundColor,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: EdgeInsets.only(
|
||||
left: buttonPadding,
|
||||
right: buttonPadding,
|
||||
),
|
||||
child: Icon(
|
||||
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
|
||||
color: iconColor,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildPlayPause(
|
||||
VideoPlayerController controller,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: _playPause,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
child: AnimatedPlayPause(
|
||||
color: widget.iconColor,
|
||||
playing: controller.value.isPlaying,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPosition(Color iconColor) {
|
||||
final position = _latestValue.position;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Text(
|
||||
formatDuration(position),
|
||||
style: TextStyle(
|
||||
color: iconColor,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRemaining(Color iconColor) {
|
||||
final position = _latestValue.duration - _latestValue.position;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Text(
|
||||
'-${formatDuration(position)}',
|
||||
style: TextStyle(color: iconColor, fontSize: 12.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitleToggle(Color iconColor, double barHeight) {
|
||||
//if don't have subtitle hiden button
|
||||
if (chewieController.subtitle?.isEmpty ?? true) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: _subtitleToggle,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
margin: const EdgeInsets.only(right: 10.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.subtitles,
|
||||
color: _subtitleOn ? iconColor : Colors.grey[700],
|
||||
size: 16.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _subtitleToggle() {
|
||||
setState(() {
|
||||
_subtitleOn = !_subtitleOn;
|
||||
});
|
||||
}
|
||||
|
||||
GestureDetector _buildSkipBack(Color iconColor, double barHeight) {
|
||||
return GestureDetector(
|
||||
onTap: _skipBack,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
margin: const EdgeInsets.only(left: 10.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.gobackward_15,
|
||||
color: iconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildSkipForward(Color iconColor, double barHeight) {
|
||||
return GestureDetector(
|
||||
onTap: _skipForward,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 8.0,
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
right: 8.0,
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.goforward_15,
|
||||
color: iconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildSpeedButton(
|
||||
VideoPlayerController controller,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
final chosenSpeed = await showCupertinoModalPopup<double>(
|
||||
context: context,
|
||||
semanticsDismissible: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => _PlaybackSpeedDialog(
|
||||
speeds: chewieController.playbackSpeeds,
|
||||
selected: _latestValue.playbackSpeed,
|
||||
),
|
||||
);
|
||||
|
||||
if (chosenSpeed != null) {
|
||||
controller.setPlaybackSpeed(chosenSpeed);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 8.0,
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
right: 8.0,
|
||||
),
|
||||
child: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.skewY(0.0)
|
||||
..rotateX(math.pi)
|
||||
..rotateZ(math.pi * 0.8),
|
||||
child: Icon(
|
||||
Icons.speed,
|
||||
color: iconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopBar(
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
double buttonPadding,
|
||||
) {
|
||||
return Container(
|
||||
height: barHeight,
|
||||
margin: EdgeInsets.only(
|
||||
top: marginSize,
|
||||
right: marginSize,
|
||||
left: marginSize,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (chewieController.allowFullScreen)
|
||||
if (chewieController.isFullScreen)
|
||||
_buildExpandButton(
|
||||
backgroundColor,
|
||||
iconColor,
|
||||
barHeight,
|
||||
buttonPadding,
|
||||
showRight: true,
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
// if (chewieController.allowMuting)
|
||||
// _buildMuteButton(
|
||||
// controller,
|
||||
// backgroundColor,
|
||||
// iconColor,
|
||||
// barHeight,
|
||||
// buttonPadding,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _cancelAndRestartTimer() {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
|
||||
_startHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
|
||||
controller.addListener(_updateState);
|
||||
|
||||
_updateState();
|
||||
|
||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
if (chewieController.showControlsOnInitialize) {
|
||||
_initTimer = Timer(const Duration(milliseconds: 200), () {
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onExpandCollapse() {
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
Navigator.of(context).pop();
|
||||
// notifier.hideStuff = true;
|
||||
//
|
||||
// chewieController.toggleFullScreen();
|
||||
// _expandCollapseTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
// setState(() {
|
||||
// _cancelAndRestartTimer();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildProgressBar() {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: CupertinoVideoProgressBar(
|
||||
controller,
|
||||
onDragStart: () {
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
});
|
||||
|
||||
_hideTimer?.cancel();
|
||||
},
|
||||
onDragEnd: () {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
});
|
||||
|
||||
_startHideTimer();
|
||||
},
|
||||
colors: chewieController.cupertinoProgressColors ??
|
||||
ChewieProgressColors(
|
||||
playedColor: const Color.fromARGB(
|
||||
120,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
handleColor: const Color.fromARGB(
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
bufferedColor: const Color.fromARGB(
|
||||
60,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
backgroundColor: const Color.fromARGB(
|
||||
20,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _playPause() {
|
||||
final isFinished = _latestValue.position >= _latestValue.duration;
|
||||
|
||||
setState(() {
|
||||
if (controller.value.isPlaying) {
|
||||
notifier.hideStuff = false;
|
||||
_hideTimer?.cancel();
|
||||
controller.pause();
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (!controller.value.isInitialized) {
|
||||
controller.initialize().then((_) {
|
||||
controller.play();
|
||||
});
|
||||
} else {
|
||||
if (isFinished) {
|
||||
controller.seekTo(Duration.zero);
|
||||
}
|
||||
controller.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _skipBack() {
|
||||
_cancelAndRestartTimer();
|
||||
final beginning = Duration.zero.inMilliseconds;
|
||||
final skip =
|
||||
(_latestValue.position - const Duration(seconds: 15)).inMilliseconds;
|
||||
controller.seekTo(Duration(milliseconds: math.max(skip, beginning)));
|
||||
}
|
||||
|
||||
void _skipForward() {
|
||||
_cancelAndRestartTimer();
|
||||
final end = _latestValue.duration.inMilliseconds;
|
||||
final skip =
|
||||
(_latestValue.position + const Duration(seconds: 15)).inMilliseconds;
|
||||
controller.seekTo(Duration(milliseconds: math.min(skip, end)));
|
||||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
||||
? ChewieController.defaultHideControlsTimer
|
||||
: chewieController.hideControlsTimer;
|
||||
_hideTimer = Timer(hideControlsTimer, () {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _bufferingTimerTimeout() {
|
||||
_displayBufferingIndicator = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
if (!mounted) return;
|
||||
|
||||
// display the progress bar indicator only after the buffering delay if it has been set
|
||||
if (chewieController.progressIndicatorDelay != null) {
|
||||
if (controller.value.isBuffering) {
|
||||
_bufferingDisplayTimer ??= Timer(
|
||||
chewieController.progressIndicatorDelay!,
|
||||
_bufferingTimerTimeout,
|
||||
);
|
||||
} else {
|
||||
_bufferingDisplayTimer?.cancel();
|
||||
_bufferingDisplayTimer = null;
|
||||
_displayBufferingIndicator = false;
|
||||
}
|
||||
} else {
|
||||
_displayBufferingIndicator = controller.value.isBuffering;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_latestValue = controller.value;
|
||||
_subtitlesPosition = controller.value.position;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaybackSpeedDialog extends StatelessWidget {
|
||||
const _PlaybackSpeedDialog({
|
||||
Key? key,
|
||||
required List<double> speeds,
|
||||
required double selected,
|
||||
}) : _speeds = speeds,
|
||||
_selected = selected,
|
||||
super(key: key);
|
||||
|
||||
final List<double> _speeds;
|
||||
final double _selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedColor = CupertinoTheme.of(context).primaryColor;
|
||||
|
||||
return CupertinoActionSheet(
|
||||
actions: _speeds
|
||||
.map(
|
||||
(e) => CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(e);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (e == _selected)
|
||||
Icon(Icons.check, size: 20.0, color: selectedColor),
|
||||
Text(e.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../chewie.dart';
|
||||
import '../progress_bar.dart';
|
||||
|
||||
class CupertinoVideoProgressBar extends StatelessWidget {
|
||||
CupertinoVideoProgressBar(
|
||||
this.controller, {
|
||||
ChewieProgressColors? colors,
|
||||
this.onDragEnd,
|
||||
this.onDragStart,
|
||||
this.onDragUpdate,
|
||||
Key? key,
|
||||
}) : colors = colors ?? ChewieProgressColors(),
|
||||
super(key: key);
|
||||
|
||||
final VideoPlayerController controller;
|
||||
final ChewieProgressColors colors;
|
||||
final Function()? onDragStart;
|
||||
final Function()? onDragEnd;
|
||||
final Function()? onDragUpdate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return VideoProgressBar(
|
||||
controller,
|
||||
barHeight: 5,
|
||||
handleHeight: 6,
|
||||
drawShadow: true,
|
||||
colors: colors,
|
||||
onDragEnd: onDragEnd,
|
||||
onDragStart: onDragStart,
|
||||
onDragUpdate: onDragUpdate,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../../chewie.dart';
|
||||
|
||||
class CupertinoOptionsDialog extends StatefulWidget {
|
||||
const CupertinoOptionsDialog({
|
||||
Key? key,
|
||||
required this.options,
|
||||
this.cancelButtonText,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<OptionItem> options;
|
||||
final String? cancelButtonText;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_CupertinoOptionsDialogState createState() => _CupertinoOptionsDialogState();
|
||||
}
|
||||
|
||||
class _CupertinoOptionsDialogState extends State<CupertinoOptionsDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: CupertinoActionSheet(
|
||||
actions: widget.options
|
||||
.map(
|
||||
(option) => CupertinoActionSheetAction(
|
||||
onPressed: () => option.onTap!(),
|
||||
child: Text(option.title),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
isDestructiveAction: true,
|
||||
child: Text(widget.cancelButtonText ?? 'Cancel'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
35
flutter_common/lib/utils/file/video/lib/src/helpers/adaptive_controls.dart
Executable file
35
flutter_common/lib/utils/file/video/lib/src/helpers/adaptive_controls.dart
Executable file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../chewie.dart';
|
||||
|
||||
class AdaptiveControls extends StatelessWidget {
|
||||
const AdaptiveControls({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
return const CupertinoControls(
|
||||
backgroundColor: Color.fromRGBO(41, 41, 41, 0.7),
|
||||
iconColor: Color.fromARGB(255, 200, 200, 200),
|
||||
);
|
||||
return const MaterialControls();
|
||||
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
case TargetPlatform.linux:
|
||||
return const MaterialDesktopControls();
|
||||
|
||||
case TargetPlatform.iOS:
|
||||
return const CupertinoControls(
|
||||
backgroundColor: Color.fromRGBO(41, 41, 41, 0.7),
|
||||
iconColor: Color.fromARGB(255, 200, 200, 200),
|
||||
);
|
||||
default:
|
||||
return const MaterialControls();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
flutter_common/lib/utils/file/video/lib/src/helpers/utils.dart
Executable file
32
flutter_common/lib/utils/file/video/lib/src/helpers/utils.dart
Executable file
@@ -0,0 +1,32 @@
|
||||
String formatDuration(Duration position) {
|
||||
final ms = position.inMilliseconds;
|
||||
|
||||
int seconds = ms ~/ 1000;
|
||||
final int hours = seconds ~/ 3600;
|
||||
seconds = seconds % 3600;
|
||||
final minutes = seconds ~/ 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
final hoursString = hours >= 10
|
||||
? '$hours'
|
||||
: hours == 0
|
||||
? '00'
|
||||
: '0$hours';
|
||||
|
||||
final minutesString = minutes >= 10
|
||||
? '$minutes'
|
||||
: minutes == 0
|
||||
? '00'
|
||||
: '0$minutes';
|
||||
|
||||
final secondsString = seconds >= 10
|
||||
? '$seconds'
|
||||
: seconds == 0
|
||||
? '00'
|
||||
: '0$seconds';
|
||||
|
||||
final formattedTime =
|
||||
'${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString';
|
||||
|
||||
return formattedTime;
|
||||
}
|
||||
623
flutter_common/lib/utils/file/video/lib/src/material/material_controls.dart
Executable file
623
flutter_common/lib/utils/file/video/lib/src/material/material_controls.dart
Executable file
@@ -0,0 +1,623 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../chewie.dart';
|
||||
import '../center_play_button.dart';
|
||||
import '../helpers/utils.dart';
|
||||
import '../notifiers/index.dart';
|
||||
import 'material_progress_bar.dart';
|
||||
import 'widgets/options_dialog.dart';
|
||||
import 'widgets/playback_speed_dialog.dart';
|
||||
|
||||
class MaterialControls extends StatefulWidget {
|
||||
const MaterialControls({
|
||||
this.showPlayButton = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool showPlayButton;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _MaterialControlsState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MaterialControlsState extends State<MaterialControls>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late PlayerNotifier notifier;
|
||||
late VideoPlayerValue _latestValue;
|
||||
double? _latestVolume;
|
||||
Timer? _hideTimer;
|
||||
Timer? _initTimer;
|
||||
late var _subtitlesPosition = Duration.zero;
|
||||
bool _subtitleOn = false;
|
||||
Timer? _showAfterExpandCollapseTimer;
|
||||
bool _dragging = false;
|
||||
bool _displayTapped = false;
|
||||
Timer? _bufferingDisplayTimer;
|
||||
bool _displayBufferingIndicator = false;
|
||||
|
||||
final barHeight = 48.0 * 1.5;
|
||||
final marginSize = 5.0;
|
||||
|
||||
late VideoPlayerController controller;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
// We know that _chewieController is set in didChangeDependencies
|
||||
ChewieController get chewieController => _chewieController!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notifier = Provider.of<PlayerNotifier>(context, listen: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_latestValue.hasError) {
|
||||
return chewieController.errorBuilder?.call(
|
||||
context,
|
||||
chewieController.videoPlayerController.value.errorDescription!,
|
||||
) ??
|
||||
const Center(
|
||||
child: Icon(
|
||||
Icons.error,
|
||||
color: Colors.white,
|
||||
size: 42,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
onHover: (_) {
|
||||
_cancelAndRestartTimer();
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () => _cancelAndRestartTimer(),
|
||||
onLongPress: () {
|
||||
///长按快进
|
||||
///
|
||||
},
|
||||
onLongPressStart: (details) {
|
||||
controller.setPlaybackSpeed(3);
|
||||
},
|
||||
onLongPressEnd: (details) {
|
||||
controller.setPlaybackSpeed(1);
|
||||
},
|
||||
child: AbsorbPointer(
|
||||
absorbing: notifier.hideStuff,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_displayBufferingIndicator)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
_buildHitArea(),
|
||||
_buildActionBar(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
if (_subtitleOn)
|
||||
Transform.translate(
|
||||
offset: Offset(
|
||||
0.0,
|
||||
notifier.hideStuff ? barHeight * 0.8 : 0.0,
|
||||
),
|
||||
child:
|
||||
_buildSubtitles(context, chewieController.subtitle!),
|
||||
),
|
||||
_buildBottomBar(context),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispose() {
|
||||
controller.removeListener(_updateState);
|
||||
_hideTimer?.cancel();
|
||||
_initTimer?.cancel();
|
||||
_showAfterExpandCollapseTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final oldController = _chewieController;
|
||||
_chewieController = ChewieController.of(context);
|
||||
controller = chewieController.videoPlayerController;
|
||||
|
||||
if (oldController != chewieController) {
|
||||
_dispose();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
Widget _buildActionBar() {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildSubtitleToggle(),
|
||||
if (chewieController.showOptions) _buildOptionsButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionsButton() {
|
||||
final options = <OptionItem>[
|
||||
OptionItem(
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_onSpeedButtonTap();
|
||||
},
|
||||
iconData: Icons.speed,
|
||||
title: chewieController.optionsTranslation?.playbackSpeedButtonText ??
|
||||
'Playback speed',
|
||||
)
|
||||
];
|
||||
|
||||
if (chewieController.additionalOptions != null &&
|
||||
chewieController.additionalOptions!(context).isNotEmpty) {
|
||||
options.addAll(chewieController.additionalOptions!(context));
|
||||
}
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
if (chewieController.optionsBuilder != null) {
|
||||
await chewieController.optionsBuilder!(context, options);
|
||||
} else {
|
||||
await showModalBottomSheet<OptionItem>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => OptionsDialog(
|
||||
options: options,
|
||||
cancelButtonText:
|
||||
chewieController.optionsTranslation?.cancelButtonText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
|
||||
if (!_subtitleOn) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition);
|
||||
if (currentSubtitle.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
if (chewieController.subtitleBuilder != null) {
|
||||
return chewieController.subtitleBuilder!(
|
||||
context,
|
||||
currentSubtitle.first!.text,
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(marginSize),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x96000000),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Text(
|
||||
currentSubtitle.first!.text.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AnimatedOpacity _buildBottomBar(
|
||||
BuildContext context,
|
||||
) {
|
||||
final iconColor = Theme.of(context).textTheme.labelMedium!.color;
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: barHeight + (chewieController.isFullScreen ? 10.0 : 0),
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
bottom: !chewieController.isFullScreen ? 10.0 : 0,
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: chewieController.isFullScreen,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
if (chewieController.isLive)
|
||||
const Expanded(child: Text('LIVE'))
|
||||
else
|
||||
_buildPosition(iconColor),
|
||||
if (chewieController.allowMuting)
|
||||
_buildMuteButton(controller),
|
||||
const Spacer(),
|
||||
if (chewieController.allowFullScreen) _buildExpandButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: chewieController.isFullScreen ? 15.0 : 0,
|
||||
),
|
||||
if (!chewieController.isLive)
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildProgressBar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildMuteButton(
|
||||
VideoPlayerController controller,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (_latestValue.volume == 0) {
|
||||
controller.setVolume(_latestVolume ?? 0.5);
|
||||
} else {
|
||||
_latestVolume = controller.value.volume;
|
||||
controller.setVolume(0.0);
|
||||
}
|
||||
},
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRect(
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
),
|
||||
child: Icon(
|
||||
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildExpandButton() {
|
||||
return GestureDetector(
|
||||
onTap: _onExpandCollapse,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: barHeight + (chewieController.isFullScreen ? 15.0 : 0),
|
||||
margin: const EdgeInsets.only(right: 12.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
chewieController.isFullScreen
|
||||
? Icons.fullscreen_exit
|
||||
: Icons.fullscreen,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHitArea() {
|
||||
final bool isFinished = _latestValue.position >= _latestValue.duration;
|
||||
final bool showPlayButton =
|
||||
widget.showPlayButton && !_dragging && !notifier.hideStuff;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (_latestValue.isPlaying) {
|
||||
if (_displayTapped) {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
}
|
||||
} else {
|
||||
_playPause();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: Colors.black54,
|
||||
iconColor: Colors.white,
|
||||
isFinished: isFinished,
|
||||
isPlaying: controller.value.isPlaying,
|
||||
show: showPlayButton,
|
||||
onPressed: _playPause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSpeedButtonTap() async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
final chosenSpeed = await showModalBottomSheet<double>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => PlaybackSpeedDialog(
|
||||
speeds: chewieController.playbackSpeeds,
|
||||
selected: _latestValue.playbackSpeed,
|
||||
),
|
||||
);
|
||||
|
||||
if (chosenSpeed != null) {
|
||||
controller.setPlaybackSpeed(chosenSpeed);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPosition(Color? iconColor) {
|
||||
final position = _latestValue.position;
|
||||
final duration = _latestValue.duration;
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: '${formatDuration(position)} ',
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: '/ ${formatDuration(duration)}',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.white.withOpacity(.75),
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
)
|
||||
],
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitleToggle() {
|
||||
//if don't have subtitle hiden button
|
||||
if (chewieController.subtitle?.isEmpty ?? true) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: _onSubtitleTap,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
),
|
||||
child: Icon(
|
||||
_subtitleOn
|
||||
? Icons.closed_caption
|
||||
: Icons.closed_caption_off_outlined,
|
||||
color: _subtitleOn ? Colors.white : Colors.grey[700],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubtitleTap() {
|
||||
setState(() {
|
||||
_subtitleOn = !_subtitleOn;
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelAndRestartTimer() {
|
||||
_hideTimer?.cancel();
|
||||
_startHideTimer();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
_displayTapped = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
|
||||
controller.addListener(_updateState);
|
||||
|
||||
_updateState();
|
||||
|
||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
if (chewieController.showControlsOnInitialize) {
|
||||
_initTimer = Timer(const Duration(milliseconds: 200), () {
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onExpandCollapse() {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
|
||||
chewieController.toggleFullScreen();
|
||||
_showAfterExpandCollapseTimer =
|
||||
Timer(const Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
_cancelAndRestartTimer();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _playPause() {
|
||||
final isFinished = _latestValue.position >= _latestValue.duration;
|
||||
|
||||
setState(() {
|
||||
if (controller.value.isPlaying) {
|
||||
notifier.hideStuff = false;
|
||||
_hideTimer?.cancel();
|
||||
controller.pause();
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (!controller.value.isInitialized) {
|
||||
controller.initialize().then((_) {
|
||||
controller.play();
|
||||
});
|
||||
} else {
|
||||
if (isFinished) {
|
||||
controller.seekTo(Duration.zero);
|
||||
}
|
||||
controller.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
||||
? ChewieController.defaultHideControlsTimer
|
||||
: chewieController.hideControlsTimer;
|
||||
_hideTimer = Timer(hideControlsTimer, () {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _bufferingTimerTimeout() {
|
||||
_displayBufferingIndicator = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
if (!mounted) return;
|
||||
|
||||
// display the progress bar indicator only after the buffering delay if it has been set
|
||||
if (chewieController.progressIndicatorDelay != null) {
|
||||
if (controller.value.isBuffering) {
|
||||
_bufferingDisplayTimer ??= Timer(
|
||||
chewieController.progressIndicatorDelay!,
|
||||
_bufferingTimerTimeout,
|
||||
);
|
||||
} else {
|
||||
_bufferingDisplayTimer?.cancel();
|
||||
_bufferingDisplayTimer = null;
|
||||
_displayBufferingIndicator = false;
|
||||
}
|
||||
} else {
|
||||
_displayBufferingIndicator = controller.value.isBuffering;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_latestValue = controller.value;
|
||||
_subtitlesPosition = controller.value.position;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildProgressBar() {
|
||||
return Expanded(
|
||||
child: MaterialVideoProgressBar(
|
||||
controller,
|
||||
onDragStart: () {
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
});
|
||||
|
||||
_hideTimer?.cancel();
|
||||
},
|
||||
onDragEnd: () {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
});
|
||||
|
||||
_startHideTimer();
|
||||
},
|
||||
colors: chewieController.materialProgressColors ??
|
||||
ChewieProgressColors(
|
||||
playedColor: Theme.of(context).colorScheme.secondary,
|
||||
handleColor: Theme.of(context).colorScheme.secondary,
|
||||
bufferedColor: Theme.of(context).disabledColor.withOpacity(0.5),
|
||||
backgroundColor: Theme.of(context).disabledColor.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/file/video/lib/chewie.dart';
|
||||
import 'package:flutter_common/utils/file/video/lib/src/chewie_player.dart';
|
||||
import 'package:flutter_common/utils/file/video/lib/src/material/widgets/options_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../animated_play_pause.dart';
|
||||
import '../center_play_button.dart';
|
||||
import '../helpers/utils.dart';
|
||||
import '../notifiers/index.dart';
|
||||
import 'material_progress_bar.dart';
|
||||
import 'widgets/playback_speed_dialog.dart';
|
||||
|
||||
class MaterialDesktopControls extends StatefulWidget {
|
||||
const MaterialDesktopControls({
|
||||
this.showPlayButton = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool showPlayButton;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _MaterialDesktopControlsState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MaterialDesktopControlsState extends State<MaterialDesktopControls>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late PlayerNotifier notifier;
|
||||
late VideoPlayerValue _latestValue;
|
||||
double? _latestVolume;
|
||||
Timer? _hideTimer;
|
||||
Timer? _initTimer;
|
||||
late var _subtitlesPosition = Duration.zero;
|
||||
bool _subtitleOn = false;
|
||||
Timer? _showAfterExpandCollapseTimer;
|
||||
bool _dragging = false;
|
||||
bool _displayTapped = false;
|
||||
Timer? _bufferingDisplayTimer;
|
||||
bool _displayBufferingIndicator = false;
|
||||
|
||||
final barHeight = 48.0 * 1.5;
|
||||
final marginSize = 5.0;
|
||||
|
||||
late VideoPlayerController controller;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
// We know that _chewieController is set in didChangeDependencies
|
||||
ChewieController get chewieController => _chewieController!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notifier = Provider.of<PlayerNotifier>(context, listen: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_latestValue.hasError) {
|
||||
return chewieController.errorBuilder?.call(
|
||||
context,
|
||||
chewieController.videoPlayerController.value.errorDescription!,
|
||||
) ??
|
||||
const Center(
|
||||
child: Icon(
|
||||
Icons.error,
|
||||
color: Colors.white,
|
||||
size: 42,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
onHover: (_) {
|
||||
_cancelAndRestartTimer();
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () => _cancelAndRestartTimer(),
|
||||
child: AbsorbPointer(
|
||||
absorbing: notifier.hideStuff,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_displayBufferingIndicator)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
_buildHitArea(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
if (_subtitleOn)
|
||||
Transform.translate(
|
||||
offset: Offset(
|
||||
0.0,
|
||||
notifier.hideStuff ? barHeight * 0.8 : 0.0,
|
||||
),
|
||||
child:
|
||||
_buildSubtitles(context, chewieController.subtitle!),
|
||||
),
|
||||
_buildBottomBar(context),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispose() {
|
||||
controller.removeListener(_updateState);
|
||||
_hideTimer?.cancel();
|
||||
_initTimer?.cancel();
|
||||
_showAfterExpandCollapseTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final oldController = _chewieController;
|
||||
_chewieController = ChewieController.of(context);
|
||||
controller = chewieController.videoPlayerController;
|
||||
|
||||
if (oldController != chewieController) {
|
||||
_dispose();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
Widget _buildSubtitleToggle({IconData? icon, bool isPadded = false}) {
|
||||
return IconButton(
|
||||
padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero,
|
||||
icon: Icon(icon, color: _subtitleOn ? Colors.white : Colors.grey[700]),
|
||||
onPressed: _onSubtitleTap,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionsButton({
|
||||
IconData? icon,
|
||||
bool isPadded = false,
|
||||
}) {
|
||||
final options = <OptionItem>[
|
||||
OptionItem(
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_onSpeedButtonTap();
|
||||
},
|
||||
iconData: Icons.speed,
|
||||
title: chewieController.optionsTranslation?.playbackSpeedButtonText ??
|
||||
'Playback speed',
|
||||
)
|
||||
];
|
||||
|
||||
if (chewieController.additionalOptions != null &&
|
||||
chewieController.additionalOptions!(context).isNotEmpty) {
|
||||
options.addAll(chewieController.additionalOptions!(context));
|
||||
}
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: IconButton(
|
||||
padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero,
|
||||
onPressed: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
if (chewieController.optionsBuilder != null) {
|
||||
await chewieController.optionsBuilder!(context, options);
|
||||
} else {
|
||||
await showModalBottomSheet<OptionItem>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => OptionsDialog(
|
||||
options: options,
|
||||
cancelButtonText:
|
||||
chewieController.optionsTranslation?.cancelButtonText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
icon ?? Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
|
||||
if (!_subtitleOn) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition);
|
||||
if (currentSubtitle.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
if (chewieController.subtitleBuilder != null) {
|
||||
return chewieController.subtitleBuilder!(
|
||||
context,
|
||||
currentSubtitle.first!.text,
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(marginSize),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x96000000),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Text(
|
||||
currentSubtitle.first!.text.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AnimatedOpacity _buildBottomBar(
|
||||
BuildContext context,
|
||||
) {
|
||||
final iconColor = Theme.of(context).textTheme.labelMedium!.color;
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: barHeight + (chewieController.isFullScreen ? 20.0 : 0),
|
||||
padding:
|
||||
EdgeInsets.only(bottom: chewieController.isFullScreen ? 10.0 : 15),
|
||||
child: SafeArea(
|
||||
bottom: chewieController.isFullScreen,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
verticalDirection: VerticalDirection.up,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
_buildPlayPause(controller),
|
||||
_buildMuteButton(controller),
|
||||
if (chewieController.isLive)
|
||||
const Expanded(child: Text('LIVE'))
|
||||
else
|
||||
_buildPosition(iconColor),
|
||||
const Spacer(),
|
||||
if (chewieController.showControls &&
|
||||
chewieController.subtitle != null &&
|
||||
chewieController.subtitle!.isNotEmpty)
|
||||
_buildSubtitleToggle(icon: Icons.subtitles),
|
||||
if (chewieController.showOptions)
|
||||
_buildOptionsButton(icon: Icons.settings),
|
||||
if (chewieController.allowFullScreen) _buildExpandButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!chewieController.isLive)
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
right: 20,
|
||||
left: 20,
|
||||
bottom: chewieController.isFullScreen ? 5.0 : 0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildProgressBar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildExpandButton() {
|
||||
return GestureDetector(
|
||||
onTap: _onExpandCollapse,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: barHeight + (chewieController.isFullScreen ? 15.0 : 0),
|
||||
margin: const EdgeInsets.only(right: 12.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
chewieController.isFullScreen
|
||||
? Icons.fullscreen_exit
|
||||
: Icons.fullscreen,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHitArea() {
|
||||
final bool isFinished = _latestValue.position >= _latestValue.duration;
|
||||
final bool showPlayButton =
|
||||
widget.showPlayButton && !_dragging && !notifier.hideStuff;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (_latestValue.isPlaying) {
|
||||
if (_displayTapped) {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
}
|
||||
} else {
|
||||
_playPause();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: Colors.black54,
|
||||
iconColor: Colors.white,
|
||||
isFinished: isFinished,
|
||||
isPlaying: controller.value.isPlaying,
|
||||
show: showPlayButton,
|
||||
onPressed: _playPause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSpeedButtonTap() async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
final chosenSpeed = await showModalBottomSheet<double>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => PlaybackSpeedDialog(
|
||||
speeds: chewieController.playbackSpeeds,
|
||||
selected: _latestValue.playbackSpeed,
|
||||
),
|
||||
);
|
||||
|
||||
if (chosenSpeed != null) {
|
||||
controller.setPlaybackSpeed(chosenSpeed);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
}
|
||||
|
||||
GestureDetector _buildMuteButton(
|
||||
VideoPlayerController controller,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (_latestValue.volume == 0) {
|
||||
controller.setVolume(_latestVolume ?? 0.5);
|
||||
} else {
|
||||
_latestVolume = controller.value.volume;
|
||||
controller.setVolume(0.0);
|
||||
}
|
||||
},
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRect(
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: const EdgeInsets.only(
|
||||
right: 15.0,
|
||||
),
|
||||
child: Icon(
|
||||
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildPlayPause(VideoPlayerController controller) {
|
||||
return GestureDetector(
|
||||
onTap: _playPause,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
margin: const EdgeInsets.only(left: 8.0, right: 4.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
),
|
||||
child: AnimatedPlayPause(
|
||||
playing: controller.value.isPlaying,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPosition(Color? iconColor) {
|
||||
final position = _latestValue.position;
|
||||
final duration = _latestValue.duration;
|
||||
|
||||
return Text(
|
||||
'${formatDuration(position)} / ${formatDuration(duration)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubtitleTap() {
|
||||
setState(() {
|
||||
_subtitleOn = !_subtitleOn;
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelAndRestartTimer() {
|
||||
_hideTimer?.cancel();
|
||||
_startHideTimer();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
_displayTapped = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
|
||||
controller.addListener(_updateState);
|
||||
|
||||
_updateState();
|
||||
|
||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
if (chewieController.showControlsOnInitialize) {
|
||||
_initTimer = Timer(const Duration(milliseconds: 200), () {
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onExpandCollapse() {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
|
||||
chewieController.toggleFullScreen();
|
||||
_showAfterExpandCollapseTimer =
|
||||
Timer(const Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
_cancelAndRestartTimer();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _playPause() {
|
||||
final isFinished = _latestValue.position >= _latestValue.duration;
|
||||
|
||||
setState(() {
|
||||
if (controller.value.isPlaying) {
|
||||
notifier.hideStuff = false;
|
||||
_hideTimer?.cancel();
|
||||
controller.pause();
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (!controller.value.isInitialized) {
|
||||
controller.initialize().then((_) {
|
||||
controller.play();
|
||||
});
|
||||
} else {
|
||||
if (isFinished) {
|
||||
controller.seekTo(Duration.zero);
|
||||
}
|
||||
controller.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
||||
? ChewieController.defaultHideControlsTimer
|
||||
: chewieController.hideControlsTimer;
|
||||
_hideTimer = Timer(hideControlsTimer, () {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _bufferingTimerTimeout() {
|
||||
_displayBufferingIndicator = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
if (!mounted) return;
|
||||
|
||||
// display the progress bar indicator only after the buffering delay if it has been set
|
||||
if (chewieController.progressIndicatorDelay != null) {
|
||||
if (controller.value.isBuffering) {
|
||||
_bufferingDisplayTimer ??= Timer(
|
||||
chewieController.progressIndicatorDelay!,
|
||||
_bufferingTimerTimeout,
|
||||
);
|
||||
} else {
|
||||
_bufferingDisplayTimer?.cancel();
|
||||
_bufferingDisplayTimer = null;
|
||||
_displayBufferingIndicator = false;
|
||||
}
|
||||
} else {
|
||||
_displayBufferingIndicator = controller.value.isBuffering;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_latestValue = controller.value;
|
||||
_subtitlesPosition = controller.value.position;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildProgressBar() {
|
||||
return Expanded(
|
||||
child: MaterialVideoProgressBar(
|
||||
controller,
|
||||
onDragStart: () {
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
});
|
||||
|
||||
_hideTimer?.cancel();
|
||||
},
|
||||
onDragEnd: () {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
});
|
||||
|
||||
_startHideTimer();
|
||||
},
|
||||
colors: chewieController.materialProgressColors ??
|
||||
ChewieProgressColors(
|
||||
playedColor: Theme.of(context).colorScheme.secondary,
|
||||
handleColor: Theme.of(context).colorScheme.secondary,
|
||||
bufferedColor: Theme.of(context).disabledColor.withOpacity(0.5),
|
||||
backgroundColor: Theme.of(context).disabledColor.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/file/video/lib/src/progress_bar.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../chewie.dart';
|
||||
|
||||
class MaterialVideoProgressBar extends StatelessWidget {
|
||||
MaterialVideoProgressBar(
|
||||
this.controller, {
|
||||
this.height = kToolbarHeight,
|
||||
ChewieProgressColors? colors,
|
||||
this.onDragEnd,
|
||||
this.onDragStart,
|
||||
this.onDragUpdate,
|
||||
Key? key,
|
||||
}) : colors = colors ?? ChewieProgressColors(),
|
||||
super(key: key);
|
||||
|
||||
final double height;
|
||||
final VideoPlayerController controller;
|
||||
final ChewieProgressColors colors;
|
||||
final Function()? onDragStart;
|
||||
final Function()? onDragEnd;
|
||||
final Function()? onDragUpdate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return VideoProgressBar(
|
||||
controller,
|
||||
barHeight: 10,
|
||||
handleHeight: 6,
|
||||
drawShadow: true,
|
||||
colors: colors,
|
||||
onDragEnd: onDragEnd,
|
||||
onDragStart: onDragStart,
|
||||
onDragUpdate: onDragUpdate,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../chewie.dart';
|
||||
|
||||
class OptionsDialog extends StatefulWidget {
|
||||
const OptionsDialog({
|
||||
Key? key,
|
||||
required this.options,
|
||||
this.cancelButtonText,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<OptionItem> options;
|
||||
final String? cancelButtonText;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_OptionsDialogState createState() => _OptionsDialogState();
|
||||
}
|
||||
|
||||
class _OptionsDialogState extends State<OptionsDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.options.length,
|
||||
itemBuilder: (context, i) {
|
||||
return ListTile(
|
||||
onTap: widget.options[i].onTap != null
|
||||
? widget.options[i].onTap!
|
||||
: null,
|
||||
leading: Icon(widget.options[i].iconData),
|
||||
title: Text(widget.options[i].title),
|
||||
subtitle: widget.options[i].subtitle != null
|
||||
? Text(widget.options[i].subtitle!)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Divider(
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => Navigator.pop(context),
|
||||
leading: const Icon(Icons.close),
|
||||
title: Text(
|
||||
widget.cancelButtonText ?? 'Cancel',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PlaybackSpeedDialog extends StatelessWidget {
|
||||
const PlaybackSpeedDialog({
|
||||
Key? key,
|
||||
required List<double> speeds,
|
||||
required double selected,
|
||||
}) : _speeds = speeds,
|
||||
_selected = selected,
|
||||
super(key: key);
|
||||
|
||||
final List<double> _speeds;
|
||||
final double _selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color selectedColor = Theme.of(context).primaryColor;
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const ScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final speed = _speeds[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Row(
|
||||
children: [
|
||||
if (speed == _selected)
|
||||
Icon(
|
||||
Icons.check,
|
||||
size: 20.0,
|
||||
color: selectedColor,
|
||||
)
|
||||
else
|
||||
Container(width: 20.0),
|
||||
const SizedBox(width: 16.0),
|
||||
Text(speed.toString()),
|
||||
],
|
||||
),
|
||||
selected: speed == _selected,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(speed);
|
||||
},
|
||||
);
|
||||
},
|
||||
itemCount: _speeds.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
3
flutter_common/lib/utils/file/video/lib/src/models/index.dart
Executable file
3
flutter_common/lib/utils/file/video/lib/src/models/index.dart
Executable file
@@ -0,0 +1,3 @@
|
||||
export 'option_item.dart';
|
||||
export 'options_translation.dart';
|
||||
export 'subtitle_model.dart';
|
||||
48
flutter_common/lib/utils/file/video/lib/src/models/option_item.dart
Executable file
48
flutter_common/lib/utils/file/video/lib/src/models/option_item.dart
Executable file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OptionItem {
|
||||
OptionItem({
|
||||
required this.onTap,
|
||||
required this.iconData,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
Function()? onTap;
|
||||
IconData iconData;
|
||||
String title;
|
||||
String? subtitle;
|
||||
|
||||
OptionItem copyWith({
|
||||
Function()? onTap,
|
||||
IconData? iconData,
|
||||
String? title,
|
||||
String? subtitle,
|
||||
}) {
|
||||
return OptionItem(
|
||||
onTap: onTap ?? this.onTap,
|
||||
iconData: iconData ?? this.iconData,
|
||||
title: title ?? this.title,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'OptionItem(onTap: $onTap, iconData: $iconData, title: $title, subtitle: $subtitle)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is OptionItem &&
|
||||
other.onTap == onTap &&
|
||||
other.iconData == iconData &&
|
||||
other.title == title &&
|
||||
other.subtitle == subtitle;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
onTap.hashCode ^ iconData.hashCode ^ title.hashCode ^ subtitle.hashCode;
|
||||
}
|
||||
44
flutter_common/lib/utils/file/video/lib/src/models/options_translation.dart
Executable file
44
flutter_common/lib/utils/file/video/lib/src/models/options_translation.dart
Executable file
@@ -0,0 +1,44 @@
|
||||
class OptionsTranslation {
|
||||
OptionsTranslation({
|
||||
this.playbackSpeedButtonText,
|
||||
this.subtitlesButtonText,
|
||||
this.cancelButtonText,
|
||||
});
|
||||
|
||||
String? playbackSpeedButtonText;
|
||||
String? subtitlesButtonText;
|
||||
String? cancelButtonText;
|
||||
|
||||
OptionsTranslation copyWith({
|
||||
String? playbackSpeedButtonText,
|
||||
String? subtitlesButtonText,
|
||||
String? cancelButtonText,
|
||||
}) {
|
||||
return OptionsTranslation(
|
||||
playbackSpeedButtonText:
|
||||
playbackSpeedButtonText ?? this.playbackSpeedButtonText,
|
||||
subtitlesButtonText: subtitlesButtonText ?? this.subtitlesButtonText,
|
||||
cancelButtonText: cancelButtonText ?? this.cancelButtonText,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'OptionsTranslation(playbackSpeedButtonText: $playbackSpeedButtonText, subtitlesButtonText: $subtitlesButtonText, cancelButtonText: $cancelButtonText)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is OptionsTranslation &&
|
||||
other.playbackSpeedButtonText == playbackSpeedButtonText &&
|
||||
other.subtitlesButtonText == subtitlesButtonText &&
|
||||
other.cancelButtonText == cancelButtonText;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
playbackSpeedButtonText.hashCode ^
|
||||
subtitlesButtonText.hashCode ^
|
||||
cancelButtonText.hashCode;
|
||||
}
|
||||
67
flutter_common/lib/utils/file/video/lib/src/models/subtitle_model.dart
Executable file
67
flutter_common/lib/utils/file/video/lib/src/models/subtitle_model.dart
Executable file
@@ -0,0 +1,67 @@
|
||||
class Subtitles {
|
||||
Subtitles(this.subtitle);
|
||||
|
||||
final List<Subtitle?> subtitle;
|
||||
|
||||
bool get isEmpty => subtitle.isEmpty;
|
||||
|
||||
bool get isNotEmpty => !isEmpty;
|
||||
|
||||
List<Subtitle?> getByPosition(Duration position) {
|
||||
final found = subtitle.where((item) {
|
||||
if (item != null) return position >= item.start && position <= item.end;
|
||||
return false;
|
||||
}).toList();
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
class Subtitle {
|
||||
Subtitle({
|
||||
required this.index,
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
Subtitle copyWith({
|
||||
int? index,
|
||||
Duration? start,
|
||||
Duration? end,
|
||||
dynamic text,
|
||||
}) {
|
||||
return Subtitle(
|
||||
index: index ?? this.index,
|
||||
start: start ?? this.start,
|
||||
end: end ?? this.end,
|
||||
text: text ?? this.text,
|
||||
);
|
||||
}
|
||||
|
||||
final int index;
|
||||
final Duration start;
|
||||
final Duration end;
|
||||
final dynamic text;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Subtitle(index: $index, start: $start, end: $end, text: $text)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is Subtitle &&
|
||||
other.index == index &&
|
||||
other.start == start &&
|
||||
other.end == end &&
|
||||
other.text == text;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return index.hashCode ^ start.hashCode ^ end.hashCode ^ text.hashCode;
|
||||
}
|
||||
}
|
||||
1
flutter_common/lib/utils/file/video/lib/src/notifiers/index.dart
Executable file
1
flutter_common/lib/utils/file/video/lib/src/notifiers/index.dart
Executable file
@@ -0,0 +1 @@
|
||||
export 'player_notifier.dart';
|
||||
28
flutter_common/lib/utils/file/video/lib/src/notifiers/player_notifier.dart
Executable file
28
flutter_common/lib/utils/file/video/lib/src/notifiers/player_notifier.dart
Executable file
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///
|
||||
/// The new State-Manager for Chewie!
|
||||
/// Has to be an instance of Singleton to survive
|
||||
/// over all State-Changes inside chewie
|
||||
///
|
||||
class PlayerNotifier extends ChangeNotifier {
|
||||
PlayerNotifier._(
|
||||
bool hideStuff,
|
||||
) : _hideStuff = hideStuff;
|
||||
|
||||
bool _hideStuff;
|
||||
|
||||
bool get hideStuff => _hideStuff;
|
||||
|
||||
set hideStuff(bool value) {
|
||||
_hideStuff = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PlayerNotifier init() {
|
||||
return PlayerNotifier._(
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
97
flutter_common/lib/utils/file/video/lib/src/player_with_controls.dart
Executable file
97
flutter_common/lib/utils/file/video/lib/src/player_with_controls.dart
Executable file
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import '../chewie.dart';
|
||||
import 'helpers/adaptive_controls.dart';
|
||||
import 'notifiers/index.dart';
|
||||
|
||||
class PlayerWithControls extends StatelessWidget {
|
||||
const PlayerWithControls({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ChewieController chewieController = ChewieController.of(context);
|
||||
|
||||
double calculateAspectRatio(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final width = size.width;
|
||||
final height = size.height;
|
||||
|
||||
return width > height ? width / height : height / width;
|
||||
}
|
||||
|
||||
Widget buildControls(
|
||||
BuildContext context,
|
||||
ChewieController chewieController,
|
||||
) {
|
||||
return chewieController.showControls
|
||||
? chewieController.customControls ?? const AdaptiveControls()
|
||||
: const SizedBox();
|
||||
}
|
||||
|
||||
Widget buildPlayerWithControls(
|
||||
ChewieController chewieController,
|
||||
BuildContext context,
|
||||
) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
if (chewieController.placeholder != null)
|
||||
chewieController.placeholder!,
|
||||
InteractiveViewer(
|
||||
transformationController: chewieController.transformationController,
|
||||
maxScale: chewieController.maxScale,
|
||||
panEnabled: chewieController.zoomAndPan,
|
||||
scaleEnabled: chewieController.zoomAndPan,
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio:
|
||||
chewieController.videoPlayerController.value.aspectRatio,
|
||||
child: VideoPlayer(chewieController.videoPlayerController),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (chewieController.overlay != null) chewieController.overlay!,
|
||||
if (Theme.of(context).platform != TargetPlatform.iOS)
|
||||
Consumer<PlayerNotifier>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PlayerNotifier notifier,
|
||||
Widget? widget,
|
||||
) =>
|
||||
Visibility(
|
||||
visible: !notifier.hideStuff,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 0.8,
|
||||
duration: const Duration(
|
||||
milliseconds: 250,
|
||||
),
|
||||
child: const DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.black54),
|
||||
child: SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!chewieController.isFullScreen)
|
||||
buildControls(context, chewieController)
|
||||
else
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: buildControls(context, chewieController),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: AspectRatio(
|
||||
aspectRatio: calculateAspectRatio(context),
|
||||
child: buildPlayerWithControls(chewieController, context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
218
flutter_common/lib/utils/file/video/lib/src/progress_bar.dart
Executable file
218
flutter_common/lib/utils/file/video/lib/src/progress_bar.dart
Executable file
@@ -0,0 +1,218 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../chewie.dart';
|
||||
|
||||
class VideoProgressBar extends StatefulWidget {
|
||||
VideoProgressBar(
|
||||
this.controller, {
|
||||
ChewieProgressColors? colors,
|
||||
this.onDragEnd,
|
||||
this.onDragStart,
|
||||
this.onDragUpdate,
|
||||
Key? key,
|
||||
required this.barHeight,
|
||||
required this.handleHeight,
|
||||
required this.drawShadow,
|
||||
}) : colors = colors ?? ChewieProgressColors(),
|
||||
super(key: key);
|
||||
|
||||
final VideoPlayerController controller;
|
||||
final ChewieProgressColors colors;
|
||||
final Function()? onDragStart;
|
||||
final Function()? onDragEnd;
|
||||
final Function()? onDragUpdate;
|
||||
|
||||
final double barHeight;
|
||||
final double handleHeight;
|
||||
final bool drawShadow;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_VideoProgressBarState createState() {
|
||||
return _VideoProgressBarState();
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoProgressBarState extends State<VideoProgressBar> {
|
||||
void listener() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
bool _controllerWasPlaying = false;
|
||||
|
||||
VideoPlayerController get controller => widget.controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.addListener(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
controller.removeListener(listener);
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
void _seekToRelativePosition(Offset globalPosition) {
|
||||
final box = context.findRenderObject()! as RenderBox;
|
||||
final Offset tapPos = box.globalToLocal(globalPosition);
|
||||
final double relative = tapPos.dx / box.size.width;
|
||||
final Duration position = controller.value.duration * relative;
|
||||
controller.seekTo(position);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onHorizontalDragStart: (DragStartDetails details) {
|
||||
if (!controller.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
_controllerWasPlaying = controller.value.isPlaying;
|
||||
if (_controllerWasPlaying) {
|
||||
controller.pause();
|
||||
}
|
||||
|
||||
widget.onDragStart?.call();
|
||||
},
|
||||
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
||||
if (!controller.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
// Should only seek if it's not running on Android, or if it is,
|
||||
// then the VideoPlayerController cannot be buffering.
|
||||
// On Android, we need to let the player buffer when scrolling
|
||||
// in order to let the player buffer. https://github.com/flutter/flutter/issues/101409
|
||||
final shouldSeekToRelativePosition =
|
||||
!Platform.isAndroid || !controller.value.isBuffering;
|
||||
if (shouldSeekToRelativePosition) {
|
||||
_seekToRelativePosition(details.globalPosition);
|
||||
}
|
||||
|
||||
widget.onDragUpdate?.call();
|
||||
},
|
||||
onHorizontalDragEnd: (DragEndDetails details) {
|
||||
if (_controllerWasPlaying) {
|
||||
controller.play();
|
||||
}
|
||||
|
||||
widget.onDragEnd?.call();
|
||||
},
|
||||
onTapDown: (TapDownDetails details) {
|
||||
if (!controller.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
_seekToRelativePosition(details.globalPosition);
|
||||
},
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
color: Colors.transparent,
|
||||
child: CustomPaint(
|
||||
painter: _ProgressBarPainter(
|
||||
value: controller.value,
|
||||
colors: widget.colors,
|
||||
barHeight: widget.barHeight,
|
||||
handleHeight: widget.handleHeight,
|
||||
drawShadow: widget.drawShadow,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressBarPainter extends CustomPainter {
|
||||
_ProgressBarPainter({
|
||||
required this.value,
|
||||
required this.colors,
|
||||
required this.barHeight,
|
||||
required this.handleHeight,
|
||||
required this.drawShadow,
|
||||
});
|
||||
|
||||
VideoPlayerValue value;
|
||||
ChewieProgressColors colors;
|
||||
|
||||
final double barHeight;
|
||||
final double handleHeight;
|
||||
final bool drawShadow;
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter painter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final baseOffset = size.height / 2 - barHeight / 2;
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromPoints(
|
||||
Offset(0.0, baseOffset),
|
||||
Offset(size.width, baseOffset + barHeight),
|
||||
),
|
||||
const Radius.circular(4.0),
|
||||
),
|
||||
colors.backgroundPaint,
|
||||
);
|
||||
if (!value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
final double playedPartPercent =
|
||||
value.position.inMilliseconds / value.duration.inMilliseconds;
|
||||
final double playedPart =
|
||||
playedPartPercent > 1 ? size.width : playedPartPercent * size.width;
|
||||
for (final DurationRange range in value.buffered) {
|
||||
final double start = range.startFraction(value.duration) * size.width;
|
||||
final double end = range.endFraction(value.duration) * size.width;
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromPoints(
|
||||
Offset(start, baseOffset),
|
||||
Offset(end, baseOffset + barHeight),
|
||||
),
|
||||
const Radius.circular(4.0),
|
||||
),
|
||||
colors.bufferedPaint,
|
||||
);
|
||||
}
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromPoints(
|
||||
Offset(0.0, baseOffset),
|
||||
Offset(playedPart, baseOffset + barHeight),
|
||||
),
|
||||
const Radius.circular(4.0),
|
||||
),
|
||||
colors.playedPaint,
|
||||
);
|
||||
|
||||
if (drawShadow) {
|
||||
final Path shadowPath = Path()
|
||||
..addOval(
|
||||
Rect.fromCircle(
|
||||
center: Offset(playedPart, baseOffset + barHeight / 2),
|
||||
radius: handleHeight,
|
||||
),
|
||||
);
|
||||
|
||||
canvas.drawShadow(shadowPath, Colors.black, 0.2, false);
|
||||
}
|
||||
|
||||
canvas.drawCircle(
|
||||
Offset(playedPart, baseOffset + barHeight / 2),
|
||||
handleHeight,
|
||||
colors.handlePaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
89
flutter_common/lib/utils/file/webview/customer_pdf.dart
Executable file
89
flutter_common/lib/utils/file/webview/customer_pdf.dart
Executable file
@@ -0,0 +1,89 @@
|
||||
// import 'dart:async';
|
||||
//
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_cached_pdfview/flutter_cached_pdfview.dart';
|
||||
//
|
||||
// class CustomerPDFPage extends StatefulWidget {
|
||||
// final String filePath;
|
||||
// const CustomerPDFPage({
|
||||
// super.key,
|
||||
// required this.filePath,
|
||||
// });
|
||||
//
|
||||
// @override
|
||||
// State<CustomerPDFPage> createState() => _CustomerPDFPageState();
|
||||
// }
|
||||
//
|
||||
// class _CustomerPDFPageState extends State<CustomerPDFPage> {
|
||||
// final StreamController<String> _pageCountController =
|
||||
// StreamController<String>();
|
||||
// final Completer<PDFViewController> _pdfViewController =
|
||||
// Completer<PDFViewController>();
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// body: SafeArea(
|
||||
// child: Stack(
|
||||
// children: [
|
||||
// PDF(
|
||||
// onPageChanged: (int? current, int? total) =>
|
||||
// _pageCountController.add('${current! + 1} - $total'),
|
||||
// onViewCreated: (PDFViewController pdfViewController) async {
|
||||
// _pdfViewController.complete(pdfViewController);
|
||||
// final int currentPage =
|
||||
// await pdfViewController.getCurrentPage() ?? 0;
|
||||
// final int? pageCount = await pdfViewController.getPageCount();
|
||||
// _pageCountController.add('${currentPage + 1} - $pageCount');
|
||||
// },
|
||||
// ).cachedFromUrl(
|
||||
// widget.filePath,
|
||||
// placeholder: (progress) => Center(child: Text('$progress %')),
|
||||
// errorWidget: (error) => Center(child: Text(error.toString())),
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// GestureDetector(
|
||||
// onTap: () => Navigator.pop(context),
|
||||
// child: Container(
|
||||
// margin: const EdgeInsets.only(left: 16, top: 16),
|
||||
// width: 44,
|
||||
// height: 44,
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(22),
|
||||
// color: Colors.grey.withOpacity(0.3),
|
||||
// ),
|
||||
// child: const Icon(
|
||||
// Icons.navigate_before,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Container(
|
||||
// margin: const EdgeInsets.only(right: 16, top: 16),
|
||||
// width: 88,
|
||||
// height: 44,
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(22),
|
||||
// color: Colors.grey.withOpacity(0.3),
|
||||
// ),
|
||||
// child: StreamBuilder<String>(
|
||||
// stream: _pageCountController.stream,
|
||||
// builder: (_, AsyncSnapshot<String> snapshot) {
|
||||
// if (snapshot.hasData) {
|
||||
// return Center(
|
||||
// child: Text(snapshot.data!),
|
||||
// );
|
||||
// }
|
||||
// return const SizedBox();
|
||||
// }),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
149
flutter_common/lib/utils/file/webview/customer_webview.dart
Executable file
149
flutter_common/lib/utils/file/webview/customer_webview.dart
Executable file
@@ -0,0 +1,149 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
|
||||
class CustomerWebView extends StatefulWidget {
|
||||
final String url;
|
||||
final bool? hideBack;
|
||||
|
||||
const CustomerWebView({
|
||||
super.key,
|
||||
required this.url,
|
||||
this.hideBack,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomerWebView> createState() => _CustomerWebViewState();
|
||||
}
|
||||
|
||||
class _CustomerWebViewState extends State<CustomerWebView> {
|
||||
final GlobalKey webViewKey = GlobalKey();
|
||||
|
||||
InAppWebViewController? webViewController;
|
||||
InAppWebViewSettings settings = InAppWebViewSettings(
|
||||
isInspectable: kDebugMode,
|
||||
mediaPlaybackRequiresUserGesture: false,
|
||||
allowsInlineMediaPlayback: true,
|
||||
iframeAllow: "camera; microphone",
|
||||
iframeAllowFullscreen: true);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
webViewController?.loadUrl(urlRequest: URLRequest(url: WebUri(widget.url)));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: [SystemUiOverlay.bottom]);
|
||||
return Stack(
|
||||
alignment: Alignment.topLeft,
|
||||
children: [
|
||||
InAppWebView(
|
||||
key: webViewKey,
|
||||
// webViewEnvironment: webViewEnvironment,
|
||||
initialUrlRequest: URLRequest(url: WebUri(widget.url)),
|
||||
// initialUrlRequest:
|
||||
// URLRequest(url: WebUri(Uri.base.toString().replaceFirst("/#/", "/") + 'page.html')),
|
||||
// initialFile: "assets/index.html",
|
||||
initialUserScripts: UnmodifiableListView<UserScript>([]),
|
||||
initialSettings: settings,
|
||||
// contextMenu: contextMenu,
|
||||
// pullToRefreshController: pullToRefreshController,
|
||||
onWebViewCreated: (controller) async {
|
||||
webViewController = controller;
|
||||
},
|
||||
onLoadStart: (controller, url) {
|
||||
setState(() {
|
||||
// this.url = url.toString();
|
||||
// urlController.text = this.url;
|
||||
});
|
||||
},
|
||||
// onPermissionRequest: (controller, request) {
|
||||
// return PermissionResponse(
|
||||
// resources: request.resources,
|
||||
// action: PermissionResponseAction.GRANT);
|
||||
// },
|
||||
// shouldOverrideUrlLoading:
|
||||
// (controller, navigationAction) async {
|
||||
// var uri = navigationAction.request.url!;
|
||||
//
|
||||
// if (![
|
||||
// "http",
|
||||
// "https",
|
||||
// "file",
|
||||
// "chrome",
|
||||
// "data",
|
||||
// "javascript",
|
||||
// "about"
|
||||
// ].contains(uri.scheme)) {
|
||||
// if (await canLaunchUrl(uri)) {
|
||||
// // Launch the App
|
||||
// await launchUrl(
|
||||
// uri,
|
||||
// );
|
||||
// // and cancel the request
|
||||
// return NavigationActionPolicy.CANCEL;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return NavigationActionPolicy.ALLOW;
|
||||
// },
|
||||
// onLoadStop: (controller, url) {
|
||||
// pullToRefreshController?.endRefreshing();
|
||||
// setState(() {
|
||||
// this.url = url.toString();
|
||||
// urlController.text = this.url;
|
||||
// });
|
||||
// },
|
||||
// onReceivedError: (controller, request, error) {
|
||||
// pullToRefreshController?.endRefreshing();
|
||||
// },
|
||||
// onProgressChanged: (controller, progress) {
|
||||
// if (progress == 100) {
|
||||
// pullToRefreshController?.endRefreshing();
|
||||
// }
|
||||
// setState(() {
|
||||
// this.progress = progress / 100;
|
||||
// urlController.text = this.url;
|
||||
// });
|
||||
// },
|
||||
// onUpdateVisitedHistory: (controller, url, isReload) {
|
||||
// setState(() {
|
||||
// this.url = url.toString();
|
||||
// urlController.text = this.url;
|
||||
// });
|
||||
// },
|
||||
onConsoleMessage: (controller, consoleMessage) {
|
||||
print(consoleMessage);
|
||||
},
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.hideBack != true,
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 16, top: 16),
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.navigate_before,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
53
flutter_common/lib/utils/frosted_glass_effect_widget.dart
Normal file
53
flutter_common/lib/utils/frosted_glass_effect_widget.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class FrostedGlassEffectWidget extends StatelessWidget {
|
||||
final Widget? child;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final double? borderRadius;
|
||||
final Widget? backgroundChild;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const FrostedGlassEffectWidget({
|
||||
super.key,
|
||||
this.child,
|
||||
this.width,
|
||||
this.height,
|
||||
this.borderRadius,
|
||||
this.backgroundChild,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
// 背景内容(可以放图片或其他内容)
|
||||
backgroundChild ?? SizedBox(),
|
||||
// 毛玻璃效果
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 20.h),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10.0,
|
||||
sigmaY: 10.0,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 20.h),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
flutter_common/lib/utils/launch_utils.dart
Executable file
42
flutter_common/lib/utils/launch_utils.dart
Executable file
@@ -0,0 +1,42 @@
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
enum LaunchType { tel, sms, email, link }
|
||||
|
||||
const launchTypeValues = {
|
||||
LaunchType.tel: "tel:",
|
||||
LaunchType.sms: "sms:",
|
||||
LaunchType.email: "mailto:",
|
||||
LaunchType.link: ''
|
||||
};
|
||||
|
||||
class LaunchUtils {
|
||||
///自定义Launch方法
|
||||
static Future<bool> customLaunch({
|
||||
required String urlString,
|
||||
LaunchType launchType = LaunchType.link,
|
||||
bool enableJavaScript = false,
|
||||
bool enableDomStorage = false,
|
||||
Map<String, String> headers = const <String, String>{},
|
||||
LaunchMode mode = LaunchMode.externalApplication,
|
||||
String? webOnlyWindowName,
|
||||
}) {
|
||||
return launchUrl(
|
||||
Uri.parse('${launchTypeValues[launchType]}$urlString'),
|
||||
webViewConfiguration: WebViewConfiguration(
|
||||
enableJavaScript: enableJavaScript,
|
||||
enableDomStorage: enableDomStorage,
|
||||
headers: headers,
|
||||
),
|
||||
mode: mode,
|
||||
webOnlyWindowName: webOnlyWindowName,
|
||||
);
|
||||
}
|
||||
|
||||
///判断是否打开链接
|
||||
static Future<bool> customCanLaunch(
|
||||
String urlString, {
|
||||
LaunchType launchType = LaunchType.link,
|
||||
}) async {
|
||||
return canLaunchUrl(Uri.parse('${launchTypeValues[launchType]}$urlString'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
///封装刷新加载pull_to_refresh控件
|
||||
class PullRefreshListWidget extends StatelessWidget {
|
||||
final RefreshController controller;
|
||||
|
||||
// final NullableIndexedWidgetBuilder itemBuilder;
|
||||
// final int itemCount;
|
||||
final bool enablePullUp;
|
||||
final bool enablePullDown;
|
||||
final bool shrinkWrap;
|
||||
final VoidCallback? onRefresh;
|
||||
final VoidCallback? onLoading;
|
||||
|
||||
// final EdgeInsetsGeometry? padding;
|
||||
final Widget? header;
|
||||
final Widget? footer;
|
||||
final Widget? placeholder;
|
||||
|
||||
// final ScrollPhysics? physics;
|
||||
final bool isWhiteTheme;
|
||||
final Widget? child;
|
||||
|
||||
const PullRefreshListWidget({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
// required this.itemBuilder,
|
||||
// required this.itemCount,
|
||||
this.enablePullUp = false,
|
||||
this.enablePullDown = true,
|
||||
this.shrinkWrap = true,
|
||||
this.onRefresh,
|
||||
this.onLoading,
|
||||
// this.padding,
|
||||
this.header,
|
||||
this.footer,
|
||||
this.placeholder,
|
||||
// this.physics,
|
||||
this.isWhiteTheme = false,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SmartRefresher(
|
||||
header: header ??
|
||||
ClassicHeader(
|
||||
releaseText: '松开刷新',
|
||||
refreshingText: '正在刷新...',
|
||||
completeText: '刷新成功',
|
||||
failedText: '刷新失败',
|
||||
idleText: '下拉刷新',
|
||||
textStyle:
|
||||
TextStyle(color: isWhiteTheme ? Colors.white : Colors.grey),
|
||||
idleIcon: Icon(Icons.arrow_downward,
|
||||
color: isWhiteTheme ? Colors.white : Colors.grey),
|
||||
failedIcon: Icon(Icons.error,
|
||||
color: isWhiteTheme ? Colors.white : Colors.grey),
|
||||
completeIcon: Icon(Icons.done,
|
||||
color: isWhiteTheme ? Colors.white : Colors.grey),
|
||||
releaseIcon: Icon(Icons.refresh,
|
||||
color: isWhiteTheme ? Colors.white : Colors.grey),
|
||||
),
|
||||
footer: footer ??
|
||||
ClassicFooter(
|
||||
loadingText: '正在加载...',
|
||||
noDataText: '没有更多数据',
|
||||
idleText: '加载更多',
|
||||
failedText: '加载失败',
|
||||
canLoadingText: '松开加载更多',
|
||||
textStyle:
|
||||
TextStyle(color: isWhiteTheme ? Colors.white : Colors.grey),
|
||||
idleIcon: Icon(Icons.arrow_downward,
|
||||
color: isWhiteTheme ? Colors.white : Colors.grey),
|
||||
failedIcon: Icon(Icons.error,
|
||||
color: isWhiteTheme ? Colors.white : Colors.grey),
|
||||
),
|
||||
enablePullDown: enablePullDown,
|
||||
enablePullUp: enablePullUp,
|
||||
controller: controller,
|
||||
onRefresh: onRefresh,
|
||||
onLoading: onLoading,
|
||||
child: child ??
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
child: placeholder ?? const Text('暂无相关数据'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
134
flutter_common/lib/utils/string_utils.dart
Executable file
134
flutter_common/lib/utils/string_utils.dart
Executable file
@@ -0,0 +1,134 @@
|
||||
// import 'package:intl/intl.dart';
|
||||
|
||||
extension StringUtils on String? {
|
||||
String get hidePhone => StringUtils._phoneString(phone: this);
|
||||
|
||||
String get hideUserName => StringUtils._userNameString(name: this);
|
||||
|
||||
bool get isNumeric => StringUtils._isNumeric(str: this);
|
||||
|
||||
String get toPrice => StringUtils._cutOutPrice(price: this);
|
||||
|
||||
String get toSplMoney => StringUtils._splNumberInsyo(str: this);
|
||||
|
||||
String get toThousandPrice => StringUtils._formatThousandPrice(price: this);
|
||||
|
||||
double get toDouble => StringUtils._strToDouble(str: this);
|
||||
|
||||
///隐藏手机号中间4位数
|
||||
static String _phoneString({String? phone}) {
|
||||
if (phone == null || phone == '') return '';
|
||||
return phone.replaceFirst(RegExp(r'\d{4}'), '****', 3);
|
||||
}
|
||||
|
||||
///隐藏姓名(只显示姓 隐藏名字)
|
||||
static String _userNameString({String? name}) {
|
||||
if (name == null || name == '') return '';
|
||||
return name.replaceAll(name.substring(1, name.length), '**');
|
||||
}
|
||||
|
||||
///判断是否是纯数字
|
||||
static bool _isNumeric({String? str}) {
|
||||
if (str == null) {
|
||||
return false;
|
||||
}
|
||||
return double.tryParse(str) != null;
|
||||
}
|
||||
|
||||
///将金额转换为小数点后两位
|
||||
static String _cutOutPrice({String? price}) {
|
||||
String cutOutPrice = '0.00';
|
||||
if (price != null && price != '') {
|
||||
var newPrice = double.tryParse(price);
|
||||
if (newPrice != null) {
|
||||
cutOutPrice = newPrice.toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
return cutOutPrice;
|
||||
}
|
||||
|
||||
///金额处理千分位金额
|
||||
static String _formatThousandPrice({String? price}) {
|
||||
String priceStr = price ?? '';
|
||||
try {
|
||||
num? money = num.tryParse(priceStr);
|
||||
int truncateMoney = money?.truncate() ?? 0;
|
||||
if (truncateMoney >= 1000) {
|
||||
// NumberFormat format = NumberFormat('0,000');
|
||||
return _formatNum(truncateMoney);
|
||||
} else {
|
||||
List<String> resultList = priceStr.split(".");
|
||||
if (resultList.isNotEmpty) {
|
||||
return priceStr.split(".").first;
|
||||
} else {
|
||||
return priceStr;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static String _formatNum(num, {point = 3}) {
|
||||
if (num != null) {
|
||||
String str = double.parse(num.toString()).toString();
|
||||
// 分开截取
|
||||
List<String> sub = str.split('.');
|
||||
// 处理值
|
||||
List val = List.from(sub[0].split(''));
|
||||
// 处理点
|
||||
List<String> points = List.from(sub[1].split(''));
|
||||
//处理分割符
|
||||
for (int index = 0, i = val.length - 1; i >= 0; index++, i--) {
|
||||
// 除以三没有余数、不等于零并且不等于1 就加个逗号
|
||||
if (index % 3 == 0 && index != 0 && i != 1) val[i] = val[i] + ',';
|
||||
}
|
||||
// 处理小数点
|
||||
for (int i = 0; i <= point - points.length; i++) {
|
||||
points.add('0');
|
||||
}
|
||||
//如果大于长度就截取
|
||||
if (points.length > point) {
|
||||
// 截取数组
|
||||
points = points.sublist(0, point);
|
||||
}
|
||||
// 判断是否有长度
|
||||
if (points.length > 0) {
|
||||
return '${val.join('')}.${points.join('')}';
|
||||
} else {
|
||||
return val.join('');
|
||||
}
|
||||
} else {
|
||||
return "0.0";
|
||||
}
|
||||
}
|
||||
|
||||
///将数字转换位千分位隔开
|
||||
static String _splNumberInsyo({String? str}) {
|
||||
if (str?.isEmpty == true) {
|
||||
return '';
|
||||
}
|
||||
// num amount = num.parse(str ?? '0');
|
||||
// final NumberFormat formatter = NumberFormat.currency(
|
||||
// locale: 'en_US', // 设置为你的本地化Locale
|
||||
// decimalDigits: 0,
|
||||
// name: '', // 设置货币符号,例如 'USD' 或 'EUR'
|
||||
// );
|
||||
// return formatter.format(amount);
|
||||
return str ?? '';
|
||||
}
|
||||
|
||||
///将字符串转为double
|
||||
static double _strToDouble({String? str}) {
|
||||
String newStr = str ?? '';
|
||||
try {
|
||||
double? doubleValue = double.tryParse(newStr);
|
||||
if (doubleValue != null) {
|
||||
return doubleValue;
|
||||
}
|
||||
return 0;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
220
flutter_common/lib/utils/toast_utils.dart
Executable file
220
flutter_common/lib/utils/toast_utils.dart
Executable file
@@ -0,0 +1,220 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/custom_dialog.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// import 'package:get/get.dart';
|
||||
|
||||
class ToastUtils {
|
||||
///提示框
|
||||
static showToast({
|
||||
String? msg,
|
||||
Color? backgroundColor,
|
||||
TextStyle? textStyle = const TextStyle(color: Colors.white),
|
||||
}) async {
|
||||
EasyLoading.showToast(msg ?? '');
|
||||
// _cancelToast();
|
||||
}
|
||||
|
||||
///加载框
|
||||
static showLoading({String? text}) async {
|
||||
await EasyLoading.show(
|
||||
// status: 'loading...',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
}
|
||||
|
||||
///成功弹窗提示
|
||||
static successToast({String? successText}) {
|
||||
EasyLoading.showError(
|
||||
successText ?? '成功',
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
cancelToast();
|
||||
}
|
||||
|
||||
///失败弹窗提示
|
||||
static errorToast({String? errorText}) {
|
||||
EasyLoading.showError(
|
||||
errorText ?? '错误',
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
cancelToast();
|
||||
}
|
||||
|
||||
///底部自适应高度弹窗
|
||||
///底部弹窗
|
||||
static showBottomSheet({
|
||||
required BuildContext context,
|
||||
bool isTime = false,
|
||||
double? height,
|
||||
Function? onConfirm,
|
||||
String? title,
|
||||
double? titleFontSize,
|
||||
double? leftIconSize,
|
||||
Widget? contentWidget,
|
||||
Widget? header,
|
||||
bool isShowConfirm = false,
|
||||
Color? barrierColor,
|
||||
EdgeInsetsGeometry? padding,
|
||||
}) {
|
||||
cancelToast();
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: MediaQuery.of(context).size.height / 2,
|
||||
margin: EdgeInsets.only(
|
||||
top: height == null
|
||||
? MediaQuery.of(context).size.height / 2
|
||||
: (MediaQuery.of(context).size.height - height),
|
||||
),
|
||||
padding: padding ?? const EdgeInsets.only(bottom: 40),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
header ??
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 5),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom:
|
||||
BorderSide(color: Color(0xffE1E1E1), width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 6, right: 10),
|
||||
color: Colors.transparent,
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
size: leftIconSize ?? 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
title ?? '头部',
|
||||
style: TextStyle(
|
||||
color: const Color(0xff333333),
|
||||
fontSize: titleFontSize ?? 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (isShowConfirm) {
|
||||
if (onConfirm != null) {
|
||||
onConfirm();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
right: 18,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
color: isShowConfirm
|
||||
? const Color(0xff4D6FD5)
|
||||
: Colors.transparent,
|
||||
fontSize: 16),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: contentWidget ?? const SizedBox())
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static cancelToast() {
|
||||
// 延时2秒
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
|
||||
///显示对话框
|
||||
static showAlterDialog({
|
||||
VoidCallback? confirmCallback,
|
||||
VoidCallback? cancelCallback,
|
||||
String? titleText,
|
||||
String? contentText,
|
||||
String? confirmText,
|
||||
TextStyle? confirmStyle,
|
||||
TextStyle? cancelStyle,
|
||||
}) {
|
||||
cancelToast();
|
||||
return Get.dialog(
|
||||
CustomDialog(
|
||||
title: titleText ?? '温馨提示',
|
||||
content: contentText ?? '您确定要退出当前登录吗?',
|
||||
cancelText: "取消",
|
||||
confirmText: "确定",
|
||||
cancelTextStyle: cancelStyle,
|
||||
confirmTextStyle: confirmStyle,
|
||||
cancelCall: () {
|
||||
Get.back();
|
||||
Future.delayed(const Duration(milliseconds: 50)).then((value) {
|
||||
if (cancelCallback != null) {
|
||||
cancelCallback();
|
||||
}
|
||||
});
|
||||
},
|
||||
confirmCall: () {
|
||||
Get.back();
|
||||
if (confirmCallback != null) {
|
||||
confirmCallback();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///错误信息弹窗
|
||||
static showExceptionToast({String? title, String? msg}) {
|
||||
_cancelToast();
|
||||
return Get.snackbar(
|
||||
title ?? '错误信息',
|
||||
msg ?? '错误信息内容',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
colorText: Colors.white,
|
||||
backgroundColor: Colors.red[800],
|
||||
margin: const EdgeInsets.only(bottom: 10, left: 10, right: 10),
|
||||
borderRadius: 4,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
|
||||
///取消弹窗
|
||||
static _cancelToast() {
|
||||
// Toast.dismissAllToast();
|
||||
}
|
||||
}
|
||||
25
flutter_common/lib/value/string.dart
Executable file
25
flutter_common/lib/value/string.dart
Executable file
@@ -0,0 +1,25 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
|
||||
class BaseStringValue {
|
||||
static String chinaMoney = '¥';
|
||||
|
||||
///md5 加密 32位
|
||||
static String generate_MD5(String data) {
|
||||
var content = const Utf8Encoder().convert(data);
|
||||
var digest = md5.convert(content);
|
||||
// 这里其实就是 digest.toString()
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
///cell key 随机数+时间
|
||||
static String cellKeyString({String? string}) {
|
||||
var random = Random();
|
||||
int randomNumber = random.nextInt(10000); // 生成0到10000000000000之间的随机整数
|
||||
return '$randomNumber + $string + ${DateTime.now().toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user