feat(delete):删除里面文件夹
This commit is contained in:
53
lib/utils/PermissionUtil.dart
Normal file
53
lib/utils/PermissionUtil.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'dart:io';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class PermissionUtil{
|
||||
/// 安卓权限
|
||||
static List<Permission> androidPermissions = <Permission>[
|
||||
// 在这里添加需要的权限
|
||||
Permission.storage
|
||||
];
|
||||
|
||||
/// ios权限
|
||||
static List<Permission> iosPermissions = <Permission>[
|
||||
// 在这里添加需要的权限
|
||||
Permission.storage
|
||||
];
|
||||
|
||||
static Future<Map<Permission, PermissionStatus>> requestAll() async {
|
||||
if (Platform.isIOS) {
|
||||
return await iosPermissions.request();
|
||||
}
|
||||
return await androidPermissions.request();
|
||||
}
|
||||
|
||||
static Future<Map<Permission, PermissionStatus>> request(
|
||||
Permission permission) async {
|
||||
final List<Permission> permissions = <Permission>[permission];
|
||||
return await permissions.request();
|
||||
}
|
||||
|
||||
static bool isDenied(Map<Permission, PermissionStatus> result) {
|
||||
var isDenied = false;
|
||||
result.forEach((key, value) {
|
||||
if (value == PermissionStatus.denied) {
|
||||
isDenied = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
return isDenied;
|
||||
}
|
||||
|
||||
|
||||
/// 检查权限
|
||||
static Future<bool> checkGranted(Permission permission) async {
|
||||
PermissionStatus storageStatus = await permission.status;
|
||||
if (storageStatus == PermissionStatus.granted) {
|
||||
//已授权
|
||||
return true;
|
||||
} else {
|
||||
//拒绝授权
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
lib/utils/custom_dialog.dart
Executable file
155
lib/utils/custom_dialog.dart
Executable file
@@ -0,0 +1,155 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
///@Author ouyangyan
|
||||
///@Time 2023/3/1 13:43
|
||||
///自定义对话框
|
||||
class CustomDialog extends Dialog {
|
||||
final String title; //标题
|
||||
final String content; //内容
|
||||
final String cancelText; //"取消" 按钮文字
|
||||
final String confirmText; //"确定" 按钮文字
|
||||
final VoidCallback cancelCall; //取消按钮回调
|
||||
final VoidCallback confirmCall; //确定按钮回调
|
||||
final TextStyle? cancelTextStyle; //"取消" 按钮文字
|
||||
final TextStyle? confirmTextStyle; //"确定" 按钮文字
|
||||
|
||||
const CustomDialog({
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.cancelText,
|
||||
required this.confirmText,
|
||||
required this.cancelCall,
|
||||
required this.confirmCall,
|
||||
this.cancelTextStyle,
|
||||
this.confirmTextStyle,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 45),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
decoration: const ShapeDecoration(
|
||||
color: Color(0xffffffff),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Text(
|
||||
title,
|
||||
textWidthBasis: TextWidthBasis.parent,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff222222),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 20,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: Text(
|
||||
content,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff999999),
|
||||
fontSize: 13,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 5,
|
||||
),
|
||||
),
|
||||
lineWidget(height: 0.5),
|
||||
_bottomButtonGroupWidget(),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///底部按钮组
|
||||
Widget _bottomButtonGroupWidget() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: buttonWidget(
|
||||
title: cancelText,
|
||||
callback: cancelCall,
|
||||
textStyle: cancelTextStyle ??
|
||||
const TextStyle(
|
||||
color: Color(0xff888888),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
lineWidget(width: 0.5, height: 50),
|
||||
Expanded(
|
||||
child: buttonWidget(
|
||||
title: confirmText,
|
||||
callback: confirmCall,
|
||||
textStyle: confirmTextStyle ??
|
||||
const TextStyle(
|
||||
color: Color(0xffFF4D1A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
///按钮共用
|
||||
Widget buttonWidget({
|
||||
required String title,
|
||||
required TextStyle textStyle,
|
||||
required VoidCallback callback,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: callback,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: double.infinity,
|
||||
alignment: Alignment.center,
|
||||
child: Text(title, style: textStyle),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///线
|
||||
Widget lineWidget({
|
||||
double? width,
|
||||
double? height,
|
||||
EdgeInsetsGeometry? margin,
|
||||
}) {
|
||||
return Container(
|
||||
width: width,
|
||||
color: const Color(0xffe9e9e9),
|
||||
height: height,
|
||||
margin: margin,
|
||||
);
|
||||
}
|
||||
}
|
||||
250
lib/utils/customer.dart
Normal file
250
lib/utils/customer.dart
Normal file
@@ -0,0 +1,250 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
// import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
|
||||
|
||||
|
||||
///字体样式
|
||||
class CustomerTextStyle extends TextStyle {
|
||||
final Color? customerColor;
|
||||
final double? customerFontSize;
|
||||
final FontWeight? customerFontWeight;
|
||||
final TextDecoration? customerDecoration;
|
||||
final Color? customerDecorationColor;
|
||||
|
||||
const CustomerTextStyle({
|
||||
this.customerFontSize,
|
||||
this.customerDecorationColor,
|
||||
this.customerFontWeight,
|
||||
this.customerDecoration,
|
||||
this.customerColor,
|
||||
}) : super(
|
||||
color: customerColor ?? const Color(0xff333333),
|
||||
fontSize: customerFontSize,
|
||||
fontWeight: customerFontWeight,
|
||||
decoration: customerDecoration,
|
||||
decorationColor: customerDecorationColor,
|
||||
);
|
||||
}
|
||||
|
||||
///基础 Container
|
||||
class CustomerContainer extends StatelessWidget {
|
||||
final Color? color;
|
||||
final double? borderRadius;
|
||||
final Widget? child;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final double? height;
|
||||
final Gradient? gradient;
|
||||
final DecorationImage? image;
|
||||
final VoidCallback? onTap;
|
||||
final List<BoxShadow>? boxShadow;
|
||||
|
||||
const CustomerContainer({
|
||||
super.key,
|
||||
this.color,
|
||||
this.borderRadius,
|
||||
this.child,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.height,
|
||||
this.gradient,
|
||||
this.image,
|
||||
this.onTap,
|
||||
this.boxShadow,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: height,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: color ?? Colors.white,
|
||||
borderRadius: BorderRadius.circular(borderRadius?.h ?? 10.h),
|
||||
gradient: gradient,
|
||||
image: image,
|
||||
boxShadow: boxShadow,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///图片加载
|
||||
class CustomerImagesNetworking extends StatelessWidget {
|
||||
final String imageUrl;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit? fit;
|
||||
final Widget? errorWidget;
|
||||
|
||||
const CustomerImagesNetworking({
|
||||
super.key,
|
||||
required this.imageUrl,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit,
|
||||
this.errorWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Image.network(
|
||||
key: Key(imageUrl),
|
||||
imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
errorBuilder: (_, object, s) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: EdgeInsets.all((width ?? 0) / 2),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/noContainer.png',
|
||||
// width: width /2,
|
||||
// height: width /2,
|
||||
fit: fit ?? BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return imageUrl.contains('http') == true
|
||||
? FadeInImage.memoryNetwork(
|
||||
placeholder: kTransparentImage,
|
||||
width: width,
|
||||
height: height,
|
||||
image: imageUrl,
|
||||
fit: fit,
|
||||
placeholderErrorBuilder:
|
||||
(_, Object object, StackTrace? stackTrace) {
|
||||
return errorWidget ?? const SizedBox();
|
||||
},
|
||||
imageErrorBuilder: (_, Object object, StackTrace? stackTrace) {
|
||||
return errorWidget ?? const SizedBox();
|
||||
},
|
||||
)
|
||||
: errorWidget ?? const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
///money字体样式
|
||||
class CustomerMoneyText extends StatelessWidget {
|
||||
final String money;
|
||||
final double? moneyFontSize;
|
||||
final Color? moneyColor;
|
||||
final FontWeight? moneyFontWeight;
|
||||
final String? unit;
|
||||
final double? unitFontSize;
|
||||
final Color? unitColor;
|
||||
final FontWeight? unitFontWeight;
|
||||
final String? rightUnit;
|
||||
final double? rightUnitFontSize;
|
||||
final Color? rightUnitColor;
|
||||
final FontWeight? rightUnitFontWeight;
|
||||
|
||||
const CustomerMoneyText({
|
||||
super.key,
|
||||
required this.money,
|
||||
this.moneyFontSize,
|
||||
this.moneyColor,
|
||||
this.moneyFontWeight,
|
||||
this.unit,
|
||||
this.unitFontSize,
|
||||
this.unitColor,
|
||||
this.unitFontWeight,
|
||||
this.rightUnit,
|
||||
this.rightUnitFontSize,
|
||||
this.rightUnitColor,
|
||||
this.rightUnitFontWeight,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: unit ?? '',
|
||||
style: CustomerTextStyle(
|
||||
customerColor: unitColor ?? const Color(0xff333333),
|
||||
customerFontSize: unitFontSize ?? 12,
|
||||
customerFontWeight: unitFontWeight ?? FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: money,
|
||||
style: CustomerTextStyle(
|
||||
customerColor: moneyColor ?? const Color(0xff333333),
|
||||
customerFontSize: moneyFontSize ?? 20,
|
||||
customerFontWeight: moneyFontWeight ?? FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: rightUnit ?? '',
|
||||
style: CustomerTextStyle(
|
||||
customerColor: rightUnitColor ?? const Color(0xff333333),
|
||||
customerFontSize: rightUnitFontSize ?? 12,
|
||||
customerFontWeight: rightUnitFontWeight ?? FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///Html widget
|
||||
class CustomerHtmlWidget extends StatelessWidget {
|
||||
final String html;
|
||||
final Function? onTap;
|
||||
|
||||
const CustomerHtmlWidget({
|
||||
super.key,
|
||||
required this.html,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HtmlWidget(html);
|
||||
// return Html(
|
||||
// data: html,
|
||||
// extensions: [
|
||||
// TagExtension(
|
||||
// tagsToExtend: {"flutter"},
|
||||
// child: const FlutterLogo(),
|
||||
// ),
|
||||
// ],
|
||||
// style: {
|
||||
// "p.fancy": Style(
|
||||
// textAlign: TextAlign.center,
|
||||
// // padding: EdgeInsets.all(),
|
||||
// backgroundColor: Colors.grey,
|
||||
// margin: Margins(left: Margin(50, Unit.px), right: Margin.auto()),
|
||||
// width: Width(300, Unit.px),
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// },
|
||||
// );
|
||||
// return Html(
|
||||
// data: html,
|
||||
// style: const TextStyle(
|
||||
// letterSpacing: 1.5,
|
||||
// ),
|
||||
// onTapUrl: (url) {
|
||||
// return onTap?.call(url);
|
||||
// },
|
||||
// );
|
||||
}
|
||||
}
|
||||
106
lib/utils/customer_title_content.dart
Normal file
106
lib/utils/customer_title_content.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/customer.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
///上下结构的文字
|
||||
///title、unit上
|
||||
///content、unit下
|
||||
class CustomerTitleAndContentWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final FontWeight? titleFontWeight;
|
||||
final double? titleFontSize;
|
||||
final Color? titleColor;
|
||||
final String? unitTitle;
|
||||
final FontWeight? unitTitleFontWeight;
|
||||
final double? unitTitleFontSize;
|
||||
final Color? unitTitleColor;
|
||||
final String content;
|
||||
final FontWeight? contentFontWeight;
|
||||
final double? contentFontSize;
|
||||
final Color? contentColor;
|
||||
final String? unitContent;
|
||||
final FontWeight? unitContentFontWeight;
|
||||
final double? unitContentFontSize;
|
||||
final Color? unitContentColor;
|
||||
final CrossAxisAlignment? crossAxisAlignment;
|
||||
final MainAxisAlignment? mainAxisAlignment;
|
||||
final double? spaceHeight;
|
||||
|
||||
const CustomerTitleAndContentWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.titleFontWeight,
|
||||
this.titleColor,
|
||||
this.titleFontSize,
|
||||
this.contentFontWeight,
|
||||
this.contentFontSize,
|
||||
this.contentColor,
|
||||
this.crossAxisAlignment,
|
||||
this.mainAxisAlignment,
|
||||
this.unitContent,
|
||||
this.unitContentFontWeight,
|
||||
this.unitContentFontSize,
|
||||
this.unitContentColor,
|
||||
this.spaceHeight = 10,
|
||||
this.unitTitle,
|
||||
this.unitTitleFontWeight,
|
||||
this.unitTitleFontSize,
|
||||
this.unitTitleColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: mainAxisAlignment ?? MainAxisAlignment.start,
|
||||
crossAxisAlignment: crossAxisAlignment ?? CrossAxisAlignment.center,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: unitTitle,
|
||||
style: CustomerTextStyle(
|
||||
customerFontSize: unitTitleFontSize,
|
||||
customerColor: unitTitleColor ?? const Color(0xff399FFF),
|
||||
customerFontWeight: unitTitleFontWeight ?? FontWeight.normal,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: title,
|
||||
style: CustomerTextStyle(
|
||||
customerFontSize: titleFontSize,
|
||||
customerColor: titleColor ?? const Color(0xff1A1A1A),
|
||||
customerFontWeight: titleFontWeight ?? FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: spaceHeight?.h ?? 30.h),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: content,
|
||||
style: TextStyle(
|
||||
fontSize: contentFontSize,
|
||||
color: contentColor ?? const Color(0xff399FFF),
|
||||
fontWeight: contentFontWeight ?? FontWeight.normal,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: unitContent,
|
||||
style: TextStyle(
|
||||
fontSize: unitContentFontSize,
|
||||
color: unitContentColor ?? const Color(0xff399FFF),
|
||||
fontWeight: unitContentFontWeight ?? FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
368
lib/utils/date_utils.dart
Executable file
368
lib/utils/date_utils.dart
Executable file
@@ -0,0 +1,368 @@
|
||||
import 'package:flustars_flutter3/flustars_flutter3.dart';
|
||||
|
||||
enum DateTimeUtilsType {
|
||||
yearMonthDayHourMinuteSecond, // 2024-01-01 01:01:01
|
||||
yearMonthDayHourMinute, // 2024-01-01 01:01
|
||||
yearMonthDay, // 2024-01-01
|
||||
monthDay, //01-01
|
||||
monthDayPoint, //01.01
|
||||
monthDayWord, //01月01日
|
||||
yearMonthDayPoint, // 2024.01.01
|
||||
yearMonthDayHourMinutePoint, // 2024.01.01
|
||||
yearMonthDayHourMinuteSecondPoint, // 2024.01.01 01:01:01
|
||||
yearMonthDayWord, // 2024年01月01日
|
||||
yearMonthWord, // 2024年01月
|
||||
yearMonth, // 2024-01
|
||||
yearMonthPoint, // 2024.01
|
||||
hourMinuteSecond, // 01:01:01
|
||||
hourMinute, // 01:01
|
||||
monthDayLine, // 01:01
|
||||
monthWord, //01月
|
||||
monthDayHourMinutePoint, //01月
|
||||
}
|
||||
|
||||
///日期处理类
|
||||
class DateTimeUtils {
|
||||
static String dateTimeUtilsTool({
|
||||
DateTimeUtilsType? dateTimeUtilsType,
|
||||
String? dateTime,
|
||||
String nullString = '-- --',
|
||||
}) {
|
||||
if (dateTime == null || dateTime == '') return nullString;
|
||||
DateTime date = DateTime.parse(dateTime).toLocal();
|
||||
switch (dateTimeUtilsType) {
|
||||
case null:
|
||||
return nullString;
|
||||
case DateTimeUtilsType.yearMonthDayHourMinuteSecond:
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}'
|
||||
':${date.second.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDayHourMinute:
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDay:
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthDay:
|
||||
return '${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthDayPoint:
|
||||
return '${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthDayWord:
|
||||
return '${date.month.toString().padLeft(2, '0')}月'
|
||||
'${date.day.toString().padLeft(2, '0')}日';
|
||||
case DateTimeUtilsType.yearMonthDayPoint:
|
||||
return '${date.year.toString()}'
|
||||
'.${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDayHourMinuteSecondPoint:
|
||||
return '${date.year.toString()}'
|
||||
'.${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}'
|
||||
':${date.second.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDayHourMinutePoint:
|
||||
return '${date.year.toString()}'
|
||||
'.${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthDayWord:
|
||||
return '${date.year.toString()}'
|
||||
'年${date.month.toString().padLeft(2, '0')}'
|
||||
'月${date.day.toString().padLeft(2, '0')}日';
|
||||
case DateTimeUtilsType.yearMonthWord:
|
||||
return '${date.year.toString()}'
|
||||
'年${date.month.toString().padLeft(2, '0')}月';
|
||||
case DateTimeUtilsType.yearMonth:
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.yearMonthPoint:
|
||||
return '${date.year.toString()}'
|
||||
'.${date.month.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.hourMinuteSecond:
|
||||
return '${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}'
|
||||
':${date.second.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.hourMinute:
|
||||
return '${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthDayLine:
|
||||
return '${date.month.toString().padLeft(2, '0')}'
|
||||
'/${date.day.toString().padLeft(2, '0')}';
|
||||
case DateTimeUtilsType.monthWord:
|
||||
return '${date.month.toString().padLeft(2, '0')}月';
|
||||
case DateTimeUtilsType.monthDayHourMinutePoint:
|
||||
return '${date.month.toString().padLeft(2, '0')}'
|
||||
'.${date.day.toString().padLeft(2, '0')}'
|
||||
' ${date.hour.toString().padLeft(2, '0')}'
|
||||
':${date.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算两个日期相差多少天?
|
||||
static int differenceTwoTimes({
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
}) {
|
||||
var startDate =
|
||||
startTime == null ? DateTime.now() : DateTime.parse(startTime);
|
||||
var endDate = endTime == null ? DateTime.now() : DateTime.parse(endTime);
|
||||
var days = endDate.difference(startDate).inDays;
|
||||
return days;
|
||||
}
|
||||
|
||||
/// 计算两个日期相差inMinutes?
|
||||
static int differenceTwoInMinutesTimes({
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
}) {
|
||||
var startDate =
|
||||
startTime == null ? DateTime.now() : DateTime.parse(startTime);
|
||||
var endDate = endTime == null ? DateTime.now() : DateTime.parse(endTime);
|
||||
var days = endDate.difference(startDate).inMinutes;
|
||||
return days;
|
||||
}
|
||||
|
||||
|
||||
/// 计算两个日期相差day huor min?
|
||||
static String differenceTwoDayHourTimes({
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
|
||||
}) {
|
||||
|
||||
String xxxxx = '0';
|
||||
var startDate =
|
||||
startTime == null ? DateTime.now() : DateTime.parse(startTime);
|
||||
var endDate = endTime == null ? DateTime.now() : DateTime.parse(endTime);
|
||||
var days = endDate.difference(startDate).inDays;
|
||||
if(days == 0){
|
||||
//days == 0 相当
|
||||
var hours = endDate.difference(startDate).inHours;
|
||||
|
||||
if(hours == 0){
|
||||
var minutes = endDate.difference(startDate).inMinutes;
|
||||
xxxxx = '$minutes分';
|
||||
}else{
|
||||
// xxxxx = hours;
|
||||
var minutes = endDate.difference(startDate).inMinutes;
|
||||
|
||||
xxxxx = '${minutes ~/ 60}小时${minutes % 60}分';
|
||||
}
|
||||
}else{
|
||||
// xxxxx = days;
|
||||
var hours = endDate.difference(startDate).inHours;
|
||||
int divisor = 24; // 除数
|
||||
int quotient = hours ~/ divisor; // 取整除法,得到商
|
||||
int remainder = hours % divisor; // 取余数,得到余数
|
||||
//
|
||||
// print('商: $quotient');
|
||||
// print('余数: $remainder');
|
||||
xxxxx = '$quotient天$remainder小时';
|
||||
}
|
||||
return xxxxx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///获取当前月份
|
||||
static String getCurrentMonth() {
|
||||
DateTime date = DateTime.now();
|
||||
return date.month.toString().padLeft(2, '0');
|
||||
}
|
||||
|
||||
///获取当前的年月日
|
||||
static String getCurrentYMD() {
|
||||
DateTime date = DateTime.now();
|
||||
return '${date.year.toString()}'
|
||||
'-${date.month.toString().padLeft(2, '0')}'
|
||||
'-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
///判断时间是否在某个时间区间内
|
||||
static bool isTimeInRange({
|
||||
required DateTime startTime,
|
||||
required DateTime endTime,
|
||||
required DateTime dateTime,
|
||||
}) {
|
||||
return dateTime.isAfter(startTime) && dateTime.isBefore(endTime);
|
||||
}
|
||||
|
||||
///获取当月份
|
||||
static String get getMonth {
|
||||
return DateTime.now().month.toString();
|
||||
}
|
||||
|
||||
///获取当年份
|
||||
static String get getYear {
|
||||
return DateTime.now().year.toString();
|
||||
}
|
||||
|
||||
///获取当年份
|
||||
static String getGMTString() {
|
||||
|
||||
return DateTime.now().year.toString();
|
||||
}
|
||||
|
||||
///获取当前属于第几周
|
||||
static String getWeekDay(DateTime dateTime) {
|
||||
List<String> weekday = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||||
return weekday[dateTime.weekday - 1];
|
||||
}
|
||||
|
||||
///根据日期获取某月的第一天和最后一天
|
||||
static Map<String, dynamic> getMonthStartAndMonthEnd(
|
||||
{required DateTime dateTime}) {
|
||||
DateTime monthStart = DateTime(
|
||||
dateTime.year,
|
||||
dateTime.month,
|
||||
1,
|
||||
); // 获取本月第一天的日期时间
|
||||
DateTime monthEnd = DateTime(
|
||||
dateTime.year,
|
||||
dateTime.month + 1,
|
||||
0,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
); // 获取本月最后一天的日期时间(时间为23:59:59)
|
||||
return {
|
||||
'monthStart': monthStart,
|
||||
'monthEnd': monthEnd,
|
||||
};
|
||||
}
|
||||
//
|
||||
// DateTime monthStart = DateTime(
|
||||
// int.parse(time.toString().split('-').first),
|
||||
// int.parse(time.toString().split('-').last),
|
||||
// 1); // 获取本月第一天的日期时间
|
||||
// DateTime monthEnd = DateTime(
|
||||
// int.parse(time.toString().split('-').first),
|
||||
// int.parse(time.toString().split('-').last) + 1,
|
||||
// 0,
|
||||
// 23,
|
||||
// 59,
|
||||
// 59); // 获取本月最后一天的日期时间(时间为23:59:59)
|
||||
/// /
|
||||
}
|
||||
|
||||
class TimeMachineUtil {
|
||||
/// 获取某一年的第一个月的第一天和最后一个月的最后一天
|
||||
static getStartEndYearDate(int iYear) {
|
||||
Map mapDate = {};
|
||||
int yearNow = DateTime.now().year;
|
||||
yearNow = yearNow + iYear;
|
||||
|
||||
String newStartYear = '$yearNow-01-01';
|
||||
String newEndtYear = '${yearNow + 1}-01-00';
|
||||
|
||||
mapDate['startTime'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(turnTimestamp(newStartYear)),
|
||||
format: 'yyyy-MM-dd');
|
||||
mapDate['endTime'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(turnTimestamp(newEndtYear)),
|
||||
format: 'yyyy-MM-dd');
|
||||
|
||||
mapDate['startStamp'] = turnTimestamp(mapDate['startTime'] + ' 00:00:00');
|
||||
mapDate['endStamp'] = turnTimestamp(mapDate['endTime'] + ' 23:59:59');
|
||||
print('某一年初和年末:$mapDate');
|
||||
}
|
||||
|
||||
/// 获得当前日期 未来/过去 第某个月第一天和最后一天时间
|
||||
static Map<String, String> getMonthDate(int iMonth) {
|
||||
//获取当前日期
|
||||
var currentDate = DateTime.now();
|
||||
if (iMonth + currentDate.month > 0) {
|
||||
return timeConversion(
|
||||
iMonth + currentDate.month, (currentDate.year).toString());
|
||||
} else {
|
||||
int beforeYear = (iMonth + currentDate.month) ~/ 12;
|
||||
String yearNew = (currentDate.year + beforeYear - 1).toString();
|
||||
int monthNew = (iMonth + currentDate.month) - beforeYear * 12;
|
||||
return timeConversion(12 + monthNew, yearNew);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, String> timeConversion(int monthTime, String yearTime) {
|
||||
Map<String, String> dateMap = {};
|
||||
dateMap['startDate'] =
|
||||
'$yearTime-${monthTime < 10 ? '0$monthTime' : '$monthTime'}-01';
|
||||
//转时间戳再转时间格式 防止出错
|
||||
dateMap['startDate'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
turnTimestamp(dateMap['startDate'] ?? "")),
|
||||
format: 'yyyy-MM-dd');
|
||||
//某个月结束时间,转时间戳再转
|
||||
String endMonth =
|
||||
'$yearTime-${(monthTime + 1) < 10 ? '0${monthTime + 1}' : (monthTime + 1)}-00';
|
||||
var endMonthTimeStamp = turnTimestamp(endMonth);
|
||||
endMonth = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(endMonthTimeStamp),
|
||||
format: 'yyyy-MM-dd');
|
||||
dateMap['endDate'] = endMonth;
|
||||
//这里是为了公司后台接口 需加时间段的时间戳 但不显示在格式化实践中
|
||||
dateMap['startDateStamp'] =
|
||||
turnTimestamp('${dateMap['startDate']} 00:00:00').toString();
|
||||
dateMap['endDateStamp'] =
|
||||
turnTimestamp('${dateMap['endDate']} 23:59:59').toString();
|
||||
// print('过去未来某个月初月末:$dateMap');
|
||||
return dateMap;
|
||||
}
|
||||
|
||||
/// 转时间戳
|
||||
static int turnTimestamp(String timestamp) {
|
||||
return DateTime.parse(timestamp).millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
/// 当前时间 过去/未来 某个周的周一和周日
|
||||
static Map<String, String> getWeeksDate(int weeks) {
|
||||
Map<String, String> mapTime = {};
|
||||
DateTime now = DateTime.now();
|
||||
int weekday = now.weekday; //今天周几
|
||||
|
||||
var sunDay = getTimestampLatest(false, 7 - weekday + weeks * 7); //周末
|
||||
var monDay = getTimestampLatest(true, -weekday + 1 + weeks * 7); //周一
|
||||
|
||||
mapTime['monDay'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(monDay),
|
||||
format: 'yyyy-MM-dd'); //周一 时间格式化
|
||||
mapTime['sunDay'] = DateUtil.formatDate(
|
||||
DateTime.fromMillisecondsSinceEpoch(sunDay),
|
||||
format: 'yyyy-MM-dd'); //周一 时间格式化
|
||||
mapTime['monDayStamp'] = '$monDay'; //周一 时间戳
|
||||
mapTime['sunDayStamp'] = '$sunDay'; //周日 时间戳
|
||||
// print('某个周的周一和周日:$mapTime');
|
||||
return mapTime;
|
||||
}
|
||||
|
||||
/// phase : 是零点还是23:59:59
|
||||
static int getTimestampLatest(bool phase, int day) {
|
||||
String newHours;
|
||||
DateTime now = DateTime.now();
|
||||
DateTime sixtyDaysFromNow = now.add(Duration(days: day));
|
||||
String formattedDate =
|
||||
DateUtil.formatDate(sixtyDaysFromNow, format: 'yyyy-MM-dd');
|
||||
if (phase) {
|
||||
newHours = '$formattedDate 00:00:00';
|
||||
} else {
|
||||
newHours = '$formattedDate 23:59:59';
|
||||
}
|
||||
|
||||
DateTime newDate = DateTime.parse(newHours);
|
||||
// String newFormattedDate =
|
||||
// DateUtil.formatDate(newDate, format: 'yyyy-MM-dd HH:mm:ss');
|
||||
int timeStamp = newDate.millisecondsSinceEpoch;
|
||||
// print('时间' + newFormattedDate);
|
||||
return timeStamp;
|
||||
}
|
||||
}
|
||||
47
lib/utils/diolog_alter_view.dart
Normal file
47
lib/utils/diolog_alter_view.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///弹窗widget
|
||||
///ctx showDialog context
|
||||
///padding 位置
|
||||
class ShowDialogWidget extends StatelessWidget {
|
||||
final BuildContext? ctx;
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
const ShowDialogWidget({
|
||||
super.key,
|
||||
this.ctx,
|
||||
required this.child,
|
||||
required this.padding,
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: ctx == null ? null : () => Navigator.pop(ctx!),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/utils/file/customer_file.dart
Normal file
126
lib/utils/file/customer_file.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/toast_utils.dart';
|
||||
import 'play/video_play_page.dart';
|
||||
|
||||
enum OpenType {
|
||||
file, //文件
|
||||
image, //图片
|
||||
video, //视频
|
||||
url, //网址
|
||||
}
|
||||
|
||||
class CustomerFile {
|
||||
static Future<void> openTypeFile({
|
||||
required OpenType type,
|
||||
required BuildContext context,
|
||||
required int isLock,
|
||||
required String filePath,
|
||||
String? fileName,
|
||||
}) async {
|
||||
if (filePath == "" || filePath == null) {
|
||||
ToastUtils.showToast(msg: "暂无内容");
|
||||
} else {
|
||||
if (type == OpenType.file) {
|
||||
CustomerAction.openFileAction(
|
||||
context: context,
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
);
|
||||
}
|
||||
if (type == OpenType.video) {
|
||||
CustomerAction.openVideoAction(
|
||||
context: context,
|
||||
videoUrl: filePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///公共方法
|
||||
class CustomerAction {
|
||||
///打开文件
|
||||
static Future<void> openFileAction({
|
||||
required BuildContext context,
|
||||
required String filePath,
|
||||
String? fileName,
|
||||
}) async {
|
||||
if (filePath == "") {
|
||||
ToastUtils.showToast(msg: "暂无文件");
|
||||
} else {
|
||||
if (filePath.contains("http")) {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (BuildContext ctx) {
|
||||
//
|
||||
// // return CustomerPDFPage(
|
||||
// // filePath: filePath,
|
||||
// // );
|
||||
// });
|
||||
} else {
|
||||
ToastUtils.showToast(msg: "地址无效");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static _dealFileName(String filePath) {
|
||||
return "${DateTime.now()}.${filePath.split(".").last}";
|
||||
}
|
||||
|
||||
///打开视频
|
||||
static Future<void> openVideoAction({
|
||||
required BuildContext context,
|
||||
required String videoUrl,
|
||||
}) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: VideoPlayPage(
|
||||
videoUrl: videoUrl,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
///打开网页
|
||||
// static Future<void> openWebview({
|
||||
// required BuildContext context,
|
||||
// required String url,
|
||||
// }) async {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (BuildContext ctx) {
|
||||
// return CustomerWebView(url: url);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
static Future<void> openTypeFile({
|
||||
required OpenType type,
|
||||
required BuildContext context,
|
||||
required int isLock,
|
||||
required String filePath,
|
||||
String? fileName,
|
||||
}) async {
|
||||
if (filePath == "" || filePath == null) {
|
||||
ToastUtils.showToast(msg: "暂无内容");
|
||||
} else {
|
||||
if (type == OpenType.file) {
|
||||
CustomerAction.openFileAction(
|
||||
context: context,
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
);
|
||||
}
|
||||
if (type == OpenType.video) {
|
||||
CustomerAction.openVideoAction(
|
||||
context: context,
|
||||
videoUrl: filePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
lib/utils/file/play/video_play_page.dart
Executable file
87
lib/utils/file/play/video_play_page.dart
Executable file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/file/video/lib/chewie.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
|
||||
|
||||
|
||||
class VideoPlayPage extends StatefulWidget {
|
||||
final String? videoUrl;
|
||||
const VideoPlayPage({super.key, this.videoUrl});
|
||||
|
||||
@override
|
||||
State<VideoPlayPage> createState() => _VideoPlayPageState();
|
||||
}
|
||||
|
||||
class _VideoPlayPageState extends State<VideoPlayPage> {
|
||||
var videoController = VideoPlayerController.network("");
|
||||
var chewieController;
|
||||
//监听视频是否在播放
|
||||
bool isPlaying = false;
|
||||
//视频地址 https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4
|
||||
// String videoUrl =
|
||||
// "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4";
|
||||
// String videoUrl =
|
||||
// "http://static.dongmenshijing.com/upload/20230720/cf742bbf-8a96-46d5-b187-edeffa168556.mp4";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Uri url = Uri.parse(videoUrl);
|
||||
// videoController = VideoPlayerController.networkUrl(url)
|
||||
// ..initialize().then((value) {
|
||||
// setState(() {});
|
||||
// });
|
||||
// videoController = VideoPlayerController.network(
|
||||
// videoUrl,
|
||||
// videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
// )..initialize().then((value) {
|
||||
// setState(() {});
|
||||
// });
|
||||
// videoController.addListener(() {
|
||||
// setState(() {});
|
||||
// });
|
||||
initVideoController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
///初始化
|
||||
initVideoController({String? url}) {
|
||||
videoController = VideoPlayerController.network(
|
||||
widget.videoUrl ?? "",
|
||||
)..initialize().then((_) {
|
||||
setState(() {
|
||||
chewieController = ChewieController(
|
||||
// autoInitialize: true,
|
||||
fullScreenByDefault: true,
|
||||
videoPlayerController: videoController,
|
||||
allowFullScreen: true,
|
||||
// aspectRatio: MediaQuery.of(context).size.height /
|
||||
// MediaQuery.of(context).size.width,
|
||||
autoPlay: true,
|
||||
looping: false,
|
||||
);
|
||||
});
|
||||
});
|
||||
videoController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
videoController.dispose();
|
||||
chewieController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return chewieController == null
|
||||
? Container()
|
||||
: Chewie(
|
||||
controller: chewieController,
|
||||
);
|
||||
}
|
||||
}
|
||||
8
lib/utils/file/video/lib/chewie.dart
Executable file
8
lib/utils/file/video/lib/chewie.dart
Executable file
@@ -0,0 +1,8 @@
|
||||
library chewie;
|
||||
|
||||
export 'src/chewie_player.dart';
|
||||
export 'src/chewie_progress_colors.dart';
|
||||
export 'src/cupertino/cupertino_controls.dart';
|
||||
export 'src/material/material_controls.dart';
|
||||
export 'src/material/material_desktop_controls.dart';
|
||||
export 'src/models/index.dart';
|
||||
57
lib/utils/file/video/lib/src/animated_play_pause.dart
Executable file
57
lib/utils/file/video/lib/src/animated_play_pause.dart
Executable file
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that animates implicitly between a play and a pause icon.
|
||||
class AnimatedPlayPause extends StatefulWidget {
|
||||
const AnimatedPlayPause({
|
||||
Key? key,
|
||||
required this.playing,
|
||||
this.size,
|
||||
this.color,
|
||||
}) : super(key: key);
|
||||
|
||||
final double? size;
|
||||
final bool playing;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => AnimatedPlayPauseState();
|
||||
}
|
||||
|
||||
class AnimatedPlayPauseState extends State<AnimatedPlayPause>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final animationController = AnimationController(
|
||||
vsync: this,
|
||||
value: widget.playing ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AnimatedPlayPause oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.playing != oldWidget.playing) {
|
||||
if (widget.playing) {
|
||||
animationController.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: AnimatedIcon(
|
||||
color: widget.color,
|
||||
size: widget.size,
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: animationController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/utils/file/video/lib/src/center_play_button.dart
Executable file
56
lib/utils/file/video/lib/src/center_play_button.dart
Executable file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'animated_play_pause.dart';
|
||||
|
||||
class CenterPlayButton extends StatelessWidget {
|
||||
const CenterPlayButton({
|
||||
Key? key,
|
||||
required this.backgroundColor,
|
||||
this.iconColor,
|
||||
required this.show,
|
||||
required this.isPlaying,
|
||||
required this.isFinished,
|
||||
this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
final Color backgroundColor;
|
||||
final Color? iconColor;
|
||||
final bool show;
|
||||
final bool isPlaying;
|
||||
final bool isFinished;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: UnconstrainedBox(
|
||||
child: AnimatedOpacity(
|
||||
opacity: show ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
// Always set the iconSize on the IconButton, not on the Icon itself:
|
||||
// https://github.com/flutter/flutter/issues/52980
|
||||
child: IconButton(
|
||||
iconSize: 32,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
icon: isFinished
|
||||
? Icon(Icons.replay, color: iconColor)
|
||||
: AnimatedPlayPause(
|
||||
color: iconColor,
|
||||
playing: isPlaying,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
633
lib/utils/file/video/lib/src/chewie_player.dart
Executable file
633
lib/utils/file/video/lib/src/chewie_player.dart
Executable file
@@ -0,0 +1,633 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
// import 'package:wakelock/wakelock.dart';
|
||||
|
||||
import '../chewie.dart';
|
||||
import 'notifiers/index.dart';
|
||||
import 'player_with_controls.dart';
|
||||
|
||||
typedef ChewieRoutePageBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
ChewieControllerProvider controllerProvider,
|
||||
);
|
||||
|
||||
/// A Video Player with Material and Cupertino skins.
|
||||
///
|
||||
/// `video_player` is pretty low level. Chewie wraps it in a friendly skin to
|
||||
/// make it easy to use!
|
||||
class Chewie extends StatefulWidget {
|
||||
const Chewie({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The [ChewieController]
|
||||
final ChewieController controller;
|
||||
|
||||
@override
|
||||
ChewieState createState() {
|
||||
return ChewieState();
|
||||
}
|
||||
}
|
||||
|
||||
class ChewieState extends State<Chewie> {
|
||||
bool _isFullScreen = false;
|
||||
|
||||
bool get isControllerFullScreen => widget.controller.isFullScreen;
|
||||
late PlayerNotifier notifier;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(listener);
|
||||
notifier = PlayerNotifier.init();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(listener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Chewie oldWidget) {
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
widget.controller.addListener(listener);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (_isFullScreen != isControllerFullScreen) {
|
||||
widget.controller._isFullScreen = _isFullScreen;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> listener() async {
|
||||
if (isControllerFullScreen && !_isFullScreen) {
|
||||
_isFullScreen = isControllerFullScreen;
|
||||
await _pushFullScreenWidget(context);
|
||||
} else if (_isFullScreen) {
|
||||
Navigator.of(
|
||||
context,
|
||||
rootNavigator: widget.controller.useRootNavigator,
|
||||
).pop();
|
||||
_isFullScreen = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChewieControllerProvider(
|
||||
controller: widget.controller,
|
||||
child: ChangeNotifierProvider<PlayerNotifier>.value(
|
||||
value: notifier,
|
||||
builder: (context, w) => const PlayerWithControls(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFullScreenVideo(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
ChewieControllerProvider controllerProvider,
|
||||
) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Container(
|
||||
alignment: Alignment.center,
|
||||
color: Colors.black,
|
||||
child: controllerProvider,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AnimatedWidget _defaultRoutePageBuilder(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
ChewieControllerProvider controllerProvider,
|
||||
) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return _buildFullScreenVideo(context, animation, controllerProvider);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fullScreenRoutePageBuilder(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
final controllerProvider = ChewieControllerProvider(
|
||||
controller: widget.controller,
|
||||
child: ChangeNotifierProvider<PlayerNotifier>.value(
|
||||
value: notifier,
|
||||
builder: (context, w) => const PlayerWithControls(),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.controller.routePageBuilder == null) {
|
||||
return _defaultRoutePageBuilder(
|
||||
context,
|
||||
animation,
|
||||
secondaryAnimation,
|
||||
controllerProvider,
|
||||
);
|
||||
}
|
||||
return widget.controller.routePageBuilder!(
|
||||
context,
|
||||
animation,
|
||||
secondaryAnimation,
|
||||
controllerProvider,
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> _pushFullScreenWidget(BuildContext context) async {
|
||||
final TransitionRoute<void> route = PageRouteBuilder<void>(
|
||||
pageBuilder: _fullScreenRoutePageBuilder,
|
||||
);
|
||||
|
||||
onEnterFullScreen();
|
||||
|
||||
if (!widget.controller.allowedScreenSleep) {
|
||||
WakelockPlus.enable;
|
||||
}
|
||||
|
||||
await Navigator.of(
|
||||
context,
|
||||
rootNavigator: widget.controller.useRootNavigator,
|
||||
).push(route);
|
||||
_isFullScreen = false;
|
||||
widget.controller.exitFullScreen();
|
||||
|
||||
// The wakelock plugins checks whether it needs to perform an action internally,
|
||||
// so we do not need to check Wakelock.isEnabled.
|
||||
WakelockPlus.disable();
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: widget.controller.systemOverlaysAfterFullScreen,
|
||||
);
|
||||
SystemChrome.setPreferredOrientations(
|
||||
widget.controller.deviceOrientationsAfterFullScreen,
|
||||
);
|
||||
}
|
||||
|
||||
void onEnterFullScreen() {
|
||||
final videoWidth = widget.controller.videoPlayerController.value.size.width;
|
||||
final videoHeight =
|
||||
widget.controller.videoPlayerController.value.size.height;
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
|
||||
// if (widget.controller.systemOverlaysOnEnterFullScreen != null) {
|
||||
// /// Optional user preferred settings
|
||||
// SystemChrome.setEnabledSystemUIMode(
|
||||
// SystemUiMode.manual,
|
||||
// overlays: widget.controller.systemOverlaysOnEnterFullScreen,
|
||||
// );
|
||||
// } else {
|
||||
// /// Default behavior
|
||||
// SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
|
||||
// }
|
||||
|
||||
if (widget.controller.deviceOrientationsOnEnterFullScreen != null) {
|
||||
/// Optional user preferred settings
|
||||
SystemChrome.setPreferredOrientations(
|
||||
widget.controller.deviceOrientationsOnEnterFullScreen!,
|
||||
);
|
||||
} else {
|
||||
final isLandscapeVideo = videoWidth > videoHeight;
|
||||
final isPortraitVideo = videoWidth < videoHeight;
|
||||
|
||||
/// Default behavior
|
||||
/// Video w > h means we force landscape
|
||||
if (isLandscapeVideo) {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Video h > w means we force portrait
|
||||
else if (isPortraitVideo) {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Otherwise if h == w (square video)
|
||||
else {
|
||||
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The ChewieController is used to configure and drive the Chewie Player
|
||||
/// Widgets. It provides methods to control playback, such as [pause] and
|
||||
/// [play], as well as methods that control the visual appearance of the player,
|
||||
/// such as [enterFullScreen] or [exitFullScreen].
|
||||
///
|
||||
/// In addition, you can listen to the ChewieController for presentational
|
||||
/// changes, such as entering and exiting full screen mode. To listen for
|
||||
/// changes to the playback, such as a change to the seek position of the
|
||||
/// player, please use the standard information provided by the
|
||||
/// `VideoPlayerController`.
|
||||
class ChewieController extends ChangeNotifier {
|
||||
ChewieController({
|
||||
required this.videoPlayerController,
|
||||
this.optionsTranslation,
|
||||
this.aspectRatio,
|
||||
this.autoInitialize = false,
|
||||
this.autoPlay = false,
|
||||
this.startAt,
|
||||
this.looping = false,
|
||||
this.fullScreenByDefault = false,
|
||||
this.cupertinoProgressColors,
|
||||
this.materialProgressColors,
|
||||
this.placeholder,
|
||||
this.overlay,
|
||||
this.showControlsOnInitialize = true,
|
||||
this.showOptions = true,
|
||||
this.optionsBuilder,
|
||||
this.additionalOptions,
|
||||
this.showControls = true,
|
||||
this.transformationController,
|
||||
this.zoomAndPan = false,
|
||||
this.maxScale = 2.5,
|
||||
this.subtitle,
|
||||
this.subtitleBuilder,
|
||||
this.customControls,
|
||||
this.errorBuilder,
|
||||
this.allowedScreenSleep = true,
|
||||
this.isLive = false,
|
||||
this.allowFullScreen = true,
|
||||
this.allowMuting = true,
|
||||
this.allowPlaybackSpeedChanging = true,
|
||||
this.useRootNavigator = true,
|
||||
this.playbackSpeeds = const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
|
||||
this.systemOverlaysOnEnterFullScreen,
|
||||
this.deviceOrientationsOnEnterFullScreen,
|
||||
this.systemOverlaysAfterFullScreen = SystemUiOverlay.values,
|
||||
this.deviceOrientationsAfterFullScreen = DeviceOrientation.values,
|
||||
this.routePageBuilder,
|
||||
this.progressIndicatorDelay,
|
||||
this.hideControlsTimer = defaultHideControlsTimer,
|
||||
}) : assert(
|
||||
playbackSpeeds.every((speed) => speed > 0),
|
||||
'The playbackSpeeds values must all be greater than 0',
|
||||
) {
|
||||
_initialize();
|
||||
}
|
||||
|
||||
ChewieController copyWith({
|
||||
VideoPlayerController? videoPlayerController,
|
||||
OptionsTranslation? optionsTranslation,
|
||||
double? aspectRatio,
|
||||
bool? autoInitialize,
|
||||
bool? autoPlay,
|
||||
Duration? startAt,
|
||||
bool? looping,
|
||||
bool? fullScreenByDefault,
|
||||
ChewieProgressColors? cupertinoProgressColors,
|
||||
ChewieProgressColors? materialProgressColors,
|
||||
Widget? placeholder,
|
||||
Widget? overlay,
|
||||
bool? showControlsOnInitialize,
|
||||
bool? showOptions,
|
||||
Future<void> Function(BuildContext, List<OptionItem>)? optionsBuilder,
|
||||
List<OptionItem> Function(BuildContext)? additionalOptions,
|
||||
bool? showControls,
|
||||
TransformationController? transformationController,
|
||||
bool? zoomAndPan,
|
||||
double? maxScale,
|
||||
Subtitles? subtitle,
|
||||
Widget Function(BuildContext, dynamic)? subtitleBuilder,
|
||||
Widget? customControls,
|
||||
Widget Function(BuildContext, String)? errorBuilder,
|
||||
bool? allowedScreenSleep,
|
||||
bool? isLive,
|
||||
bool? allowFullScreen,
|
||||
bool? allowMuting,
|
||||
bool? allowPlaybackSpeedChanging,
|
||||
bool? useRootNavigator,
|
||||
Duration? hideControlsTimer,
|
||||
List<double>? playbackSpeeds,
|
||||
List<SystemUiOverlay>? systemOverlaysOnEnterFullScreen,
|
||||
List<DeviceOrientation>? deviceOrientationsOnEnterFullScreen,
|
||||
List<SystemUiOverlay>? systemOverlaysAfterFullScreen,
|
||||
List<DeviceOrientation>? deviceOrientationsAfterFullScreen,
|
||||
Duration? progressIndicatorDelay,
|
||||
Widget Function(
|
||||
BuildContext,
|
||||
Animation<double>,
|
||||
Animation<double>,
|
||||
ChewieControllerProvider,
|
||||
)? routePageBuilder,
|
||||
}) {
|
||||
return ChewieController(
|
||||
videoPlayerController:
|
||||
videoPlayerController ?? this.videoPlayerController,
|
||||
optionsTranslation: optionsTranslation ?? this.optionsTranslation,
|
||||
aspectRatio: aspectRatio ?? this.aspectRatio,
|
||||
autoInitialize: autoInitialize ?? this.autoInitialize,
|
||||
autoPlay: autoPlay ?? this.autoPlay,
|
||||
startAt: startAt ?? this.startAt,
|
||||
looping: looping ?? this.looping,
|
||||
fullScreenByDefault: fullScreenByDefault ?? this.fullScreenByDefault,
|
||||
cupertinoProgressColors:
|
||||
cupertinoProgressColors ?? this.cupertinoProgressColors,
|
||||
materialProgressColors:
|
||||
materialProgressColors ?? this.materialProgressColors,
|
||||
placeholder: placeholder ?? this.placeholder,
|
||||
overlay: overlay ?? this.overlay,
|
||||
showControlsOnInitialize:
|
||||
showControlsOnInitialize ?? this.showControlsOnInitialize,
|
||||
showOptions: showOptions ?? this.showOptions,
|
||||
optionsBuilder: optionsBuilder ?? this.optionsBuilder,
|
||||
additionalOptions: additionalOptions ?? this.additionalOptions,
|
||||
showControls: showControls ?? this.showControls,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
subtitleBuilder: subtitleBuilder ?? this.subtitleBuilder,
|
||||
customControls: customControls ?? this.customControls,
|
||||
errorBuilder: errorBuilder ?? this.errorBuilder,
|
||||
allowedScreenSleep: allowedScreenSleep ?? this.allowedScreenSleep,
|
||||
isLive: isLive ?? this.isLive,
|
||||
allowFullScreen: allowFullScreen ?? this.allowFullScreen,
|
||||
allowMuting: allowMuting ?? this.allowMuting,
|
||||
allowPlaybackSpeedChanging:
|
||||
allowPlaybackSpeedChanging ?? this.allowPlaybackSpeedChanging,
|
||||
useRootNavigator: useRootNavigator ?? this.useRootNavigator,
|
||||
playbackSpeeds: playbackSpeeds ?? this.playbackSpeeds,
|
||||
systemOverlaysOnEnterFullScreen: systemOverlaysOnEnterFullScreen ??
|
||||
this.systemOverlaysOnEnterFullScreen,
|
||||
deviceOrientationsOnEnterFullScreen:
|
||||
deviceOrientationsOnEnterFullScreen ??
|
||||
this.deviceOrientationsOnEnterFullScreen,
|
||||
systemOverlaysAfterFullScreen:
|
||||
systemOverlaysAfterFullScreen ?? this.systemOverlaysAfterFullScreen,
|
||||
deviceOrientationsAfterFullScreen: deviceOrientationsAfterFullScreen ??
|
||||
this.deviceOrientationsAfterFullScreen,
|
||||
routePageBuilder: routePageBuilder ?? this.routePageBuilder,
|
||||
hideControlsTimer: hideControlsTimer ?? this.hideControlsTimer,
|
||||
progressIndicatorDelay:
|
||||
progressIndicatorDelay ?? this.progressIndicatorDelay,
|
||||
);
|
||||
}
|
||||
|
||||
static const defaultHideControlsTimer = Duration(seconds: 3);
|
||||
|
||||
/// If false, the options button in MaterialUI and MaterialDesktopUI
|
||||
/// won't be shown.
|
||||
final bool showOptions;
|
||||
|
||||
/// Pass your translations for the options like:
|
||||
/// - PlaybackSpeed
|
||||
/// - Subtitles
|
||||
/// - Cancel
|
||||
///
|
||||
/// Buttons
|
||||
///
|
||||
/// These are required for the default `OptionItem`'s
|
||||
final OptionsTranslation? optionsTranslation;
|
||||
|
||||
/// Build your own options with default chewieOptions shiped through
|
||||
/// the builder method. Just add your own options to the Widget
|
||||
/// you'll build. If you want to hide the chewieOptions, just leave them
|
||||
/// out from your Widget.
|
||||
final Future<void> Function(
|
||||
BuildContext context,
|
||||
List<OptionItem> chewieOptions,
|
||||
)? optionsBuilder;
|
||||
|
||||
/// Add your own additional options on top of chewie options
|
||||
final List<OptionItem> Function(BuildContext context)? additionalOptions;
|
||||
|
||||
/// Define here your own Widget on how your n'th subtitle will look like
|
||||
Widget Function(BuildContext context, dynamic subtitle)? subtitleBuilder;
|
||||
|
||||
/// Add a List of Subtitles here in `Subtitles.subtitle`
|
||||
Subtitles? subtitle;
|
||||
|
||||
/// The controller for the video you want to play
|
||||
final VideoPlayerController videoPlayerController;
|
||||
|
||||
/// Initialize the Video on Startup. This will prep the video for playback.
|
||||
final bool autoInitialize;
|
||||
|
||||
/// Play the video as soon as it's displayed
|
||||
final bool autoPlay;
|
||||
|
||||
/// Start video at a certain position
|
||||
final Duration? startAt;
|
||||
|
||||
/// Whether or not the video should loop
|
||||
final bool looping;
|
||||
|
||||
/// Wether or not to show the controls when initializing the widget.
|
||||
final bool showControlsOnInitialize;
|
||||
|
||||
/// Whether or not to show the controls at all
|
||||
final bool showControls;
|
||||
|
||||
/// Controller to pass into the [InteractiveViewer] component
|
||||
final TransformationController? transformationController;
|
||||
|
||||
/// Whether or not to allow zooming and panning
|
||||
final bool zoomAndPan;
|
||||
|
||||
/// Max scale when zooming
|
||||
final double maxScale;
|
||||
|
||||
/// Defines customised controls. Check [MaterialControls] or
|
||||
/// [CupertinoControls] for reference.
|
||||
final Widget? customControls;
|
||||
|
||||
/// When the video playback runs into an error, you can build a custom
|
||||
/// error message.
|
||||
final Widget Function(BuildContext context, String errorMessage)?
|
||||
errorBuilder;
|
||||
|
||||
/// The Aspect Ratio of the Video. Important to get the correct size of the
|
||||
/// video!
|
||||
///
|
||||
/// Will fallback to fitting within the space allowed.
|
||||
final double? aspectRatio;
|
||||
|
||||
/// The colors to use for controls on iOS. By default, the iOS player uses
|
||||
/// colors sampled from the original iOS 11 designs.
|
||||
final ChewieProgressColors? cupertinoProgressColors;
|
||||
|
||||
/// The colors to use for the Material Progress Bar. By default, the Material
|
||||
/// player uses the colors from your Theme.
|
||||
final ChewieProgressColors? materialProgressColors;
|
||||
|
||||
/// The placeholder is displayed underneath the Video before it is initialized
|
||||
/// or played.
|
||||
final Widget? placeholder;
|
||||
|
||||
/// A widget which is placed between the video and the controls
|
||||
final Widget? overlay;
|
||||
|
||||
/// Defines if the player will start in fullscreen when play is pressed
|
||||
final bool fullScreenByDefault;
|
||||
|
||||
/// Defines if the player will sleep in fullscreen or not
|
||||
final bool allowedScreenSleep;
|
||||
|
||||
/// Defines if the controls should be shown for live stream video
|
||||
final bool isLive;
|
||||
|
||||
/// Defines if the fullscreen control should be shown
|
||||
final bool allowFullScreen;
|
||||
|
||||
/// Defines if the mute control should be shown
|
||||
final bool allowMuting;
|
||||
|
||||
/// Defines if the playback speed control should be shown
|
||||
final bool allowPlaybackSpeedChanging;
|
||||
|
||||
/// Defines if push/pop navigations use the rootNavigator
|
||||
final bool useRootNavigator;
|
||||
|
||||
/// Defines the [Duration] before the video controls are hidden. By default, this is set to three seconds.
|
||||
final Duration hideControlsTimer;
|
||||
|
||||
/// Defines the set of allowed playback speeds user can change
|
||||
final List<double> playbackSpeeds;
|
||||
|
||||
/// Defines the system overlays visible on entering fullscreen
|
||||
final List<SystemUiOverlay>? systemOverlaysOnEnterFullScreen;
|
||||
|
||||
/// Defines the set of allowed device orientations on entering fullscreen
|
||||
final List<DeviceOrientation>? deviceOrientationsOnEnterFullScreen;
|
||||
|
||||
/// Defines the system overlays visible after exiting fullscreen
|
||||
final List<SystemUiOverlay> systemOverlaysAfterFullScreen;
|
||||
|
||||
/// Defines the set of allowed device orientations after exiting fullscreen
|
||||
final List<DeviceOrientation> deviceOrientationsAfterFullScreen;
|
||||
|
||||
/// Defines a custom RoutePageBuilder for the fullscreen
|
||||
final ChewieRoutePageBuilder? routePageBuilder;
|
||||
|
||||
/// Defines a delay in milliseconds between entering buffering state and displaying the loading spinner. Set null (default) to disable it.
|
||||
final Duration? progressIndicatorDelay;
|
||||
|
||||
static ChewieController of(BuildContext context) {
|
||||
final chewieControllerProvider =
|
||||
context.dependOnInheritedWidgetOfExactType<ChewieControllerProvider>()!;
|
||||
|
||||
return chewieControllerProvider.controller;
|
||||
}
|
||||
|
||||
bool _isFullScreen = false;
|
||||
|
||||
bool get isFullScreen => _isFullScreen;
|
||||
|
||||
bool get isPlaying => videoPlayerController.value.isPlaying;
|
||||
|
||||
Future _initialize() async {
|
||||
await videoPlayerController.setLooping(looping);
|
||||
|
||||
if ((autoInitialize || autoPlay) &&
|
||||
!videoPlayerController.value.isInitialized) {
|
||||
await videoPlayerController.initialize();
|
||||
}
|
||||
|
||||
if (autoPlay) {
|
||||
if (fullScreenByDefault) {
|
||||
enterFullScreen();
|
||||
}
|
||||
|
||||
await videoPlayerController.play();
|
||||
}
|
||||
|
||||
if (startAt != null) {
|
||||
await videoPlayerController.seekTo(startAt!);
|
||||
}
|
||||
|
||||
if (fullScreenByDefault) {
|
||||
videoPlayerController.addListener(_fullScreenListener);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fullScreenListener() async {
|
||||
if (videoPlayerController.value.isPlaying && !_isFullScreen) {
|
||||
enterFullScreen();
|
||||
videoPlayerController.removeListener(_fullScreenListener);
|
||||
}
|
||||
}
|
||||
|
||||
void enterFullScreen() {
|
||||
_isFullScreen = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void exitFullScreen() {
|
||||
_isFullScreen = false;
|
||||
SystemChrome.setPreferredOrientations([
|
||||
// 强制竖屏
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown
|
||||
]);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleFullScreen() {
|
||||
_isFullScreen = !_isFullScreen;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void togglePause() {
|
||||
isPlaying ? pause() : play();
|
||||
}
|
||||
|
||||
Future<void> play() async {
|
||||
await videoPlayerController.play();
|
||||
}
|
||||
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
Future<void> setLooping(bool looping) async {
|
||||
await videoPlayerController.setLooping(looping);
|
||||
}
|
||||
|
||||
Future<void> pause() async {
|
||||
await videoPlayerController.pause();
|
||||
}
|
||||
|
||||
Future<void> seekTo(Duration moment) async {
|
||||
await videoPlayerController.seekTo(moment);
|
||||
}
|
||||
|
||||
Future<void> setVolume(double volume) async {
|
||||
await videoPlayerController.setVolume(volume);
|
||||
}
|
||||
|
||||
void setSubtitle(List<Subtitle> newSubtitle) {
|
||||
subtitle = Subtitles(newSubtitle);
|
||||
}
|
||||
}
|
||||
|
||||
class ChewieControllerProvider extends InheritedWidget {
|
||||
const ChewieControllerProvider({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final ChewieController controller;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ChewieControllerProvider oldWidget) =>
|
||||
controller != oldWidget.controller;
|
||||
}
|
||||
18
lib/utils/file/video/lib/src/chewie_progress_colors.dart
Executable file
18
lib/utils/file/video/lib/src/chewie_progress_colors.dart
Executable file
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class ChewieProgressColors {
|
||||
ChewieProgressColors({
|
||||
Color playedColor = const Color.fromRGBO(255, 0, 0, 0.7),
|
||||
Color bufferedColor = const Color.fromRGBO(30, 30, 200, 0.2),
|
||||
Color handleColor = const Color.fromRGBO(200, 200, 200, 1.0),
|
||||
Color backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5),
|
||||
}) : playedPaint = Paint()..color = playedColor,
|
||||
bufferedPaint = Paint()..color = bufferedColor,
|
||||
handlePaint = Paint()..color = handleColor,
|
||||
backgroundPaint = Paint()..color = backgroundColor;
|
||||
|
||||
final Paint playedPaint;
|
||||
final Paint bufferedPaint;
|
||||
final Paint handlePaint;
|
||||
final Paint backgroundPaint;
|
||||
}
|
||||
955
lib/utils/file/video/lib/src/cupertino/cupertino_controls.dart
Executable file
955
lib/utils/file/video/lib/src/cupertino/cupertino_controls.dart
Executable file
@@ -0,0 +1,955 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
// import 'package:perfect_volume_control/perfect_volume_control.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../chewie.dart';
|
||||
import '../animated_play_pause.dart';
|
||||
import '../center_play_button.dart';
|
||||
import '../helpers/utils.dart';
|
||||
import '../notifiers/index.dart';
|
||||
import 'cupertino_progress_bar.dart';
|
||||
import 'widgets/cupertino_options_dialog.dart';
|
||||
|
||||
class CupertinoControls extends StatefulWidget {
|
||||
const CupertinoControls({
|
||||
required this.backgroundColor,
|
||||
required this.iconColor,
|
||||
this.showPlayButton = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final Color backgroundColor;
|
||||
final Color iconColor;
|
||||
final bool showPlayButton;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _CupertinoControlsState();
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoControlsState extends State<CupertinoControls>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late PlayerNotifier notifier;
|
||||
late VideoPlayerValue _latestValue;
|
||||
double? _latestVolume;
|
||||
Timer? _hideTimer;
|
||||
final marginSize = 5.0;
|
||||
Timer? _expandCollapseTimer;
|
||||
Timer? _initTimer;
|
||||
bool _dragging = false;
|
||||
Duration? _subtitlesPosition;
|
||||
bool _subtitleOn = false;
|
||||
Timer? _bufferingDisplayTimer;
|
||||
bool _displayBufferingIndicator = false;
|
||||
|
||||
late VideoPlayerController controller;
|
||||
|
||||
// We know that _chewieController is set in didChangeDependencies
|
||||
ChewieController get chewieController => _chewieController!;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notifier = Provider.of<PlayerNotifier>(context, listen: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_latestValue.hasError) {
|
||||
return chewieController.errorBuilder != null
|
||||
? chewieController.errorBuilder!(
|
||||
context,
|
||||
chewieController.videoPlayerController.value.errorDescription!,
|
||||
)
|
||||
: const Center(
|
||||
child: Icon(
|
||||
CupertinoIcons.exclamationmark_circle,
|
||||
color: Colors.white,
|
||||
size: 42,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final backgroundColor = widget.backgroundColor;
|
||||
final iconColor = widget.iconColor;
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
final barHeight = orientation == Orientation.portrait ? 30.0 : 47.0;
|
||||
final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0;
|
||||
double volumeStart = 0.0; //开始手指位置
|
||||
double volume = 0.0; //开始声音
|
||||
double _brightness = 0.0; //屏幕亮度
|
||||
double volumeDouble = 0.018;
|
||||
|
||||
return MouseRegion(
|
||||
onHover: (_) => _cancelAndRestartTimer(),
|
||||
child: GestureDetector(
|
||||
onTap: () => _cancelAndRestartTimer(),
|
||||
onDoubleTap: _playPause,
|
||||
onLongPress: () {
|
||||
///长按快进
|
||||
///
|
||||
|
||||
printInfo(info: "长按快进");
|
||||
},
|
||||
onLongPressStart: (details) {
|
||||
controller.setPlaybackSpeed(3);
|
||||
},
|
||||
onLongPressEnd: (details) {
|
||||
controller.setPlaybackSpeed(1);
|
||||
},
|
||||
onVerticalDragCancel: () {
|
||||
print("onVerticalDragCancel");
|
||||
},
|
||||
// onVerticalDragDown: (details) {
|
||||
// print(
|
||||
// "onVerticalDragDown---${details.globalPosition}---${details.localPosition}");
|
||||
// },
|
||||
onVerticalDragEnd: (details) {
|
||||
// print(
|
||||
// "onVerticalDragEnd---${details.velocity}---${details.primaryVelocity}");
|
||||
},
|
||||
onVerticalDragStart: (details) async {
|
||||
volumeStart = details.localPosition.dy;
|
||||
// PerfectVolumeControl.getVolume();
|
||||
// volume = await PerfectVolumeControl.volume;
|
||||
_brightness = await ScreenBrightness().current;
|
||||
print("volumevolume---volume$volume---_brightness $_brightness");
|
||||
},
|
||||
onVerticalDragUpdate: (details) async {
|
||||
double midle = MediaQuery.of(context).size.width / 2;
|
||||
double width = details.localPosition.dx;
|
||||
print("====== volume $volume");
|
||||
if (midle > width) {
|
||||
if (details.delta.direction > 0) {
|
||||
_brightness =
|
||||
_brightness - volumeDouble * details.primaryDelta!.obs.value;
|
||||
if (_brightness <= 0) {
|
||||
_brightness = 0;
|
||||
}
|
||||
print("====== 左边向下滑 $_brightness");
|
||||
} else {
|
||||
_brightness =
|
||||
_brightness - volumeDouble * details.primaryDelta!.obs.value;
|
||||
if (_brightness >= 1) {
|
||||
_brightness = 1;
|
||||
}
|
||||
print("====== 左边向上滑 $_brightness");
|
||||
}
|
||||
await ScreenBrightness().setScreenBrightness(_brightness);
|
||||
} else {
|
||||
if (details.delta.direction > 0) {
|
||||
volume = volume - volumeDouble * details.primaryDelta!.obs.value;
|
||||
if (volume <= 0) {
|
||||
volume = 0;
|
||||
}
|
||||
print("====== 右边向下滑 $volume");
|
||||
} else {
|
||||
print("====== 右边向上滑 之前 $volume");
|
||||
volume = volume - volumeDouble * details.primaryDelta!.obs.value;
|
||||
if (volume >= 1) {
|
||||
volume = 1;
|
||||
}
|
||||
print("====== 右边向上滑 $volume");
|
||||
}
|
||||
// PerfectVolumeControl.setVolume(volume);
|
||||
}
|
||||
|
||||
// print(
|
||||
// "高度====== ${details.localPosition.dy} /250 ${details.localPosition.dy / 250}");
|
||||
//
|
||||
// print(
|
||||
// "onVerticalDragUpdate---${details.globalPosition}---${details.localPosition}---${details.delta}");
|
||||
},
|
||||
child: AbsorbPointer(
|
||||
absorbing: notifier.hideStuff,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_displayBufferingIndicator)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
_buildHitArea(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
_buildTopBar(
|
||||
backgroundColor,
|
||||
iconColor,
|
||||
barHeight,
|
||||
buttonPadding,
|
||||
),
|
||||
const Spacer(),
|
||||
if (_subtitleOn)
|
||||
Transform.translate(
|
||||
offset: Offset(
|
||||
0.0,
|
||||
notifier.hideStuff ? barHeight * 0.8 : 0.0,
|
||||
),
|
||||
child: _buildSubtitles(chewieController.subtitle!),
|
||||
),
|
||||
_buildBottomBar(backgroundColor, iconColor, barHeight),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispose() {
|
||||
controller.removeListener(_updateState);
|
||||
_hideTimer?.cancel();
|
||||
_expandCollapseTimer?.cancel();
|
||||
_initTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final oldController = _chewieController;
|
||||
_chewieController = ChewieController.of(context);
|
||||
controller = chewieController.videoPlayerController;
|
||||
|
||||
if (oldController != chewieController) {
|
||||
_dispose();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
GestureDetector _buildOptionsButton(
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
final options = <OptionItem>[];
|
||||
|
||||
if (chewieController.additionalOptions != null &&
|
||||
chewieController.additionalOptions!(context).isNotEmpty) {
|
||||
options.addAll(chewieController.additionalOptions!(context));
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
if (chewieController.optionsBuilder != null) {
|
||||
await chewieController.optionsBuilder!(context, options);
|
||||
} else {
|
||||
await showCupertinoModalPopup<OptionItem>(
|
||||
context: context,
|
||||
semanticsDismissible: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => CupertinoOptionsDialog(
|
||||
options: options,
|
||||
cancelButtonText:
|
||||
chewieController.optionsTranslation?.cancelButtonText,
|
||||
),
|
||||
);
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(left: 4.0, right: 8.0),
|
||||
margin: const EdgeInsets.only(right: 6.0),
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
color: iconColor,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitles(Subtitles subtitles) {
|
||||
if (!_subtitleOn) {
|
||||
return const SizedBox();
|
||||
}
|
||||
if (_subtitlesPosition == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition!);
|
||||
if (currentSubtitle.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
if (chewieController.subtitleBuilder != null) {
|
||||
return chewieController.subtitleBuilder!(
|
||||
context,
|
||||
currentSubtitle.first!.text,
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: marginSize, right: marginSize),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x96000000),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Text(
|
||||
currentSubtitle.first!.text.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar(
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
return SafeArea(
|
||||
bottom: chewieController.isFullScreen,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
alignment: Alignment.bottomCenter,
|
||||
margin: EdgeInsets.all(marginSize),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(
|
||||
sigmaX: 10.0,
|
||||
sigmaY: 10.0,
|
||||
),
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: backgroundColor,
|
||||
child: chewieController.isLive
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
_buildPlayPause(controller, iconColor, barHeight),
|
||||
_buildLive(iconColor),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: <Widget>[
|
||||
// _buildSkipBack(iconColor, barHeight),
|
||||
_buildPlayPause(controller, iconColor, barHeight),
|
||||
// _buildSkipForward(iconColor, barHeight),
|
||||
_buildPosition(iconColor),
|
||||
_buildProgressBar(),
|
||||
_buildRemaining(iconColor),
|
||||
_buildSubtitleToggle(iconColor, barHeight),
|
||||
// if (chewieController.allowPlaybackSpeedChanging)
|
||||
// _buildSpeedButton(controller, iconColor, barHeight),
|
||||
if (chewieController.additionalOptions != null &&
|
||||
chewieController
|
||||
.additionalOptions!(context).isNotEmpty)
|
||||
_buildOptionsButton(iconColor, barHeight),
|
||||
// if (chewieController.allowFullScreen)
|
||||
// _buildExpandButton(
|
||||
// backgroundColor,
|
||||
// iconColor,
|
||||
// barHeight,
|
||||
// MediaQuery.of(context).orientation ==
|
||||
// Orientation.portrait
|
||||
// ? 16.0
|
||||
// : 24.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLive(Color iconColor) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Text(
|
||||
'LIVE',
|
||||
style: TextStyle(color: iconColor, fontSize: 12.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildExpandButton(
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
double buttonPadding, {
|
||||
bool? showRight,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: _onExpandCollapse,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 10.0),
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: EdgeInsets.only(
|
||||
left: buttonPadding,
|
||||
right: buttonPadding,
|
||||
),
|
||||
color: backgroundColor,
|
||||
child: Center(
|
||||
child: showRight == true
|
||||
? const Icon(
|
||||
Icons.keyboard_arrow_left,
|
||||
size: 22,
|
||||
color: Colors.white,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.fullscreen,
|
||||
size: 22,
|
||||
color: Colors.white,
|
||||
)
|
||||
// 'Image.asset(
|
||||
// Assets.imagesFullScreen,
|
||||
// width: 22,
|
||||
// height: 22,
|
||||
// ),
|
||||
// child: Icon(
|
||||
// chewieController.isFullScreen
|
||||
// ? CupertinoIcons.arrow_down_right_arrow_up_left
|
||||
// : CupertinoIcons.arrow_up_left_arrow_down_right,
|
||||
// color: iconColor,
|
||||
// size: 16,
|
||||
// ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHitArea() {
|
||||
final bool isFinished = _latestValue.position >= _latestValue.duration;
|
||||
final bool showPlayButton =
|
||||
widget.showPlayButton && !_latestValue.isPlaying && !_dragging;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: _latestValue.isPlaying
|
||||
? _cancelAndRestartTimer
|
||||
: () {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: widget.backgroundColor,
|
||||
iconColor: widget.iconColor,
|
||||
isFinished: isFinished,
|
||||
isPlaying: controller.value.isPlaying,
|
||||
show: showPlayButton,
|
||||
onPressed: _playPause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildMuteButton(
|
||||
VideoPlayerController controller,
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
double buttonPadding,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (_latestValue.volume == 0) {
|
||||
controller.setVolume(_latestVolume ?? 0.5);
|
||||
} else {
|
||||
_latestVolume = controller.value.volume;
|
||||
controller.setVolume(0.0);
|
||||
}
|
||||
},
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 10.0),
|
||||
child: ColoredBox(
|
||||
color: backgroundColor,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: EdgeInsets.only(
|
||||
left: buttonPadding,
|
||||
right: buttonPadding,
|
||||
),
|
||||
child: Icon(
|
||||
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
|
||||
color: iconColor,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildPlayPause(
|
||||
VideoPlayerController controller,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: _playPause,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
child: AnimatedPlayPause(
|
||||
color: widget.iconColor,
|
||||
playing: controller.value.isPlaying,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPosition(Color iconColor) {
|
||||
final position = _latestValue.position;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Text(
|
||||
formatDuration(position),
|
||||
style: TextStyle(
|
||||
color: iconColor,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRemaining(Color iconColor) {
|
||||
final position = _latestValue.duration - _latestValue.position;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Text(
|
||||
'-${formatDuration(position)}',
|
||||
style: TextStyle(color: iconColor, fontSize: 12.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitleToggle(Color iconColor, double barHeight) {
|
||||
//if don't have subtitle hiden button
|
||||
if (chewieController.subtitle?.isEmpty ?? true) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: _subtitleToggle,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
margin: const EdgeInsets.only(right: 10.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.subtitles,
|
||||
color: _subtitleOn ? iconColor : Colors.grey[700],
|
||||
size: 16.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _subtitleToggle() {
|
||||
setState(() {
|
||||
_subtitleOn = !_subtitleOn;
|
||||
});
|
||||
}
|
||||
|
||||
GestureDetector _buildSkipBack(Color iconColor, double barHeight) {
|
||||
return GestureDetector(
|
||||
onTap: _skipBack,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
margin: const EdgeInsets.only(left: 10.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.gobackward_15,
|
||||
color: iconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildSkipForward(Color iconColor, double barHeight) {
|
||||
return GestureDetector(
|
||||
onTap: _skipForward,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 8.0,
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
right: 8.0,
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.goforward_15,
|
||||
color: iconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildSpeedButton(
|
||||
VideoPlayerController controller,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
final chosenSpeed = await showCupertinoModalPopup<double>(
|
||||
context: context,
|
||||
semanticsDismissible: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => _PlaybackSpeedDialog(
|
||||
speeds: chewieController.playbackSpeeds,
|
||||
selected: _latestValue.playbackSpeed,
|
||||
),
|
||||
);
|
||||
|
||||
if (chosenSpeed != null) {
|
||||
controller.setPlaybackSpeed(chosenSpeed);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 8.0,
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
right: 8.0,
|
||||
),
|
||||
child: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.skewY(0.0)
|
||||
..rotateX(math.pi)
|
||||
..rotateZ(math.pi * 0.8),
|
||||
child: Icon(
|
||||
Icons.speed,
|
||||
color: iconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopBar(
|
||||
Color backgroundColor,
|
||||
Color iconColor,
|
||||
double barHeight,
|
||||
double buttonPadding,
|
||||
) {
|
||||
return Container(
|
||||
height: barHeight,
|
||||
margin: EdgeInsets.only(
|
||||
top: marginSize,
|
||||
right: marginSize,
|
||||
left: marginSize,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (chewieController.allowFullScreen)
|
||||
if (chewieController.isFullScreen)
|
||||
_buildExpandButton(
|
||||
backgroundColor,
|
||||
iconColor,
|
||||
barHeight,
|
||||
buttonPadding,
|
||||
showRight: true,
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
// if (chewieController.allowMuting)
|
||||
// _buildMuteButton(
|
||||
// controller,
|
||||
// backgroundColor,
|
||||
// iconColor,
|
||||
// barHeight,
|
||||
// buttonPadding,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _cancelAndRestartTimer() {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
|
||||
_startHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
|
||||
controller.addListener(_updateState);
|
||||
|
||||
_updateState();
|
||||
|
||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
if (chewieController.showControlsOnInitialize) {
|
||||
_initTimer = Timer(const Duration(milliseconds: 200), () {
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onExpandCollapse() {
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
Navigator.of(context).pop();
|
||||
// notifier.hideStuff = true;
|
||||
//
|
||||
// chewieController.toggleFullScreen();
|
||||
// _expandCollapseTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
// setState(() {
|
||||
// _cancelAndRestartTimer();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildProgressBar() {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: CupertinoVideoProgressBar(
|
||||
controller,
|
||||
onDragStart: () {
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
});
|
||||
|
||||
_hideTimer?.cancel();
|
||||
},
|
||||
onDragEnd: () {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
});
|
||||
|
||||
_startHideTimer();
|
||||
},
|
||||
colors: chewieController.cupertinoProgressColors ??
|
||||
ChewieProgressColors(
|
||||
playedColor: const Color.fromARGB(
|
||||
120,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
handleColor: const Color.fromARGB(
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
bufferedColor: const Color.fromARGB(
|
||||
60,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
backgroundColor: const Color.fromARGB(
|
||||
20,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _playPause() {
|
||||
final isFinished = _latestValue.position >= _latestValue.duration;
|
||||
|
||||
setState(() {
|
||||
if (controller.value.isPlaying) {
|
||||
notifier.hideStuff = false;
|
||||
_hideTimer?.cancel();
|
||||
controller.pause();
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (!controller.value.isInitialized) {
|
||||
controller.initialize().then((_) {
|
||||
controller.play();
|
||||
});
|
||||
} else {
|
||||
if (isFinished) {
|
||||
controller.seekTo(Duration.zero);
|
||||
}
|
||||
controller.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _skipBack() {
|
||||
_cancelAndRestartTimer();
|
||||
final beginning = Duration.zero.inMilliseconds;
|
||||
final skip =
|
||||
(_latestValue.position - const Duration(seconds: 15)).inMilliseconds;
|
||||
controller.seekTo(Duration(milliseconds: math.max(skip, beginning)));
|
||||
}
|
||||
|
||||
void _skipForward() {
|
||||
_cancelAndRestartTimer();
|
||||
final end = _latestValue.duration.inMilliseconds;
|
||||
final skip =
|
||||
(_latestValue.position + const Duration(seconds: 15)).inMilliseconds;
|
||||
controller.seekTo(Duration(milliseconds: math.min(skip, end)));
|
||||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
||||
? ChewieController.defaultHideControlsTimer
|
||||
: chewieController.hideControlsTimer;
|
||||
_hideTimer = Timer(hideControlsTimer, () {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _bufferingTimerTimeout() {
|
||||
_displayBufferingIndicator = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
if (!mounted) return;
|
||||
|
||||
// display the progress bar indicator only after the buffering delay if it has been set
|
||||
if (chewieController.progressIndicatorDelay != null) {
|
||||
if (controller.value.isBuffering) {
|
||||
_bufferingDisplayTimer ??= Timer(
|
||||
chewieController.progressIndicatorDelay!,
|
||||
_bufferingTimerTimeout,
|
||||
);
|
||||
} else {
|
||||
_bufferingDisplayTimer?.cancel();
|
||||
_bufferingDisplayTimer = null;
|
||||
_displayBufferingIndicator = false;
|
||||
}
|
||||
} else {
|
||||
_displayBufferingIndicator = controller.value.isBuffering;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_latestValue = controller.value;
|
||||
_subtitlesPosition = controller.value.position;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaybackSpeedDialog extends StatelessWidget {
|
||||
const _PlaybackSpeedDialog({
|
||||
Key? key,
|
||||
required List<double> speeds,
|
||||
required double selected,
|
||||
}) : _speeds = speeds,
|
||||
_selected = selected,
|
||||
super(key: key);
|
||||
|
||||
final List<double> _speeds;
|
||||
final double _selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedColor = CupertinoTheme.of(context).primaryColor;
|
||||
|
||||
return CupertinoActionSheet(
|
||||
actions: _speeds
|
||||
.map(
|
||||
(e) => CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(e);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (e == _selected)
|
||||
Icon(Icons.check, size: 20.0, color: selectedColor),
|
||||
Text(e.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/utils/file/video/lib/src/cupertino/cupertino_progress_bar.dart
Executable file
38
lib/utils/file/video/lib/src/cupertino/cupertino_progress_bar.dart
Executable 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
41
lib/utils/file/video/lib/src/cupertino/widgets/cupertino_options_dialog.dart
Executable file
41
lib/utils/file/video/lib/src/cupertino/widgets/cupertino_options_dialog.dart
Executable 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'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
35
lib/utils/file/video/lib/src/helpers/adaptive_controls.dart
Executable file
35
lib/utils/file/video/lib/src/helpers/adaptive_controls.dart
Executable file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../chewie.dart';
|
||||
|
||||
class AdaptiveControls extends StatelessWidget {
|
||||
const AdaptiveControls({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
return const CupertinoControls(
|
||||
backgroundColor: Color.fromRGBO(41, 41, 41, 0.7),
|
||||
iconColor: Color.fromARGB(255, 200, 200, 200),
|
||||
);
|
||||
return const MaterialControls();
|
||||
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
case TargetPlatform.linux:
|
||||
return const MaterialDesktopControls();
|
||||
|
||||
case TargetPlatform.iOS:
|
||||
return const CupertinoControls(
|
||||
backgroundColor: Color.fromRGBO(41, 41, 41, 0.7),
|
||||
iconColor: Color.fromARGB(255, 200, 200, 200),
|
||||
);
|
||||
default:
|
||||
return const MaterialControls();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
lib/utils/file/video/lib/src/helpers/utils.dart
Executable file
32
lib/utils/file/video/lib/src/helpers/utils.dart
Executable file
@@ -0,0 +1,32 @@
|
||||
String formatDuration(Duration position) {
|
||||
final ms = position.inMilliseconds;
|
||||
|
||||
int seconds = ms ~/ 1000;
|
||||
final int hours = seconds ~/ 3600;
|
||||
seconds = seconds % 3600;
|
||||
final minutes = seconds ~/ 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
final hoursString = hours >= 10
|
||||
? '$hours'
|
||||
: hours == 0
|
||||
? '00'
|
||||
: '0$hours';
|
||||
|
||||
final minutesString = minutes >= 10
|
||||
? '$minutes'
|
||||
: minutes == 0
|
||||
? '00'
|
||||
: '0$minutes';
|
||||
|
||||
final secondsString = seconds >= 10
|
||||
? '$seconds'
|
||||
: seconds == 0
|
||||
? '00'
|
||||
: '0$seconds';
|
||||
|
||||
final formattedTime =
|
||||
'${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString';
|
||||
|
||||
return formattedTime;
|
||||
}
|
||||
623
lib/utils/file/video/lib/src/material/material_controls.dart
Executable file
623
lib/utils/file/video/lib/src/material/material_controls.dart
Executable file
@@ -0,0 +1,623 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../chewie.dart';
|
||||
import '../center_play_button.dart';
|
||||
import '../helpers/utils.dart';
|
||||
import '../notifiers/index.dart';
|
||||
import 'material_progress_bar.dart';
|
||||
import 'widgets/options_dialog.dart';
|
||||
import 'widgets/playback_speed_dialog.dart';
|
||||
|
||||
class MaterialControls extends StatefulWidget {
|
||||
const MaterialControls({
|
||||
this.showPlayButton = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool showPlayButton;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _MaterialControlsState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MaterialControlsState extends State<MaterialControls>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late PlayerNotifier notifier;
|
||||
late VideoPlayerValue _latestValue;
|
||||
double? _latestVolume;
|
||||
Timer? _hideTimer;
|
||||
Timer? _initTimer;
|
||||
late var _subtitlesPosition = Duration.zero;
|
||||
bool _subtitleOn = false;
|
||||
Timer? _showAfterExpandCollapseTimer;
|
||||
bool _dragging = false;
|
||||
bool _displayTapped = false;
|
||||
Timer? _bufferingDisplayTimer;
|
||||
bool _displayBufferingIndicator = false;
|
||||
|
||||
final barHeight = 48.0 * 1.5;
|
||||
final marginSize = 5.0;
|
||||
|
||||
late VideoPlayerController controller;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
// We know that _chewieController is set in didChangeDependencies
|
||||
ChewieController get chewieController => _chewieController!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notifier = Provider.of<PlayerNotifier>(context, listen: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_latestValue.hasError) {
|
||||
return chewieController.errorBuilder?.call(
|
||||
context,
|
||||
chewieController.videoPlayerController.value.errorDescription!,
|
||||
) ??
|
||||
const Center(
|
||||
child: Icon(
|
||||
Icons.error,
|
||||
color: Colors.white,
|
||||
size: 42,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
onHover: (_) {
|
||||
_cancelAndRestartTimer();
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () => _cancelAndRestartTimer(),
|
||||
onLongPress: () {
|
||||
///长按快进
|
||||
///
|
||||
},
|
||||
onLongPressStart: (details) {
|
||||
controller.setPlaybackSpeed(3);
|
||||
},
|
||||
onLongPressEnd: (details) {
|
||||
controller.setPlaybackSpeed(1);
|
||||
},
|
||||
child: AbsorbPointer(
|
||||
absorbing: notifier.hideStuff,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_displayBufferingIndicator)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
_buildHitArea(),
|
||||
_buildActionBar(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
if (_subtitleOn)
|
||||
Transform.translate(
|
||||
offset: Offset(
|
||||
0.0,
|
||||
notifier.hideStuff ? barHeight * 0.8 : 0.0,
|
||||
),
|
||||
child:
|
||||
_buildSubtitles(context, chewieController.subtitle!),
|
||||
),
|
||||
_buildBottomBar(context),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispose() {
|
||||
controller.removeListener(_updateState);
|
||||
_hideTimer?.cancel();
|
||||
_initTimer?.cancel();
|
||||
_showAfterExpandCollapseTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final oldController = _chewieController;
|
||||
_chewieController = ChewieController.of(context);
|
||||
controller = chewieController.videoPlayerController;
|
||||
|
||||
if (oldController != chewieController) {
|
||||
_dispose();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
Widget _buildActionBar() {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildSubtitleToggle(),
|
||||
if (chewieController.showOptions) _buildOptionsButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionsButton() {
|
||||
final options = <OptionItem>[
|
||||
OptionItem(
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_onSpeedButtonTap();
|
||||
},
|
||||
iconData: Icons.speed,
|
||||
title: chewieController.optionsTranslation?.playbackSpeedButtonText ??
|
||||
'Playback speed',
|
||||
)
|
||||
];
|
||||
|
||||
if (chewieController.additionalOptions != null &&
|
||||
chewieController.additionalOptions!(context).isNotEmpty) {
|
||||
options.addAll(chewieController.additionalOptions!(context));
|
||||
}
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
if (chewieController.optionsBuilder != null) {
|
||||
await chewieController.optionsBuilder!(context, options);
|
||||
} else {
|
||||
await showModalBottomSheet<OptionItem>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => OptionsDialog(
|
||||
options: options,
|
||||
cancelButtonText:
|
||||
chewieController.optionsTranslation?.cancelButtonText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
|
||||
if (!_subtitleOn) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition);
|
||||
if (currentSubtitle.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
if (chewieController.subtitleBuilder != null) {
|
||||
return chewieController.subtitleBuilder!(
|
||||
context,
|
||||
currentSubtitle.first!.text,
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(marginSize),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x96000000),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Text(
|
||||
currentSubtitle.first!.text.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AnimatedOpacity _buildBottomBar(
|
||||
BuildContext context,
|
||||
) {
|
||||
final iconColor = Theme.of(context).textTheme.labelMedium!.color;
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: barHeight + (chewieController.isFullScreen ? 10.0 : 0),
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
bottom: !chewieController.isFullScreen ? 10.0 : 0,
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: chewieController.isFullScreen,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
if (chewieController.isLive)
|
||||
const Expanded(child: Text('LIVE'))
|
||||
else
|
||||
_buildPosition(iconColor),
|
||||
if (chewieController.allowMuting)
|
||||
_buildMuteButton(controller),
|
||||
const Spacer(),
|
||||
if (chewieController.allowFullScreen) _buildExpandButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: chewieController.isFullScreen ? 15.0 : 0,
|
||||
),
|
||||
if (!chewieController.isLive)
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildProgressBar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildMuteButton(
|
||||
VideoPlayerController controller,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (_latestValue.volume == 0) {
|
||||
controller.setVolume(_latestVolume ?? 0.5);
|
||||
} else {
|
||||
_latestVolume = controller.value.volume;
|
||||
controller.setVolume(0.0);
|
||||
}
|
||||
},
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ClipRect(
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
),
|
||||
child: Icon(
|
||||
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildExpandButton() {
|
||||
return GestureDetector(
|
||||
onTap: _onExpandCollapse,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: barHeight + (chewieController.isFullScreen ? 15.0 : 0),
|
||||
margin: const EdgeInsets.only(right: 12.0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
chewieController.isFullScreen
|
||||
? Icons.fullscreen_exit
|
||||
: Icons.fullscreen,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHitArea() {
|
||||
final bool isFinished = _latestValue.position >= _latestValue.duration;
|
||||
final bool showPlayButton =
|
||||
widget.showPlayButton && !_dragging && !notifier.hideStuff;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (_latestValue.isPlaying) {
|
||||
if (_displayTapped) {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
}
|
||||
} else {
|
||||
_playPause();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: Colors.black54,
|
||||
iconColor: Colors.white,
|
||||
isFinished: isFinished,
|
||||
isPlaying: controller.value.isPlaying,
|
||||
show: showPlayButton,
|
||||
onPressed: _playPause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSpeedButtonTap() async {
|
||||
_hideTimer?.cancel();
|
||||
|
||||
final chosenSpeed = await showModalBottomSheet<double>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: chewieController.useRootNavigator,
|
||||
builder: (context) => PlaybackSpeedDialog(
|
||||
speeds: chewieController.playbackSpeeds,
|
||||
selected: _latestValue.playbackSpeed,
|
||||
),
|
||||
);
|
||||
|
||||
if (chosenSpeed != null) {
|
||||
controller.setPlaybackSpeed(chosenSpeed);
|
||||
}
|
||||
|
||||
if (_latestValue.isPlaying) {
|
||||
_startHideTimer();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPosition(Color? iconColor) {
|
||||
final position = _latestValue.position;
|
||||
final duration = _latestValue.duration;
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: '${formatDuration(position)} ',
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: '/ ${formatDuration(duration)}',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.white.withOpacity(.75),
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
)
|
||||
],
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitleToggle() {
|
||||
//if don't have subtitle hiden button
|
||||
if (chewieController.subtitle?.isEmpty ?? true) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: _onSubtitleTap,
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
),
|
||||
child: Icon(
|
||||
_subtitleOn
|
||||
? Icons.closed_caption
|
||||
: Icons.closed_caption_off_outlined,
|
||||
color: _subtitleOn ? Colors.white : Colors.grey[700],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubtitleTap() {
|
||||
setState(() {
|
||||
_subtitleOn = !_subtitleOn;
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelAndRestartTimer() {
|
||||
_hideTimer?.cancel();
|
||||
_startHideTimer();
|
||||
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
_displayTapped = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
|
||||
controller.addListener(_updateState);
|
||||
|
||||
_updateState();
|
||||
|
||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
if (chewieController.showControlsOnInitialize) {
|
||||
_initTimer = Timer(const Duration(milliseconds: 200), () {
|
||||
setState(() {
|
||||
notifier.hideStuff = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onExpandCollapse() {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
|
||||
chewieController.toggleFullScreen();
|
||||
_showAfterExpandCollapseTimer =
|
||||
Timer(const Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
_cancelAndRestartTimer();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _playPause() {
|
||||
final isFinished = _latestValue.position >= _latestValue.duration;
|
||||
|
||||
setState(() {
|
||||
if (controller.value.isPlaying) {
|
||||
notifier.hideStuff = false;
|
||||
_hideTimer?.cancel();
|
||||
controller.pause();
|
||||
} else {
|
||||
_cancelAndRestartTimer();
|
||||
|
||||
if (!controller.value.isInitialized) {
|
||||
controller.initialize().then((_) {
|
||||
controller.play();
|
||||
});
|
||||
} else {
|
||||
if (isFinished) {
|
||||
controller.seekTo(Duration.zero);
|
||||
}
|
||||
controller.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
||||
? ChewieController.defaultHideControlsTimer
|
||||
: chewieController.hideControlsTimer;
|
||||
_hideTimer = Timer(hideControlsTimer, () {
|
||||
setState(() {
|
||||
notifier.hideStuff = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _bufferingTimerTimeout() {
|
||||
_displayBufferingIndicator = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
if (!mounted) return;
|
||||
|
||||
// display the progress bar indicator only after the buffering delay if it has been set
|
||||
if (chewieController.progressIndicatorDelay != null) {
|
||||
if (controller.value.isBuffering) {
|
||||
_bufferingDisplayTimer ??= Timer(
|
||||
chewieController.progressIndicatorDelay!,
|
||||
_bufferingTimerTimeout,
|
||||
);
|
||||
} else {
|
||||
_bufferingDisplayTimer?.cancel();
|
||||
_bufferingDisplayTimer = null;
|
||||
_displayBufferingIndicator = false;
|
||||
}
|
||||
} else {
|
||||
_displayBufferingIndicator = controller.value.isBuffering;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_latestValue = controller.value;
|
||||
_subtitlesPosition = controller.value.position;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildProgressBar() {
|
||||
return Expanded(
|
||||
child: MaterialVideoProgressBar(
|
||||
controller,
|
||||
onDragStart: () {
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
});
|
||||
|
||||
_hideTimer?.cancel();
|
||||
},
|
||||
onDragEnd: () {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
});
|
||||
|
||||
_startHideTimer();
|
||||
},
|
||||
colors: chewieController.materialProgressColors ??
|
||||
ChewieProgressColors(
|
||||
playedColor: Theme.of(context).colorScheme.secondary,
|
||||
handleColor: Theme.of(context).colorScheme.secondary,
|
||||
bufferedColor: Theme.of(context).disabledColor.withOpacity(0.5),
|
||||
backgroundColor: Theme.of(context).disabledColor.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
593
lib/utils/file/video/lib/src/material/material_desktop_controls.dart
Executable file
593
lib/utils/file/video/lib/src/material/material_desktop_controls.dart
Executable 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/utils/file/video/lib/src/material/material_progress_bar.dart
Executable file
39
lib/utils/file/video/lib/src/material/material_progress_bar.dart
Executable 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
60
lib/utils/file/video/lib/src/material/widgets/options_dialog.dart
Executable file
60
lib/utils/file/video/lib/src/material/widgets/options_dialog.dart
Executable 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',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
lib/utils/file/video/lib/src/material/widgets/playback_speed_dialog.dart
Executable file
49
lib/utils/file/video/lib/src/material/widgets/playback_speed_dialog.dart
Executable 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
3
lib/utils/file/video/lib/src/models/index.dart
Executable file
3
lib/utils/file/video/lib/src/models/index.dart
Executable file
@@ -0,0 +1,3 @@
|
||||
export 'option_item.dart';
|
||||
export 'options_translation.dart';
|
||||
export 'subtitle_model.dart';
|
||||
48
lib/utils/file/video/lib/src/models/option_item.dart
Executable file
48
lib/utils/file/video/lib/src/models/option_item.dart
Executable file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OptionItem {
|
||||
OptionItem({
|
||||
required this.onTap,
|
||||
required this.iconData,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
Function()? onTap;
|
||||
IconData iconData;
|
||||
String title;
|
||||
String? subtitle;
|
||||
|
||||
OptionItem copyWith({
|
||||
Function()? onTap,
|
||||
IconData? iconData,
|
||||
String? title,
|
||||
String? subtitle,
|
||||
}) {
|
||||
return OptionItem(
|
||||
onTap: onTap ?? this.onTap,
|
||||
iconData: iconData ?? this.iconData,
|
||||
title: title ?? this.title,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'OptionItem(onTap: $onTap, iconData: $iconData, title: $title, subtitle: $subtitle)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is OptionItem &&
|
||||
other.onTap == onTap &&
|
||||
other.iconData == iconData &&
|
||||
other.title == title &&
|
||||
other.subtitle == subtitle;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
onTap.hashCode ^ iconData.hashCode ^ title.hashCode ^ subtitle.hashCode;
|
||||
}
|
||||
44
lib/utils/file/video/lib/src/models/options_translation.dart
Executable file
44
lib/utils/file/video/lib/src/models/options_translation.dart
Executable file
@@ -0,0 +1,44 @@
|
||||
class OptionsTranslation {
|
||||
OptionsTranslation({
|
||||
this.playbackSpeedButtonText,
|
||||
this.subtitlesButtonText,
|
||||
this.cancelButtonText,
|
||||
});
|
||||
|
||||
String? playbackSpeedButtonText;
|
||||
String? subtitlesButtonText;
|
||||
String? cancelButtonText;
|
||||
|
||||
OptionsTranslation copyWith({
|
||||
String? playbackSpeedButtonText,
|
||||
String? subtitlesButtonText,
|
||||
String? cancelButtonText,
|
||||
}) {
|
||||
return OptionsTranslation(
|
||||
playbackSpeedButtonText:
|
||||
playbackSpeedButtonText ?? this.playbackSpeedButtonText,
|
||||
subtitlesButtonText: subtitlesButtonText ?? this.subtitlesButtonText,
|
||||
cancelButtonText: cancelButtonText ?? this.cancelButtonText,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'OptionsTranslation(playbackSpeedButtonText: $playbackSpeedButtonText, subtitlesButtonText: $subtitlesButtonText, cancelButtonText: $cancelButtonText)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is OptionsTranslation &&
|
||||
other.playbackSpeedButtonText == playbackSpeedButtonText &&
|
||||
other.subtitlesButtonText == subtitlesButtonText &&
|
||||
other.cancelButtonText == cancelButtonText;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
playbackSpeedButtonText.hashCode ^
|
||||
subtitlesButtonText.hashCode ^
|
||||
cancelButtonText.hashCode;
|
||||
}
|
||||
67
lib/utils/file/video/lib/src/models/subtitle_model.dart
Executable file
67
lib/utils/file/video/lib/src/models/subtitle_model.dart
Executable file
@@ -0,0 +1,67 @@
|
||||
class Subtitles {
|
||||
Subtitles(this.subtitle);
|
||||
|
||||
final List<Subtitle?> subtitle;
|
||||
|
||||
bool get isEmpty => subtitle.isEmpty;
|
||||
|
||||
bool get isNotEmpty => !isEmpty;
|
||||
|
||||
List<Subtitle?> getByPosition(Duration position) {
|
||||
final found = subtitle.where((item) {
|
||||
if (item != null) return position >= item.start && position <= item.end;
|
||||
return false;
|
||||
}).toList();
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
class Subtitle {
|
||||
Subtitle({
|
||||
required this.index,
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
Subtitle copyWith({
|
||||
int? index,
|
||||
Duration? start,
|
||||
Duration? end,
|
||||
dynamic text,
|
||||
}) {
|
||||
return Subtitle(
|
||||
index: index ?? this.index,
|
||||
start: start ?? this.start,
|
||||
end: end ?? this.end,
|
||||
text: text ?? this.text,
|
||||
);
|
||||
}
|
||||
|
||||
final int index;
|
||||
final Duration start;
|
||||
final Duration end;
|
||||
final dynamic text;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Subtitle(index: $index, start: $start, end: $end, text: $text)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is Subtitle &&
|
||||
other.index == index &&
|
||||
other.start == start &&
|
||||
other.end == end &&
|
||||
other.text == text;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return index.hashCode ^ start.hashCode ^ end.hashCode ^ text.hashCode;
|
||||
}
|
||||
}
|
||||
1
lib/utils/file/video/lib/src/notifiers/index.dart
Executable file
1
lib/utils/file/video/lib/src/notifiers/index.dart
Executable file
@@ -0,0 +1 @@
|
||||
export 'player_notifier.dart';
|
||||
28
lib/utils/file/video/lib/src/notifiers/player_notifier.dart
Executable file
28
lib/utils/file/video/lib/src/notifiers/player_notifier.dart
Executable file
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///
|
||||
/// The new State-Manager for Chewie!
|
||||
/// Has to be an instance of Singleton to survive
|
||||
/// over all State-Changes inside chewie
|
||||
///
|
||||
class PlayerNotifier extends ChangeNotifier {
|
||||
PlayerNotifier._(
|
||||
bool hideStuff,
|
||||
) : _hideStuff = hideStuff;
|
||||
|
||||
bool _hideStuff;
|
||||
|
||||
bool get hideStuff => _hideStuff;
|
||||
|
||||
set hideStuff(bool value) {
|
||||
_hideStuff = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PlayerNotifier init() {
|
||||
return PlayerNotifier._(
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
97
lib/utils/file/video/lib/src/player_with_controls.dart
Executable file
97
lib/utils/file/video/lib/src/player_with_controls.dart
Executable file
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import '../chewie.dart';
|
||||
import 'helpers/adaptive_controls.dart';
|
||||
import 'notifiers/index.dart';
|
||||
|
||||
class PlayerWithControls extends StatelessWidget {
|
||||
const PlayerWithControls({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ChewieController chewieController = ChewieController.of(context);
|
||||
|
||||
double calculateAspectRatio(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final width = size.width;
|
||||
final height = size.height;
|
||||
|
||||
return width > height ? width / height : height / width;
|
||||
}
|
||||
|
||||
Widget buildControls(
|
||||
BuildContext context,
|
||||
ChewieController chewieController,
|
||||
) {
|
||||
return chewieController.showControls
|
||||
? chewieController.customControls ?? const AdaptiveControls()
|
||||
: const SizedBox();
|
||||
}
|
||||
|
||||
Widget buildPlayerWithControls(
|
||||
ChewieController chewieController,
|
||||
BuildContext context,
|
||||
) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
if (chewieController.placeholder != null)
|
||||
chewieController.placeholder!,
|
||||
InteractiveViewer(
|
||||
transformationController: chewieController.transformationController,
|
||||
maxScale: chewieController.maxScale,
|
||||
panEnabled: chewieController.zoomAndPan,
|
||||
scaleEnabled: chewieController.zoomAndPan,
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio:
|
||||
chewieController.videoPlayerController.value.aspectRatio,
|
||||
child: VideoPlayer(chewieController.videoPlayerController),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (chewieController.overlay != null) chewieController.overlay!,
|
||||
if (Theme.of(context).platform != TargetPlatform.iOS)
|
||||
Consumer<PlayerNotifier>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PlayerNotifier notifier,
|
||||
Widget? widget,
|
||||
) =>
|
||||
Visibility(
|
||||
visible: !notifier.hideStuff,
|
||||
child: AnimatedOpacity(
|
||||
opacity: notifier.hideStuff ? 0.0 : 0.8,
|
||||
duration: const Duration(
|
||||
milliseconds: 250,
|
||||
),
|
||||
child: const DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.black54),
|
||||
child: SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!chewieController.isFullScreen)
|
||||
buildControls(context, chewieController)
|
||||
else
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: buildControls(context, chewieController),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: AspectRatio(
|
||||
aspectRatio: calculateAspectRatio(context),
|
||||
child: buildPlayerWithControls(chewieController, context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
218
lib/utils/file/video/lib/src/progress_bar.dart
Executable file
218
lib/utils/file/video/lib/src/progress_bar.dart
Executable file
@@ -0,0 +1,218 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../chewie.dart';
|
||||
|
||||
class VideoProgressBar extends StatefulWidget {
|
||||
VideoProgressBar(
|
||||
this.controller, {
|
||||
ChewieProgressColors? colors,
|
||||
this.onDragEnd,
|
||||
this.onDragStart,
|
||||
this.onDragUpdate,
|
||||
Key? key,
|
||||
required this.barHeight,
|
||||
required this.handleHeight,
|
||||
required this.drawShadow,
|
||||
}) : colors = colors ?? ChewieProgressColors(),
|
||||
super(key: key);
|
||||
|
||||
final VideoPlayerController controller;
|
||||
final ChewieProgressColors colors;
|
||||
final Function()? onDragStart;
|
||||
final Function()? onDragEnd;
|
||||
final Function()? onDragUpdate;
|
||||
|
||||
final double barHeight;
|
||||
final double handleHeight;
|
||||
final bool drawShadow;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_VideoProgressBarState createState() {
|
||||
return _VideoProgressBarState();
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoProgressBarState extends State<VideoProgressBar> {
|
||||
void listener() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
bool _controllerWasPlaying = false;
|
||||
|
||||
VideoPlayerController get controller => widget.controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.addListener(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
controller.removeListener(listener);
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
void _seekToRelativePosition(Offset globalPosition) {
|
||||
final box = context.findRenderObject()! as RenderBox;
|
||||
final Offset tapPos = box.globalToLocal(globalPosition);
|
||||
final double relative = tapPos.dx / box.size.width;
|
||||
final Duration position = controller.value.duration * relative;
|
||||
controller.seekTo(position);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onHorizontalDragStart: (DragStartDetails details) {
|
||||
if (!controller.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
_controllerWasPlaying = controller.value.isPlaying;
|
||||
if (_controllerWasPlaying) {
|
||||
controller.pause();
|
||||
}
|
||||
|
||||
widget.onDragStart?.call();
|
||||
},
|
||||
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
||||
if (!controller.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
// Should only seek if it's not running on Android, or if it is,
|
||||
// then the VideoPlayerController cannot be buffering.
|
||||
// On Android, we need to let the player buffer when scrolling
|
||||
// in order to let the player buffer. https://github.com/flutter/flutter/issues/101409
|
||||
final shouldSeekToRelativePosition =
|
||||
!Platform.isAndroid || !controller.value.isBuffering;
|
||||
if (shouldSeekToRelativePosition) {
|
||||
_seekToRelativePosition(details.globalPosition);
|
||||
}
|
||||
|
||||
widget.onDragUpdate?.call();
|
||||
},
|
||||
onHorizontalDragEnd: (DragEndDetails details) {
|
||||
if (_controllerWasPlaying) {
|
||||
controller.play();
|
||||
}
|
||||
|
||||
widget.onDragEnd?.call();
|
||||
},
|
||||
onTapDown: (TapDownDetails details) {
|
||||
if (!controller.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
_seekToRelativePosition(details.globalPosition);
|
||||
},
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
color: Colors.transparent,
|
||||
child: CustomPaint(
|
||||
painter: _ProgressBarPainter(
|
||||
value: controller.value,
|
||||
colors: widget.colors,
|
||||
barHeight: widget.barHeight,
|
||||
handleHeight: widget.handleHeight,
|
||||
drawShadow: widget.drawShadow,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressBarPainter extends CustomPainter {
|
||||
_ProgressBarPainter({
|
||||
required this.value,
|
||||
required this.colors,
|
||||
required this.barHeight,
|
||||
required this.handleHeight,
|
||||
required this.drawShadow,
|
||||
});
|
||||
|
||||
VideoPlayerValue value;
|
||||
ChewieProgressColors colors;
|
||||
|
||||
final double barHeight;
|
||||
final double handleHeight;
|
||||
final bool drawShadow;
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter painter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final baseOffset = size.height / 2 - barHeight / 2;
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromPoints(
|
||||
Offset(0.0, baseOffset),
|
||||
Offset(size.width, baseOffset + barHeight),
|
||||
),
|
||||
const Radius.circular(4.0),
|
||||
),
|
||||
colors.backgroundPaint,
|
||||
);
|
||||
if (!value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
final double playedPartPercent =
|
||||
value.position.inMilliseconds / value.duration.inMilliseconds;
|
||||
final double playedPart =
|
||||
playedPartPercent > 1 ? size.width : playedPartPercent * size.width;
|
||||
for (final DurationRange range in value.buffered) {
|
||||
final double start = range.startFraction(value.duration) * size.width;
|
||||
final double end = range.endFraction(value.duration) * size.width;
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromPoints(
|
||||
Offset(start, baseOffset),
|
||||
Offset(end, baseOffset + barHeight),
|
||||
),
|
||||
const Radius.circular(4.0),
|
||||
),
|
||||
colors.bufferedPaint,
|
||||
);
|
||||
}
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromPoints(
|
||||
Offset(0.0, baseOffset),
|
||||
Offset(playedPart, baseOffset + barHeight),
|
||||
),
|
||||
const Radius.circular(4.0),
|
||||
),
|
||||
colors.playedPaint,
|
||||
);
|
||||
|
||||
if (drawShadow) {
|
||||
final Path shadowPath = Path()
|
||||
..addOval(
|
||||
Rect.fromCircle(
|
||||
center: Offset(playedPart, baseOffset + barHeight / 2),
|
||||
radius: handleHeight,
|
||||
),
|
||||
);
|
||||
|
||||
canvas.drawShadow(shadowPath, Colors.black, 0.2, false);
|
||||
}
|
||||
|
||||
canvas.drawCircle(
|
||||
Offset(playedPart, baseOffset + barHeight / 2),
|
||||
handleHeight,
|
||||
colors.handlePaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/utils/file/webview/customer_pdf.dart
Executable file
89
lib/utils/file/webview/customer_pdf.dart
Executable file
@@ -0,0 +1,89 @@
|
||||
// import 'dart:async';
|
||||
//
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_cached_pdfview/flutter_cached_pdfview.dart';
|
||||
//
|
||||
// class CustomerPDFPage extends StatefulWidget {
|
||||
// final String filePath;
|
||||
// const CustomerPDFPage({
|
||||
// super.key,
|
||||
// required this.filePath,
|
||||
// });
|
||||
//
|
||||
// @override
|
||||
// State<CustomerPDFPage> createState() => _CustomerPDFPageState();
|
||||
// }
|
||||
//
|
||||
// class _CustomerPDFPageState extends State<CustomerPDFPage> {
|
||||
// final StreamController<String> _pageCountController =
|
||||
// StreamController<String>();
|
||||
// final Completer<PDFViewController> _pdfViewController =
|
||||
// Completer<PDFViewController>();
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// body: SafeArea(
|
||||
// child: Stack(
|
||||
// children: [
|
||||
// PDF(
|
||||
// onPageChanged: (int? current, int? total) =>
|
||||
// _pageCountController.add('${current! + 1} - $total'),
|
||||
// onViewCreated: (PDFViewController pdfViewController) async {
|
||||
// _pdfViewController.complete(pdfViewController);
|
||||
// final int currentPage =
|
||||
// await pdfViewController.getCurrentPage() ?? 0;
|
||||
// final int? pageCount = await pdfViewController.getPageCount();
|
||||
// _pageCountController.add('${currentPage + 1} - $pageCount');
|
||||
// },
|
||||
// ).cachedFromUrl(
|
||||
// widget.filePath,
|
||||
// placeholder: (progress) => Center(child: Text('$progress %')),
|
||||
// errorWidget: (error) => Center(child: Text(error.toString())),
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// GestureDetector(
|
||||
// onTap: () => Navigator.pop(context),
|
||||
// child: Container(
|
||||
// margin: const EdgeInsets.only(left: 16, top: 16),
|
||||
// width: 44,
|
||||
// height: 44,
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(22),
|
||||
// color: Colors.grey.withOpacity(0.3),
|
||||
// ),
|
||||
// child: const Icon(
|
||||
// Icons.navigate_before,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Container(
|
||||
// margin: const EdgeInsets.only(right: 16, top: 16),
|
||||
// width: 88,
|
||||
// height: 44,
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(22),
|
||||
// color: Colors.grey.withOpacity(0.3),
|
||||
// ),
|
||||
// child: StreamBuilder<String>(
|
||||
// stream: _pageCountController.stream,
|
||||
// builder: (_, AsyncSnapshot<String> snapshot) {
|
||||
// if (snapshot.hasData) {
|
||||
// return Center(
|
||||
// child: Text(snapshot.data!),
|
||||
// );
|
||||
// }
|
||||
// return const SizedBox();
|
||||
// }),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
149
lib/utils/file/webview/customer_webview.dart
Executable file
149
lib/utils/file/webview/customer_webview.dart
Executable file
@@ -0,0 +1,149 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
|
||||
class CustomerWebView extends StatefulWidget {
|
||||
final String url;
|
||||
final bool? hideBack;
|
||||
|
||||
const CustomerWebView({
|
||||
super.key,
|
||||
required this.url,
|
||||
this.hideBack,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomerWebView> createState() => _CustomerWebViewState();
|
||||
}
|
||||
|
||||
class _CustomerWebViewState extends State<CustomerWebView> {
|
||||
final GlobalKey webViewKey = GlobalKey();
|
||||
|
||||
InAppWebViewController? webViewController;
|
||||
InAppWebViewSettings settings = InAppWebViewSettings(
|
||||
isInspectable: kDebugMode,
|
||||
mediaPlaybackRequiresUserGesture: false,
|
||||
allowsInlineMediaPlayback: true,
|
||||
iframeAllow: "camera; microphone",
|
||||
iframeAllowFullscreen: true);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
webViewController?.loadUrl(urlRequest: URLRequest(url: WebUri(widget.url)));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: [SystemUiOverlay.bottom]);
|
||||
return Stack(
|
||||
alignment: Alignment.topLeft,
|
||||
children: [
|
||||
InAppWebView(
|
||||
key: webViewKey,
|
||||
// webViewEnvironment: webViewEnvironment,
|
||||
initialUrlRequest: URLRequest(url: WebUri(widget.url)),
|
||||
// initialUrlRequest:
|
||||
// URLRequest(url: WebUri(Uri.base.toString().replaceFirst("/#/", "/") + 'page.html')),
|
||||
// initialFile: "assets/index.html",
|
||||
initialUserScripts: UnmodifiableListView<UserScript>([]),
|
||||
initialSettings: settings,
|
||||
// contextMenu: contextMenu,
|
||||
// pullToRefreshController: pullToRefreshController,
|
||||
onWebViewCreated: (controller) async {
|
||||
webViewController = controller;
|
||||
},
|
||||
onLoadStart: (controller, url) {
|
||||
setState(() {
|
||||
// this.url = url.toString();
|
||||
// urlController.text = this.url;
|
||||
});
|
||||
},
|
||||
// onPermissionRequest: (controller, request) {
|
||||
// return PermissionResponse(
|
||||
// resources: request.resources,
|
||||
// action: PermissionResponseAction.GRANT);
|
||||
// },
|
||||
// shouldOverrideUrlLoading:
|
||||
// (controller, navigationAction) async {
|
||||
// var uri = navigationAction.request.url!;
|
||||
//
|
||||
// if (![
|
||||
// "http",
|
||||
// "https",
|
||||
// "file",
|
||||
// "chrome",
|
||||
// "data",
|
||||
// "javascript",
|
||||
// "about"
|
||||
// ].contains(uri.scheme)) {
|
||||
// if (await canLaunchUrl(uri)) {
|
||||
// // Launch the App
|
||||
// await launchUrl(
|
||||
// uri,
|
||||
// );
|
||||
// // and cancel the request
|
||||
// return NavigationActionPolicy.CANCEL;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return NavigationActionPolicy.ALLOW;
|
||||
// },
|
||||
// onLoadStop: (controller, url) {
|
||||
// pullToRefreshController?.endRefreshing();
|
||||
// setState(() {
|
||||
// this.url = url.toString();
|
||||
// urlController.text = this.url;
|
||||
// });
|
||||
// },
|
||||
// onReceivedError: (controller, request, error) {
|
||||
// pullToRefreshController?.endRefreshing();
|
||||
// },
|
||||
// onProgressChanged: (controller, progress) {
|
||||
// if (progress == 100) {
|
||||
// pullToRefreshController?.endRefreshing();
|
||||
// }
|
||||
// setState(() {
|
||||
// this.progress = progress / 100;
|
||||
// urlController.text = this.url;
|
||||
// });
|
||||
// },
|
||||
// onUpdateVisitedHistory: (controller, url, isReload) {
|
||||
// setState(() {
|
||||
// this.url = url.toString();
|
||||
// urlController.text = this.url;
|
||||
// });
|
||||
// },
|
||||
onConsoleMessage: (controller, consoleMessage) {
|
||||
print(consoleMessage);
|
||||
},
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.hideBack != true,
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 16, top: 16),
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.navigate_before,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
53
lib/utils/frosted_glass_effect_widget.dart
Normal file
53
lib/utils/frosted_glass_effect_widget.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class FrostedGlassEffectWidget extends StatelessWidget {
|
||||
final Widget? child;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final double? borderRadius;
|
||||
final Widget? backgroundChild;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const FrostedGlassEffectWidget({
|
||||
super.key,
|
||||
this.child,
|
||||
this.width,
|
||||
this.height,
|
||||
this.borderRadius,
|
||||
this.backgroundChild,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
// 背景内容(可以放图片或其他内容)
|
||||
backgroundChild ?? SizedBox(),
|
||||
// 毛玻璃效果
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 20.h),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10.0,
|
||||
sigmaY: 10.0,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 20.h),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
lib/utils/launch_utils.dart
Executable file
42
lib/utils/launch_utils.dart
Executable file
@@ -0,0 +1,42 @@
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
enum LaunchType { tel, sms, email, link }
|
||||
|
||||
const launchTypeValues = {
|
||||
LaunchType.tel: "tel:",
|
||||
LaunchType.sms: "sms:",
|
||||
LaunchType.email: "mailto:",
|
||||
LaunchType.link: ''
|
||||
};
|
||||
|
||||
class LaunchUtils {
|
||||
///自定义Launch方法
|
||||
static Future<bool> customLaunch({
|
||||
required String urlString,
|
||||
LaunchType launchType = LaunchType.link,
|
||||
bool enableJavaScript = false,
|
||||
bool enableDomStorage = false,
|
||||
Map<String, String> headers = const <String, String>{},
|
||||
LaunchMode mode = LaunchMode.externalApplication,
|
||||
String? webOnlyWindowName,
|
||||
}) {
|
||||
return launchUrl(
|
||||
Uri.parse('${launchTypeValues[launchType]}$urlString'),
|
||||
webViewConfiguration: WebViewConfiguration(
|
||||
enableJavaScript: enableJavaScript,
|
||||
enableDomStorage: enableDomStorage,
|
||||
headers: headers,
|
||||
),
|
||||
mode: mode,
|
||||
webOnlyWindowName: webOnlyWindowName,
|
||||
);
|
||||
}
|
||||
|
||||
///判断是否打开链接
|
||||
static Future<bool> customCanLaunch(
|
||||
String urlString, {
|
||||
LaunchType launchType = LaunchType.link,
|
||||
}) async {
|
||||
return canLaunchUrl(Uri.parse('${launchTypeValues[launchType]}$urlString'));
|
||||
}
|
||||
}
|
||||
96
lib/utils/pull_refresh_widget/pull_refresh_widget.dart
Normal file
96
lib/utils/pull_refresh_widget/pull_refresh_widget.dart
Normal 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('暂无相关数据'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
134
lib/utils/string_utils.dart
Executable file
134
lib/utils/string_utils.dart
Executable file
@@ -0,0 +1,134 @@
|
||||
// import 'package:intl/intl.dart';
|
||||
|
||||
extension StringUtils on String? {
|
||||
String get hidePhone => StringUtils._phoneString(phone: this);
|
||||
|
||||
String get hideUserName => StringUtils._userNameString(name: this);
|
||||
|
||||
bool get isNumeric => StringUtils._isNumeric(str: this);
|
||||
|
||||
String get toPrice => StringUtils._cutOutPrice(price: this);
|
||||
|
||||
String get toSplMoney => StringUtils._splNumberInsyo(str: this);
|
||||
|
||||
String get toThousandPrice => StringUtils._formatThousandPrice(price: this);
|
||||
|
||||
double get toDouble => StringUtils._strToDouble(str: this);
|
||||
|
||||
///隐藏手机号中间4位数
|
||||
static String _phoneString({String? phone}) {
|
||||
if (phone == null || phone == '') return '';
|
||||
return phone.replaceFirst(RegExp(r'\d{4}'), '****', 3);
|
||||
}
|
||||
|
||||
///隐藏姓名(只显示姓 隐藏名字)
|
||||
static String _userNameString({String? name}) {
|
||||
if (name == null || name == '') return '';
|
||||
return name.replaceAll(name.substring(1, name.length), '**');
|
||||
}
|
||||
|
||||
///判断是否是纯数字
|
||||
static bool _isNumeric({String? str}) {
|
||||
if (str == null) {
|
||||
return false;
|
||||
}
|
||||
return double.tryParse(str) != null;
|
||||
}
|
||||
|
||||
///将金额转换为小数点后两位
|
||||
static String _cutOutPrice({String? price}) {
|
||||
String cutOutPrice = '0.00';
|
||||
if (price != null && price != '') {
|
||||
var newPrice = double.tryParse(price);
|
||||
if (newPrice != null) {
|
||||
cutOutPrice = newPrice.toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
return cutOutPrice;
|
||||
}
|
||||
|
||||
///金额处理千分位金额
|
||||
static String _formatThousandPrice({String? price}) {
|
||||
String priceStr = price ?? '';
|
||||
try {
|
||||
num? money = num.tryParse(priceStr);
|
||||
int truncateMoney = money?.truncate() ?? 0;
|
||||
if (truncateMoney >= 1000) {
|
||||
// NumberFormat format = NumberFormat('0,000');
|
||||
return _formatNum(truncateMoney);
|
||||
} else {
|
||||
List<String> resultList = priceStr.split(".");
|
||||
if (resultList.isNotEmpty) {
|
||||
return priceStr.split(".").first;
|
||||
} else {
|
||||
return priceStr;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static String _formatNum(num, {point = 3}) {
|
||||
if (num != null) {
|
||||
String str = double.parse(num.toString()).toString();
|
||||
// 分开截取
|
||||
List<String> sub = str.split('.');
|
||||
// 处理值
|
||||
List val = List.from(sub[0].split(''));
|
||||
// 处理点
|
||||
List<String> points = List.from(sub[1].split(''));
|
||||
//处理分割符
|
||||
for (int index = 0, i = val.length - 1; i >= 0; index++, i--) {
|
||||
// 除以三没有余数、不等于零并且不等于1 就加个逗号
|
||||
if (index % 3 == 0 && index != 0 && i != 1) val[i] = val[i] + ',';
|
||||
}
|
||||
// 处理小数点
|
||||
for (int i = 0; i <= point - points.length; i++) {
|
||||
points.add('0');
|
||||
}
|
||||
//如果大于长度就截取
|
||||
if (points.length > point) {
|
||||
// 截取数组
|
||||
points = points.sublist(0, point);
|
||||
}
|
||||
// 判断是否有长度
|
||||
if (points.length > 0) {
|
||||
return '${val.join('')}.${points.join('')}';
|
||||
} else {
|
||||
return val.join('');
|
||||
}
|
||||
} else {
|
||||
return "0.0";
|
||||
}
|
||||
}
|
||||
|
||||
///将数字转换位千分位隔开
|
||||
static String _splNumberInsyo({String? str}) {
|
||||
if (str?.isEmpty == true) {
|
||||
return '';
|
||||
}
|
||||
// num amount = num.parse(str ?? '0');
|
||||
// final NumberFormat formatter = NumberFormat.currency(
|
||||
// locale: 'en_US', // 设置为你的本地化Locale
|
||||
// decimalDigits: 0,
|
||||
// name: '', // 设置货币符号,例如 'USD' 或 'EUR'
|
||||
// );
|
||||
// return formatter.format(amount);
|
||||
return str ?? '';
|
||||
}
|
||||
|
||||
///将字符串转为double
|
||||
static double _strToDouble({String? str}) {
|
||||
String newStr = str ?? '';
|
||||
try {
|
||||
double? doubleValue = double.tryParse(newStr);
|
||||
if (doubleValue != null) {
|
||||
return doubleValue;
|
||||
}
|
||||
return 0;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
220
lib/utils/toast_utils.dart
Executable file
220
lib/utils/toast_utils.dart
Executable file
@@ -0,0 +1,220 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/custom_dialog.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// import 'package:get/get.dart';
|
||||
|
||||
class ToastUtils {
|
||||
///提示框
|
||||
static showToast({
|
||||
String? msg,
|
||||
Color? backgroundColor,
|
||||
TextStyle? textStyle = const TextStyle(color: Colors.white),
|
||||
}) async {
|
||||
EasyLoading.showToast(msg ?? '');
|
||||
// _cancelToast();
|
||||
}
|
||||
|
||||
///加载框
|
||||
static showLoading({String? text}) async {
|
||||
await EasyLoading.show(
|
||||
// status: 'loading...',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
}
|
||||
|
||||
///成功弹窗提示
|
||||
static successToast({String? successText}) {
|
||||
EasyLoading.showError(
|
||||
successText ?? '成功',
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
cancelToast();
|
||||
}
|
||||
|
||||
///失败弹窗提示
|
||||
static errorToast({String? errorText}) {
|
||||
EasyLoading.showError(
|
||||
errorText ?? '错误',
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
cancelToast();
|
||||
}
|
||||
|
||||
///底部自适应高度弹窗
|
||||
///底部弹窗
|
||||
static showBottomSheet({
|
||||
required BuildContext context,
|
||||
bool isTime = false,
|
||||
double? height,
|
||||
Function? onConfirm,
|
||||
String? title,
|
||||
double? titleFontSize,
|
||||
double? leftIconSize,
|
||||
Widget? contentWidget,
|
||||
Widget? header,
|
||||
bool isShowConfirm = false,
|
||||
Color? barrierColor,
|
||||
EdgeInsetsGeometry? padding,
|
||||
}) {
|
||||
cancelToast();
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: MediaQuery.of(context).size.height / 2,
|
||||
margin: EdgeInsets.only(
|
||||
top: height == null
|
||||
? MediaQuery.of(context).size.height / 2
|
||||
: (MediaQuery.of(context).size.height - height),
|
||||
),
|
||||
padding: padding ?? const EdgeInsets.only(bottom: 40),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
header ??
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 5),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom:
|
||||
BorderSide(color: Color(0xffE1E1E1), width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 6, right: 10),
|
||||
color: Colors.transparent,
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
size: leftIconSize ?? 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
title ?? '头部',
|
||||
style: TextStyle(
|
||||
color: const Color(0xff333333),
|
||||
fontSize: titleFontSize ?? 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (isShowConfirm) {
|
||||
if (onConfirm != null) {
|
||||
onConfirm();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
right: 18,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
color: isShowConfirm
|
||||
? const Color(0xff4D6FD5)
|
||||
: Colors.transparent,
|
||||
fontSize: 16),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: contentWidget ?? const SizedBox())
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static cancelToast() {
|
||||
// 延时2秒
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
|
||||
///显示对话框
|
||||
static showAlterDialog({
|
||||
VoidCallback? confirmCallback,
|
||||
VoidCallback? cancelCallback,
|
||||
String? titleText,
|
||||
String? contentText,
|
||||
String? confirmText,
|
||||
TextStyle? confirmStyle,
|
||||
TextStyle? cancelStyle,
|
||||
}) {
|
||||
cancelToast();
|
||||
return Get.dialog(
|
||||
CustomDialog(
|
||||
title: titleText ?? '温馨提示',
|
||||
content: contentText ?? '您确定要退出当前登录吗?',
|
||||
cancelText: "取消",
|
||||
confirmText: "确定",
|
||||
cancelTextStyle: cancelStyle,
|
||||
confirmTextStyle: confirmStyle,
|
||||
cancelCall: () {
|
||||
Get.back();
|
||||
Future.delayed(const Duration(milliseconds: 50)).then((value) {
|
||||
if (cancelCallback != null) {
|
||||
cancelCallback();
|
||||
}
|
||||
});
|
||||
},
|
||||
confirmCall: () {
|
||||
Get.back();
|
||||
if (confirmCallback != null) {
|
||||
confirmCallback();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///错误信息弹窗
|
||||
static showExceptionToast({String? title, String? msg}) {
|
||||
_cancelToast();
|
||||
return Get.snackbar(
|
||||
title ?? '错误信息',
|
||||
msg ?? '错误信息内容',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
colorText: Colors.white,
|
||||
backgroundColor: Colors.red[800],
|
||||
margin: const EdgeInsets.only(bottom: 10, left: 10, right: 10),
|
||||
borderRadius: 4,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
|
||||
///取消弹窗
|
||||
static _cancelToast() {
|
||||
// Toast.dismissAllToast();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user