feat(create):添加 files

This commit is contained in:
2025-10-20 10:44:59 +08:00
parent e0e47278e1
commit 35d26643ba
56 changed files with 13176 additions and 0 deletions

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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 'NetResultstatusCode: $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();
}
}

View 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),
];
}

View 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),
),
),
),
],
),
);
}
}

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

View 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,
),
)
],
);
}
}

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

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

View 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;
// }
// }
}

View 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,
),
),
),
)
],
),
);
}
}

View 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);
}
}
}

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

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

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

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

View 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);
// },
// );
}
}

View 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,
),
),
],
),
)
],
);
}
}

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

View 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,
),
),
),
],
),
);
}
}

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

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

View 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';

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

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

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

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

View 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(),
);
}
}

View File

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

View File

@@ -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'),
),
),
);
}
}

View 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();
}
}
}

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

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

View File

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

View File

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

View File

@@ -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',
),
),
],
),
);
}
}

View File

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

View File

@@ -0,0 +1,3 @@
export 'option_item.dart';
export 'options_translation.dart';
export 'subtitle_model.dart';

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

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

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

View File

@@ -0,0 +1 @@
export 'player_notifier.dart';

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

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

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

View 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();
// }),
// ),
// ],
// ),
// ],
// ),
// ),
// );
// }
// }

View 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,
),
),
),
)
],
);
}
}

View 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,
),
),
),
],
),
);
}
}

View 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'));
}
}

View File

@@ -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('暂无相关数据'),
),
),
],
),
);
}
}

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

View 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();
}
}

View 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()}';
}
}