feat(delete):删除里面文件夹

This commit is contained in:
2025-10-23 10:47:53 +08:00
parent 35d26643ba
commit 55ab438c11
75 changed files with 2424 additions and 41 deletions

View File

@@ -0,0 +1,53 @@
import 'dart:io';
import 'package:permission_handler/permission_handler.dart';
class PermissionUtil{
/// 安卓权限
static List<Permission> androidPermissions = <Permission>[
// 在这里添加需要的权限
Permission.storage
];
/// ios权限
static List<Permission> iosPermissions = <Permission>[
// 在这里添加需要的权限
Permission.storage
];
static Future<Map<Permission, PermissionStatus>> requestAll() async {
if (Platform.isIOS) {
return await iosPermissions.request();
}
return await androidPermissions.request();
}
static Future<Map<Permission, PermissionStatus>> request(
Permission permission) async {
final List<Permission> permissions = <Permission>[permission];
return await permissions.request();
}
static bool isDenied(Map<Permission, PermissionStatus> result) {
var isDenied = false;
result.forEach((key, value) {
if (value == PermissionStatus.denied) {
isDenied = true;
return;
}
});
return isDenied;
}
/// 检查权限
static Future<bool> checkGranted(Permission permission) async {
PermissionStatus storageStatus = await permission.status;
if (storageStatus == PermissionStatus.granted) {
//已授权
return true;
} else {
//拒绝授权
return false;
}
}
}

155
lib/utils/custom_dialog.dart Executable file
View File

@@ -0,0 +1,155 @@
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
///@Author ouyangyan
///@Time 2023/3/1 13:43
///自定义对话框
class CustomDialog extends Dialog {
final String title; //标题
final String content; //内容
final String cancelText; //"取消" 按钮文字
final String confirmText; //"确定" 按钮文字
final VoidCallback cancelCall; //取消按钮回调
final VoidCallback confirmCall; //确定按钮回调
final TextStyle? cancelTextStyle; //"取消" 按钮文字
final TextStyle? confirmTextStyle; //"确定" 按钮文字
const CustomDialog({
required this.title,
required this.content,
required this.cancelText,
required this.confirmText,
required this.cancelCall,
required this.confirmCall,
this.cancelTextStyle,
this.confirmTextStyle,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 45),
child: Material(
type: MaterialType.transparency,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
decoration: const ShapeDecoration(
color: Color(0xffffffff),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 20),
child: Text(
title,
textWidthBasis: TextWidthBasis.parent,
style: const TextStyle(
color: Color(0xff222222),
fontWeight: FontWeight.w500,
fontSize: 15,
),
),
),
Container(
margin: const EdgeInsets.only(
top: 12,
bottom: 20,
left: 10,
right: 10,
),
child: Text(
content,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xff999999),
fontSize: 13,
),
overflow: TextOverflow.ellipsis,
maxLines: 5,
),
),
lineWidget(height: 0.5),
_bottomButtonGroupWidget(),
],
),
)
],
),
),
);
}
///底部按钮组
Widget _bottomButtonGroupWidget() {
return Row(
children: [
Expanded(
child: buttonWidget(
title: cancelText,
callback: cancelCall,
textStyle: cancelTextStyle ??
const TextStyle(
color: Color(0xff888888),
fontWeight: FontWeight.w500,
),
),
),
lineWidget(width: 0.5, height: 50),
Expanded(
child: buttonWidget(
title: confirmText,
callback: confirmCall,
textStyle: confirmTextStyle ??
const TextStyle(
color: Color(0xffFF4D1A),
fontWeight: FontWeight.w500,
),
),
),
],
);
}
///按钮共用
Widget buttonWidget({
required String title,
required TextStyle textStyle,
required VoidCallback callback,
}) {
return GestureDetector(
onTap: callback,
behavior: HitTestBehavior.opaque,
child: Container(
height: 50,
width: double.infinity,
alignment: Alignment.center,
child: Text(title, style: textStyle),
),
);
}
///线
Widget lineWidget({
double? width,
double? height,
EdgeInsetsGeometry? margin,
}) {
return Container(
width: width,
color: const Color(0xffe9e9e9),
height: height,
margin: margin,
);
}
}

250
lib/utils/customer.dart Normal file
View File

@@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
// import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import 'package:transparent_image/transparent_image.dart';
///字体样式
class CustomerTextStyle extends TextStyle {
final Color? customerColor;
final double? customerFontSize;
final FontWeight? customerFontWeight;
final TextDecoration? customerDecoration;
final Color? customerDecorationColor;
const CustomerTextStyle({
this.customerFontSize,
this.customerDecorationColor,
this.customerFontWeight,
this.customerDecoration,
this.customerColor,
}) : super(
color: customerColor ?? const Color(0xff333333),
fontSize: customerFontSize,
fontWeight: customerFontWeight,
decoration: customerDecoration,
decorationColor: customerDecorationColor,
);
}
///基础 Container
class CustomerContainer extends StatelessWidget {
final Color? color;
final double? borderRadius;
final Widget? child;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final double? height;
final Gradient? gradient;
final DecorationImage? image;
final VoidCallback? onTap;
final List<BoxShadow>? boxShadow;
const CustomerContainer({
super.key,
this.color,
this.borderRadius,
this.child,
this.padding,
this.margin,
this.height,
this.gradient,
this.image,
this.onTap,
this.boxShadow,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: height,
margin: margin,
padding: padding,
decoration: BoxDecoration(
color: color ?? Colors.white,
borderRadius: BorderRadius.circular(borderRadius?.h ?? 10.h),
gradient: gradient,
image: image,
boxShadow: boxShadow,
),
child: child,
),
);
}
}
///图片加载
class CustomerImagesNetworking extends StatelessWidget {
final String imageUrl;
final double? width;
final double? height;
final BoxFit? fit;
final Widget? errorWidget;
const CustomerImagesNetworking({
super.key,
required this.imageUrl,
this.width,
this.height,
this.fit,
this.errorWidget,
});
@override
Widget build(BuildContext context) {
return Image.network(
key: Key(imageUrl),
imageUrl,
width: width,
height: height,
fit: fit,
errorBuilder: (_, object, s) {
return Container(
width: width,
height: height,
padding: EdgeInsets.all((width ?? 0) / 2),
child: Center(
child: Image.asset(
'assets/images/noContainer.png',
// width: width /2,
// height: width /2,
fit: fit ?? BoxFit.contain,
),
),
);
},
);
return imageUrl.contains('http') == true
? FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
width: width,
height: height,
image: imageUrl,
fit: fit,
placeholderErrorBuilder:
(_, Object object, StackTrace? stackTrace) {
return errorWidget ?? const SizedBox();
},
imageErrorBuilder: (_, Object object, StackTrace? stackTrace) {
return errorWidget ?? const SizedBox();
},
)
: errorWidget ?? const SizedBox();
}
}
///money字体样式
class CustomerMoneyText extends StatelessWidget {
final String money;
final double? moneyFontSize;
final Color? moneyColor;
final FontWeight? moneyFontWeight;
final String? unit;
final double? unitFontSize;
final Color? unitColor;
final FontWeight? unitFontWeight;
final String? rightUnit;
final double? rightUnitFontSize;
final Color? rightUnitColor;
final FontWeight? rightUnitFontWeight;
const CustomerMoneyText({
super.key,
required this.money,
this.moneyFontSize,
this.moneyColor,
this.moneyFontWeight,
this.unit,
this.unitFontSize,
this.unitColor,
this.unitFontWeight,
this.rightUnit,
this.rightUnitFontSize,
this.rightUnitColor,
this.rightUnitFontWeight,
});
@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
children: [
TextSpan(
text: unit ?? '',
style: CustomerTextStyle(
customerColor: unitColor ?? const Color(0xff333333),
customerFontSize: unitFontSize ?? 12,
customerFontWeight: unitFontWeight ?? FontWeight.bold,
),
),
TextSpan(
text: money,
style: CustomerTextStyle(
customerColor: moneyColor ?? const Color(0xff333333),
customerFontSize: moneyFontSize ?? 20,
customerFontWeight: moneyFontWeight ?? FontWeight.bold,
),
),
TextSpan(
text: rightUnit ?? '',
style: CustomerTextStyle(
customerColor: rightUnitColor ?? const Color(0xff333333),
customerFontSize: rightUnitFontSize ?? 12,
customerFontWeight: rightUnitFontWeight ?? FontWeight.bold,
),
),
],
),
);
}
}
///Html widget
class CustomerHtmlWidget extends StatelessWidget {
final String html;
final Function? onTap;
const CustomerHtmlWidget({
super.key,
required this.html,
this.onTap,
});
@override
Widget build(BuildContext context) {
return HtmlWidget(html);
// return Html(
// data: html,
// extensions: [
// TagExtension(
// tagsToExtend: {"flutter"},
// child: const FlutterLogo(),
// ),
// ],
// style: {
// "p.fancy": Style(
// textAlign: TextAlign.center,
// // padding: EdgeInsets.all(),
// backgroundColor: Colors.grey,
// margin: Margins(left: Margin(50, Unit.px), right: Margin.auto()),
// width: Width(300, Unit.px),
// fontWeight: FontWeight.bold,
// ),
// },
// );
// return Html(
// data: html,
// style: const TextStyle(
// letterSpacing: 1.5,
// ),
// onTapUrl: (url) {
// return onTap?.call(url);
// },
// );
}
}

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/customer.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
///上下结构的文字
///title、unit上
///content、unit下
class CustomerTitleAndContentWidget extends StatelessWidget {
final String title;
final FontWeight? titleFontWeight;
final double? titleFontSize;
final Color? titleColor;
final String? unitTitle;
final FontWeight? unitTitleFontWeight;
final double? unitTitleFontSize;
final Color? unitTitleColor;
final String content;
final FontWeight? contentFontWeight;
final double? contentFontSize;
final Color? contentColor;
final String? unitContent;
final FontWeight? unitContentFontWeight;
final double? unitContentFontSize;
final Color? unitContentColor;
final CrossAxisAlignment? crossAxisAlignment;
final MainAxisAlignment? mainAxisAlignment;
final double? spaceHeight;
const CustomerTitleAndContentWidget({
super.key,
required this.title,
required this.content,
this.titleFontWeight,
this.titleColor,
this.titleFontSize,
this.contentFontWeight,
this.contentFontSize,
this.contentColor,
this.crossAxisAlignment,
this.mainAxisAlignment,
this.unitContent,
this.unitContentFontWeight,
this.unitContentFontSize,
this.unitContentColor,
this.spaceHeight = 10,
this.unitTitle,
this.unitTitleFontWeight,
this.unitTitleFontSize,
this.unitTitleColor,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: mainAxisAlignment ?? MainAxisAlignment.start,
crossAxisAlignment: crossAxisAlignment ?? CrossAxisAlignment.center,
children: [
RichText(
text: TextSpan(
children: [
TextSpan(
text: unitTitle,
style: CustomerTextStyle(
customerFontSize: unitTitleFontSize,
customerColor: unitTitleColor ?? const Color(0xff399FFF),
customerFontWeight: unitTitleFontWeight ?? FontWeight.normal,
),
),
TextSpan(
text: title,
style: CustomerTextStyle(
customerFontSize: titleFontSize,
customerColor: titleColor ?? const Color(0xff1A1A1A),
customerFontWeight: titleFontWeight ?? FontWeight.normal,
),
),
],
),
),
SizedBox(height: spaceHeight?.h ?? 30.h),
RichText(
text: TextSpan(
children: [
TextSpan(
text: content,
style: TextStyle(
fontSize: contentFontSize,
color: contentColor ?? const Color(0xff399FFF),
fontWeight: contentFontWeight ?? FontWeight.normal,
),
),
TextSpan(
text: unitContent,
style: TextStyle(
fontSize: unitContentFontSize,
color: unitContentColor ?? const Color(0xff399FFF),
fontWeight: unitContentFontWeight ?? FontWeight.normal,
),
),
],
),
)
],
);
}
}

368
lib/utils/date_utils.dart Executable file
View File

@@ -0,0 +1,368 @@
import 'package:flustars_flutter3/flustars_flutter3.dart';
enum DateTimeUtilsType {
yearMonthDayHourMinuteSecond, // 2024-01-01 01:01:01
yearMonthDayHourMinute, // 2024-01-01 01:01
yearMonthDay, // 2024-01-01
monthDay, //01-01
monthDayPoint, //01.01
monthDayWord, //01月01日
yearMonthDayPoint, // 2024.01.01
yearMonthDayHourMinutePoint, // 2024.01.01
yearMonthDayHourMinuteSecondPoint, // 2024.01.01 01:01:01
yearMonthDayWord, // 2024年01月01日
yearMonthWord, // 2024年01月
yearMonth, // 2024-01
yearMonthPoint, // 2024.01
hourMinuteSecond, // 01:01:01
hourMinute, // 01:01
monthDayLine, // 01:01
monthWord, //01月
monthDayHourMinutePoint, //01月
}
///日期处理类
class DateTimeUtils {
static String dateTimeUtilsTool({
DateTimeUtilsType? dateTimeUtilsType,
String? dateTime,
String nullString = '-- --',
}) {
if (dateTime == null || dateTime == '') return nullString;
DateTime date = DateTime.parse(dateTime).toLocal();
switch (dateTimeUtilsType) {
case null:
return nullString;
case DateTimeUtilsType.yearMonthDayHourMinuteSecond:
return '${date.year.toString()}'
'-${date.month.toString().padLeft(2, '0')}'
'-${date.day.toString().padLeft(2, '0')}'
' ${date.hour.toString().padLeft(2, '0')}'
':${date.minute.toString().padLeft(2, '0')}'
':${date.second.toString().padLeft(2, '0')}';
case DateTimeUtilsType.yearMonthDayHourMinute:
return '${date.year.toString()}'
'-${date.month.toString().padLeft(2, '0')}'
'-${date.day.toString().padLeft(2, '0')}'
' ${date.hour.toString().padLeft(2, '0')}'
':${date.minute.toString().padLeft(2, '0')}';
case DateTimeUtilsType.yearMonthDay:
return '${date.year.toString()}'
'-${date.month.toString().padLeft(2, '0')}'
'-${date.day.toString().padLeft(2, '0')}';
case DateTimeUtilsType.monthDay:
return '${date.month.toString().padLeft(2, '0')}'
'-${date.day.toString().padLeft(2, '0')}';
case DateTimeUtilsType.monthDayPoint:
return '${date.month.toString().padLeft(2, '0')}'
'.${date.day.toString().padLeft(2, '0')}';
case DateTimeUtilsType.monthDayWord:
return '${date.month.toString().padLeft(2, '0')}'
'${date.day.toString().padLeft(2, '0')}';
case DateTimeUtilsType.yearMonthDayPoint:
return '${date.year.toString()}'
'.${date.month.toString().padLeft(2, '0')}'
'.${date.day.toString().padLeft(2, '0')}';
case DateTimeUtilsType.yearMonthDayHourMinuteSecondPoint:
return '${date.year.toString()}'
'.${date.month.toString().padLeft(2, '0')}'
'.${date.day.toString().padLeft(2, '0')}'
' ${date.hour.toString().padLeft(2, '0')}'
':${date.minute.toString().padLeft(2, '0')}'
':${date.second.toString().padLeft(2, '0')}';
case DateTimeUtilsType.yearMonthDayHourMinutePoint:
return '${date.year.toString()}'
'.${date.month.toString().padLeft(2, '0')}'
'.${date.day.toString().padLeft(2, '0')}'
' ${date.hour.toString().padLeft(2, '0')}'
':${date.minute.toString().padLeft(2, '0')}';
case DateTimeUtilsType.yearMonthDayWord:
return '${date.year.toString()}'
'${date.month.toString().padLeft(2, '0')}'
'${date.day.toString().padLeft(2, '0')}';
case DateTimeUtilsType.yearMonthWord:
return '${date.year.toString()}'
'${date.month.toString().padLeft(2, '0')}';
case DateTimeUtilsType.yearMonth:
return '${date.year.toString()}'
'-${date.month.toString().padLeft(2, '0')}';
case DateTimeUtilsType.yearMonthPoint:
return '${date.year.toString()}'
'.${date.month.toString().padLeft(2, '0')}';
case DateTimeUtilsType.hourMinuteSecond:
return '${date.hour.toString().padLeft(2, '0')}'
':${date.minute.toString().padLeft(2, '0')}'
':${date.second.toString().padLeft(2, '0')}';
case DateTimeUtilsType.hourMinute:
return '${date.hour.toString().padLeft(2, '0')}'
':${date.minute.toString().padLeft(2, '0')}';
case DateTimeUtilsType.monthDayLine:
return '${date.month.toString().padLeft(2, '0')}'
'/${date.day.toString().padLeft(2, '0')}';
case DateTimeUtilsType.monthWord:
return '${date.month.toString().padLeft(2, '0')}';
case DateTimeUtilsType.monthDayHourMinutePoint:
return '${date.month.toString().padLeft(2, '0')}'
'.${date.day.toString().padLeft(2, '0')}'
' ${date.hour.toString().padLeft(2, '0')}'
':${date.minute.toString().padLeft(2, '0')}';
}
}
/// 计算两个日期相差多少天?
static int differenceTwoTimes({
String? startTime,
String? endTime,
}) {
var startDate =
startTime == null ? DateTime.now() : DateTime.parse(startTime);
var endDate = endTime == null ? DateTime.now() : DateTime.parse(endTime);
var days = endDate.difference(startDate).inDays;
return days;
}
/// 计算两个日期相差inMinutes
static int differenceTwoInMinutesTimes({
String? startTime,
String? endTime,
}) {
var startDate =
startTime == null ? DateTime.now() : DateTime.parse(startTime);
var endDate = endTime == null ? DateTime.now() : DateTime.parse(endTime);
var days = endDate.difference(startDate).inMinutes;
return days;
}
/// 计算两个日期相差day huor min
static String differenceTwoDayHourTimes({
String? startTime,
String? endTime,
}) {
String xxxxx = '0';
var startDate =
startTime == null ? DateTime.now() : DateTime.parse(startTime);
var endDate = endTime == null ? DateTime.now() : DateTime.parse(endTime);
var days = endDate.difference(startDate).inDays;
if(days == 0){
//days == 0 相当
var hours = endDate.difference(startDate).inHours;
if(hours == 0){
var minutes = endDate.difference(startDate).inMinutes;
xxxxx = '$minutes分';
}else{
// xxxxx = hours;
var minutes = endDate.difference(startDate).inMinutes;
xxxxx = '${minutes ~/ 60}小时${minutes % 60}';
}
}else{
// xxxxx = days;
var hours = endDate.difference(startDate).inHours;
int divisor = 24; // 除数
int quotient = hours ~/ divisor; // 取整除法,得到商
int remainder = hours % divisor; // 取余数,得到余数
//
// print('商: $quotient');
// print('余数: $remainder');
xxxxx = '$quotient天$remainder小时';
}
return xxxxx;
}
///获取当前月份
static String getCurrentMonth() {
DateTime date = DateTime.now();
return date.month.toString().padLeft(2, '0');
}
///获取当前的年月日
static String getCurrentYMD() {
DateTime date = DateTime.now();
return '${date.year.toString()}'
'-${date.month.toString().padLeft(2, '0')}'
'-${date.day.toString().padLeft(2, '0')}';
}
///判断时间是否在某个时间区间内
static bool isTimeInRange({
required DateTime startTime,
required DateTime endTime,
required DateTime dateTime,
}) {
return dateTime.isAfter(startTime) && dateTime.isBefore(endTime);
}
///获取当月份
static String get getMonth {
return DateTime.now().month.toString();
}
///获取当年份
static String get getYear {
return DateTime.now().year.toString();
}
///获取当年份
static String getGMTString() {
return DateTime.now().year.toString();
}
///获取当前属于第几周
static String getWeekDay(DateTime dateTime) {
List<String> weekday = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
return weekday[dateTime.weekday - 1];
}
///根据日期获取某月的第一天和最后一天
static Map<String, dynamic> getMonthStartAndMonthEnd(
{required DateTime dateTime}) {
DateTime monthStart = DateTime(
dateTime.year,
dateTime.month,
1,
); // 获取本月第一天的日期时间
DateTime monthEnd = DateTime(
dateTime.year,
dateTime.month + 1,
0,
23,
59,
59,
); // 获取本月最后一天的日期时间时间为23:59:59
return {
'monthStart': monthStart,
'monthEnd': monthEnd,
};
}
//
// DateTime monthStart = DateTime(
// int.parse(time.toString().split('-').first),
// int.parse(time.toString().split('-').last),
// 1); // 获取本月第一天的日期时间
// DateTime monthEnd = DateTime(
// int.parse(time.toString().split('-').first),
// int.parse(time.toString().split('-').last) + 1,
// 0,
// 23,
// 59,
// 59); // 获取本月最后一天的日期时间时间为23:59:59
/// /
}
class TimeMachineUtil {
/// 获取某一年的第一个月的第一天和最后一个月的最后一天
static getStartEndYearDate(int iYear) {
Map mapDate = {};
int yearNow = DateTime.now().year;
yearNow = yearNow + iYear;
String newStartYear = '$yearNow-01-01';
String newEndtYear = '${yearNow + 1}-01-00';
mapDate['startTime'] = DateUtil.formatDate(
DateTime.fromMillisecondsSinceEpoch(turnTimestamp(newStartYear)),
format: 'yyyy-MM-dd');
mapDate['endTime'] = DateUtil.formatDate(
DateTime.fromMillisecondsSinceEpoch(turnTimestamp(newEndtYear)),
format: 'yyyy-MM-dd');
mapDate['startStamp'] = turnTimestamp(mapDate['startTime'] + ' 00:00:00');
mapDate['endStamp'] = turnTimestamp(mapDate['endTime'] + ' 23:59:59');
print('某一年初和年末:$mapDate');
}
/// 获得当前日期 未来/过去 第某个月第一天和最后一天时间
static Map<String, String> getMonthDate(int iMonth) {
//获取当前日期
var currentDate = DateTime.now();
if (iMonth + currentDate.month > 0) {
return timeConversion(
iMonth + currentDate.month, (currentDate.year).toString());
} else {
int beforeYear = (iMonth + currentDate.month) ~/ 12;
String yearNew = (currentDate.year + beforeYear - 1).toString();
int monthNew = (iMonth + currentDate.month) - beforeYear * 12;
return timeConversion(12 + monthNew, yearNew);
}
}
static Map<String, String> timeConversion(int monthTime, String yearTime) {
Map<String, String> dateMap = {};
dateMap['startDate'] =
'$yearTime-${monthTime < 10 ? '0$monthTime' : '$monthTime'}-01';
//转时间戳再转时间格式 防止出错
dateMap['startDate'] = DateUtil.formatDate(
DateTime.fromMillisecondsSinceEpoch(
turnTimestamp(dateMap['startDate'] ?? "")),
format: 'yyyy-MM-dd');
//某个月结束时间,转时间戳再转
String endMonth =
'$yearTime-${(monthTime + 1) < 10 ? '0${monthTime + 1}' : (monthTime + 1)}-00';
var endMonthTimeStamp = turnTimestamp(endMonth);
endMonth = DateUtil.formatDate(
DateTime.fromMillisecondsSinceEpoch(endMonthTimeStamp),
format: 'yyyy-MM-dd');
dateMap['endDate'] = endMonth;
//这里是为了公司后台接口 需加时间段的时间戳 但不显示在格式化实践中
dateMap['startDateStamp'] =
turnTimestamp('${dateMap['startDate']} 00:00:00').toString();
dateMap['endDateStamp'] =
turnTimestamp('${dateMap['endDate']} 23:59:59').toString();
// print('过去未来某个月初月末:$dateMap');
return dateMap;
}
/// 转时间戳
static int turnTimestamp(String timestamp) {
return DateTime.parse(timestamp).millisecondsSinceEpoch;
}
/// 当前时间 过去/未来 某个周的周一和周日
static Map<String, String> getWeeksDate(int weeks) {
Map<String, String> mapTime = {};
DateTime now = DateTime.now();
int weekday = now.weekday; //今天周几
var sunDay = getTimestampLatest(false, 7 - weekday + weeks * 7); //周末
var monDay = getTimestampLatest(true, -weekday + 1 + weeks * 7); //周一
mapTime['monDay'] = DateUtil.formatDate(
DateTime.fromMillisecondsSinceEpoch(monDay),
format: 'yyyy-MM-dd'); //周一 时间格式化
mapTime['sunDay'] = DateUtil.formatDate(
DateTime.fromMillisecondsSinceEpoch(sunDay),
format: 'yyyy-MM-dd'); //周一 时间格式化
mapTime['monDayStamp'] = '$monDay'; //周一 时间戳
mapTime['sunDayStamp'] = '$sunDay'; //周日 时间戳
// print('某个周的周一和周日:$mapTime');
return mapTime;
}
/// phase : 是零点还是23:59:59
static int getTimestampLatest(bool phase, int day) {
String newHours;
DateTime now = DateTime.now();
DateTime sixtyDaysFromNow = now.add(Duration(days: day));
String formattedDate =
DateUtil.formatDate(sixtyDaysFromNow, format: 'yyyy-MM-dd');
if (phase) {
newHours = '$formattedDate 00:00:00';
} else {
newHours = '$formattedDate 23:59:59';
}
DateTime newDate = DateTime.parse(newHours);
// String newFormattedDate =
// DateUtil.formatDate(newDate, format: 'yyyy-MM-dd HH:mm:ss');
int timeStamp = newDate.millisecondsSinceEpoch;
// print('时间' + newFormattedDate);
return timeStamp;
}
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
///弹窗widget
///ctx showDialog context
///padding 位置
class ShowDialogWidget extends StatelessWidget {
final BuildContext? ctx;
final Widget child;
final EdgeInsetsGeometry padding;
final ScrollController? scrollController;
const ShowDialogWidget({
super.key,
this.ctx,
required this.child,
required this.padding,
this.scrollController,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: ctx == null ? null : () => Navigator.pop(ctx!),
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.transparent,
),
),
Center(
child: Padding(
padding: padding,
child: SingleChildScrollView(
controller: scrollController,
child: child,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/toast_utils.dart';
import 'play/video_play_page.dart';
enum OpenType {
file, //文件
image, //图片
video, //视频
url, //网址
}
class CustomerFile {
static Future<void> openTypeFile({
required OpenType type,
required BuildContext context,
required int isLock,
required String filePath,
String? fileName,
}) async {
if (filePath == "" || filePath == null) {
ToastUtils.showToast(msg: "暂无内容");
} else {
if (type == OpenType.file) {
CustomerAction.openFileAction(
context: context,
filePath: filePath,
fileName: fileName,
);
}
if (type == OpenType.video) {
CustomerAction.openVideoAction(
context: context,
videoUrl: filePath,
);
}
}
}
}
///公共方法
class CustomerAction {
///打开文件
static Future<void> openFileAction({
required BuildContext context,
required String filePath,
String? fileName,
}) async {
if (filePath == "") {
ToastUtils.showToast(msg: "暂无文件");
} else {
if (filePath.contains("http")) {
// showDialog(
// context: context,
// builder: (BuildContext ctx) {
//
// // return CustomerPDFPage(
// // filePath: filePath,
// // );
// });
} else {
ToastUtils.showToast(msg: "地址无效");
}
}
}
static _dealFileName(String filePath) {
return "${DateTime.now()}.${filePath.split(".").last}";
}
///打开视频
static Future<void> openVideoAction({
required BuildContext context,
required String videoUrl,
}) async {
showDialog(
context: context,
builder: (BuildContext ctx) {
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: VideoPlayPage(
videoUrl: videoUrl,
),
);
});
}
///打开网页
// static Future<void> openWebview({
// required BuildContext context,
// required String url,
// }) async {
// showDialog(
// context: context,
// builder: (BuildContext ctx) {
// return CustomerWebView(url: url);
// },
// );
// }
static Future<void> openTypeFile({
required OpenType type,
required BuildContext context,
required int isLock,
required String filePath,
String? fileName,
}) async {
if (filePath == "" || filePath == null) {
ToastUtils.showToast(msg: "暂无内容");
} else {
if (type == OpenType.file) {
CustomerAction.openFileAction(
context: context,
filePath: filePath,
fileName: fileName,
);
}
if (type == OpenType.video) {
CustomerAction.openVideoAction(
context: context,
videoUrl: filePath,
);
}
}
}
}

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/file/video/lib/chewie.dart';
import 'package:video_player/video_player.dart';
class VideoPlayPage extends StatefulWidget {
final String? videoUrl;
const VideoPlayPage({super.key, this.videoUrl});
@override
State<VideoPlayPage> createState() => _VideoPlayPageState();
}
class _VideoPlayPageState extends State<VideoPlayPage> {
var videoController = VideoPlayerController.network("");
var chewieController;
//监听视频是否在播放
bool isPlaying = false;
//视频地址 https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4
// String videoUrl =
// "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4";
// String videoUrl =
// "http://static.dongmenshijing.com/upload/20230720/cf742bbf-8a96-46d5-b187-edeffa168556.mp4";
@override
void initState() {
// Uri url = Uri.parse(videoUrl);
// videoController = VideoPlayerController.networkUrl(url)
// ..initialize().then((value) {
// setState(() {});
// });
// videoController = VideoPlayerController.network(
// videoUrl,
// videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
// )..initialize().then((value) {
// setState(() {});
// });
// videoController.addListener(() {
// setState(() {});
// });
initVideoController();
super.initState();
}
///初始化
initVideoController({String? url}) {
videoController = VideoPlayerController.network(
widget.videoUrl ?? "",
)..initialize().then((_) {
setState(() {
chewieController = ChewieController(
// autoInitialize: true,
fullScreenByDefault: true,
videoPlayerController: videoController,
allowFullScreen: true,
// aspectRatio: MediaQuery.of(context).size.height /
// MediaQuery.of(context).size.width,
autoPlay: true,
looping: false,
);
});
});
videoController.addListener(() {
setState(() {});
});
setState(() {});
}
@override
void dispose() {
videoController.dispose();
chewieController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return chewieController == null
? Container()
: Chewie(
controller: chewieController,
);
}
}

View File

@@ -0,0 +1,8 @@
library chewie;
export 'src/chewie_player.dart';
export 'src/chewie_progress_colors.dart';
export 'src/cupertino/cupertino_controls.dart';
export 'src/material/material_controls.dart';
export 'src/material/material_desktop_controls.dart';
export 'src/models/index.dart';

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
/// A widget that animates implicitly between a play and a pause icon.
class AnimatedPlayPause extends StatefulWidget {
const AnimatedPlayPause({
Key? key,
required this.playing,
this.size,
this.color,
}) : super(key: key);
final double? size;
final bool playing;
final Color? color;
@override
State<StatefulWidget> createState() => AnimatedPlayPauseState();
}
class AnimatedPlayPauseState extends State<AnimatedPlayPause>
with SingleTickerProviderStateMixin {
late final animationController = AnimationController(
vsync: this,
value: widget.playing ? 1 : 0,
duration: const Duration(milliseconds: 400),
);
@override
void didUpdateWidget(AnimatedPlayPause oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.playing != oldWidget.playing) {
if (widget.playing) {
animationController.forward();
} else {
animationController.reverse();
}
}
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedIcon(
color: widget.color,
size: widget.size,
icon: AnimatedIcons.play_pause,
progress: animationController,
),
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'animated_play_pause.dart';
class CenterPlayButton extends StatelessWidget {
const CenterPlayButton({
Key? key,
required this.backgroundColor,
this.iconColor,
required this.show,
required this.isPlaying,
required this.isFinished,
this.onPressed,
}) : super(key: key);
final Color backgroundColor;
final Color? iconColor;
final bool show;
final bool isPlaying;
final bool isFinished;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.transparent,
child: Center(
child: UnconstrainedBox(
child: AnimatedOpacity(
opacity: show ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
shape: BoxShape.circle,
),
// Always set the iconSize on the IconButton, not on the Icon itself:
// https://github.com/flutter/flutter/issues/52980
child: IconButton(
iconSize: 32,
padding: const EdgeInsets.all(12.0),
icon: isFinished
? Icon(Icons.replay, color: iconColor)
: AnimatedPlayPause(
color: iconColor,
playing: isPlaying,
),
onPressed: onPressed,
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,633 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
// import 'package:wakelock/wakelock.dart';
import '../chewie.dart';
import 'notifiers/index.dart';
import 'player_with_controls.dart';
typedef ChewieRoutePageBuilder = Widget Function(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
ChewieControllerProvider controllerProvider,
);
/// A Video Player with Material and Cupertino skins.
///
/// `video_player` is pretty low level. Chewie wraps it in a friendly skin to
/// make it easy to use!
class Chewie extends StatefulWidget {
const Chewie({
Key? key,
required this.controller,
}) : super(key: key);
/// The [ChewieController]
final ChewieController controller;
@override
ChewieState createState() {
return ChewieState();
}
}
class ChewieState extends State<Chewie> {
bool _isFullScreen = false;
bool get isControllerFullScreen => widget.controller.isFullScreen;
late PlayerNotifier notifier;
@override
void initState() {
super.initState();
widget.controller.addListener(listener);
notifier = PlayerNotifier.init();
}
@override
void dispose() {
widget.controller.removeListener(listener);
super.dispose();
}
@override
void didUpdateWidget(Chewie oldWidget) {
if (oldWidget.controller != widget.controller) {
widget.controller.addListener(listener);
}
super.didUpdateWidget(oldWidget);
if (_isFullScreen != isControllerFullScreen) {
widget.controller._isFullScreen = _isFullScreen;
}
}
Future<void> listener() async {
if (isControllerFullScreen && !_isFullScreen) {
_isFullScreen = isControllerFullScreen;
await _pushFullScreenWidget(context);
} else if (_isFullScreen) {
Navigator.of(
context,
rootNavigator: widget.controller.useRootNavigator,
).pop();
_isFullScreen = false;
}
}
@override
Widget build(BuildContext context) {
return ChewieControllerProvider(
controller: widget.controller,
child: ChangeNotifierProvider<PlayerNotifier>.value(
value: notifier,
builder: (context, w) => const PlayerWithControls(),
),
);
}
Widget _buildFullScreenVideo(
BuildContext context,
Animation<double> animation,
ChewieControllerProvider controllerProvider,
) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Container(
alignment: Alignment.center,
color: Colors.black,
child: controllerProvider,
),
);
}
AnimatedWidget _defaultRoutePageBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
ChewieControllerProvider controllerProvider,
) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return _buildFullScreenVideo(context, animation, controllerProvider);
},
);
}
Widget _fullScreenRoutePageBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
final controllerProvider = ChewieControllerProvider(
controller: widget.controller,
child: ChangeNotifierProvider<PlayerNotifier>.value(
value: notifier,
builder: (context, w) => const PlayerWithControls(),
),
);
if (widget.controller.routePageBuilder == null) {
return _defaultRoutePageBuilder(
context,
animation,
secondaryAnimation,
controllerProvider,
);
}
return widget.controller.routePageBuilder!(
context,
animation,
secondaryAnimation,
controllerProvider,
);
}
Future<dynamic> _pushFullScreenWidget(BuildContext context) async {
final TransitionRoute<void> route = PageRouteBuilder<void>(
pageBuilder: _fullScreenRoutePageBuilder,
);
onEnterFullScreen();
if (!widget.controller.allowedScreenSleep) {
WakelockPlus.enable;
}
await Navigator.of(
context,
rootNavigator: widget.controller.useRootNavigator,
).push(route);
_isFullScreen = false;
widget.controller.exitFullScreen();
// The wakelock plugins checks whether it needs to perform an action internally,
// so we do not need to check Wakelock.isEnabled.
WakelockPlus.disable();
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: widget.controller.systemOverlaysAfterFullScreen,
);
SystemChrome.setPreferredOrientations(
widget.controller.deviceOrientationsAfterFullScreen,
);
}
void onEnterFullScreen() {
final videoWidth = widget.controller.videoPlayerController.value.size.width;
final videoHeight =
widget.controller.videoPlayerController.value.size.height;
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
// if (widget.controller.systemOverlaysOnEnterFullScreen != null) {
// /// Optional user preferred settings
// SystemChrome.setEnabledSystemUIMode(
// SystemUiMode.manual,
// overlays: widget.controller.systemOverlaysOnEnterFullScreen,
// );
// } else {
// /// Default behavior
// SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
// }
if (widget.controller.deviceOrientationsOnEnterFullScreen != null) {
/// Optional user preferred settings
SystemChrome.setPreferredOrientations(
widget.controller.deviceOrientationsOnEnterFullScreen!,
);
} else {
final isLandscapeVideo = videoWidth > videoHeight;
final isPortraitVideo = videoWidth < videoHeight;
/// Default behavior
/// Video w > h means we force landscape
if (isLandscapeVideo) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
/// Video h > w means we force portrait
else if (isPortraitVideo) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
}
/// Otherwise if h == w (square video)
else {
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
}
}
}
}
/// The ChewieController is used to configure and drive the Chewie Player
/// Widgets. It provides methods to control playback, such as [pause] and
/// [play], as well as methods that control the visual appearance of the player,
/// such as [enterFullScreen] or [exitFullScreen].
///
/// In addition, you can listen to the ChewieController for presentational
/// changes, such as entering and exiting full screen mode. To listen for
/// changes to the playback, such as a change to the seek position of the
/// player, please use the standard information provided by the
/// `VideoPlayerController`.
class ChewieController extends ChangeNotifier {
ChewieController({
required this.videoPlayerController,
this.optionsTranslation,
this.aspectRatio,
this.autoInitialize = false,
this.autoPlay = false,
this.startAt,
this.looping = false,
this.fullScreenByDefault = false,
this.cupertinoProgressColors,
this.materialProgressColors,
this.placeholder,
this.overlay,
this.showControlsOnInitialize = true,
this.showOptions = true,
this.optionsBuilder,
this.additionalOptions,
this.showControls = true,
this.transformationController,
this.zoomAndPan = false,
this.maxScale = 2.5,
this.subtitle,
this.subtitleBuilder,
this.customControls,
this.errorBuilder,
this.allowedScreenSleep = true,
this.isLive = false,
this.allowFullScreen = true,
this.allowMuting = true,
this.allowPlaybackSpeedChanging = true,
this.useRootNavigator = true,
this.playbackSpeeds = const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
this.systemOverlaysOnEnterFullScreen,
this.deviceOrientationsOnEnterFullScreen,
this.systemOverlaysAfterFullScreen = SystemUiOverlay.values,
this.deviceOrientationsAfterFullScreen = DeviceOrientation.values,
this.routePageBuilder,
this.progressIndicatorDelay,
this.hideControlsTimer = defaultHideControlsTimer,
}) : assert(
playbackSpeeds.every((speed) => speed > 0),
'The playbackSpeeds values must all be greater than 0',
) {
_initialize();
}
ChewieController copyWith({
VideoPlayerController? videoPlayerController,
OptionsTranslation? optionsTranslation,
double? aspectRatio,
bool? autoInitialize,
bool? autoPlay,
Duration? startAt,
bool? looping,
bool? fullScreenByDefault,
ChewieProgressColors? cupertinoProgressColors,
ChewieProgressColors? materialProgressColors,
Widget? placeholder,
Widget? overlay,
bool? showControlsOnInitialize,
bool? showOptions,
Future<void> Function(BuildContext, List<OptionItem>)? optionsBuilder,
List<OptionItem> Function(BuildContext)? additionalOptions,
bool? showControls,
TransformationController? transformationController,
bool? zoomAndPan,
double? maxScale,
Subtitles? subtitle,
Widget Function(BuildContext, dynamic)? subtitleBuilder,
Widget? customControls,
Widget Function(BuildContext, String)? errorBuilder,
bool? allowedScreenSleep,
bool? isLive,
bool? allowFullScreen,
bool? allowMuting,
bool? allowPlaybackSpeedChanging,
bool? useRootNavigator,
Duration? hideControlsTimer,
List<double>? playbackSpeeds,
List<SystemUiOverlay>? systemOverlaysOnEnterFullScreen,
List<DeviceOrientation>? deviceOrientationsOnEnterFullScreen,
List<SystemUiOverlay>? systemOverlaysAfterFullScreen,
List<DeviceOrientation>? deviceOrientationsAfterFullScreen,
Duration? progressIndicatorDelay,
Widget Function(
BuildContext,
Animation<double>,
Animation<double>,
ChewieControllerProvider,
)? routePageBuilder,
}) {
return ChewieController(
videoPlayerController:
videoPlayerController ?? this.videoPlayerController,
optionsTranslation: optionsTranslation ?? this.optionsTranslation,
aspectRatio: aspectRatio ?? this.aspectRatio,
autoInitialize: autoInitialize ?? this.autoInitialize,
autoPlay: autoPlay ?? this.autoPlay,
startAt: startAt ?? this.startAt,
looping: looping ?? this.looping,
fullScreenByDefault: fullScreenByDefault ?? this.fullScreenByDefault,
cupertinoProgressColors:
cupertinoProgressColors ?? this.cupertinoProgressColors,
materialProgressColors:
materialProgressColors ?? this.materialProgressColors,
placeholder: placeholder ?? this.placeholder,
overlay: overlay ?? this.overlay,
showControlsOnInitialize:
showControlsOnInitialize ?? this.showControlsOnInitialize,
showOptions: showOptions ?? this.showOptions,
optionsBuilder: optionsBuilder ?? this.optionsBuilder,
additionalOptions: additionalOptions ?? this.additionalOptions,
showControls: showControls ?? this.showControls,
subtitle: subtitle ?? this.subtitle,
subtitleBuilder: subtitleBuilder ?? this.subtitleBuilder,
customControls: customControls ?? this.customControls,
errorBuilder: errorBuilder ?? this.errorBuilder,
allowedScreenSleep: allowedScreenSleep ?? this.allowedScreenSleep,
isLive: isLive ?? this.isLive,
allowFullScreen: allowFullScreen ?? this.allowFullScreen,
allowMuting: allowMuting ?? this.allowMuting,
allowPlaybackSpeedChanging:
allowPlaybackSpeedChanging ?? this.allowPlaybackSpeedChanging,
useRootNavigator: useRootNavigator ?? this.useRootNavigator,
playbackSpeeds: playbackSpeeds ?? this.playbackSpeeds,
systemOverlaysOnEnterFullScreen: systemOverlaysOnEnterFullScreen ??
this.systemOverlaysOnEnterFullScreen,
deviceOrientationsOnEnterFullScreen:
deviceOrientationsOnEnterFullScreen ??
this.deviceOrientationsOnEnterFullScreen,
systemOverlaysAfterFullScreen:
systemOverlaysAfterFullScreen ?? this.systemOverlaysAfterFullScreen,
deviceOrientationsAfterFullScreen: deviceOrientationsAfterFullScreen ??
this.deviceOrientationsAfterFullScreen,
routePageBuilder: routePageBuilder ?? this.routePageBuilder,
hideControlsTimer: hideControlsTimer ?? this.hideControlsTimer,
progressIndicatorDelay:
progressIndicatorDelay ?? this.progressIndicatorDelay,
);
}
static const defaultHideControlsTimer = Duration(seconds: 3);
/// If false, the options button in MaterialUI and MaterialDesktopUI
/// won't be shown.
final bool showOptions;
/// Pass your translations for the options like:
/// - PlaybackSpeed
/// - Subtitles
/// - Cancel
///
/// Buttons
///
/// These are required for the default `OptionItem`'s
final OptionsTranslation? optionsTranslation;
/// Build your own options with default chewieOptions shiped through
/// the builder method. Just add your own options to the Widget
/// you'll build. If you want to hide the chewieOptions, just leave them
/// out from your Widget.
final Future<void> Function(
BuildContext context,
List<OptionItem> chewieOptions,
)? optionsBuilder;
/// Add your own additional options on top of chewie options
final List<OptionItem> Function(BuildContext context)? additionalOptions;
/// Define here your own Widget on how your n'th subtitle will look like
Widget Function(BuildContext context, dynamic subtitle)? subtitleBuilder;
/// Add a List of Subtitles here in `Subtitles.subtitle`
Subtitles? subtitle;
/// The controller for the video you want to play
final VideoPlayerController videoPlayerController;
/// Initialize the Video on Startup. This will prep the video for playback.
final bool autoInitialize;
/// Play the video as soon as it's displayed
final bool autoPlay;
/// Start video at a certain position
final Duration? startAt;
/// Whether or not the video should loop
final bool looping;
/// Wether or not to show the controls when initializing the widget.
final bool showControlsOnInitialize;
/// Whether or not to show the controls at all
final bool showControls;
/// Controller to pass into the [InteractiveViewer] component
final TransformationController? transformationController;
/// Whether or not to allow zooming and panning
final bool zoomAndPan;
/// Max scale when zooming
final double maxScale;
/// Defines customised controls. Check [MaterialControls] or
/// [CupertinoControls] for reference.
final Widget? customControls;
/// When the video playback runs into an error, you can build a custom
/// error message.
final Widget Function(BuildContext context, String errorMessage)?
errorBuilder;
/// The Aspect Ratio of the Video. Important to get the correct size of the
/// video!
///
/// Will fallback to fitting within the space allowed.
final double? aspectRatio;
/// The colors to use for controls on iOS. By default, the iOS player uses
/// colors sampled from the original iOS 11 designs.
final ChewieProgressColors? cupertinoProgressColors;
/// The colors to use for the Material Progress Bar. By default, the Material
/// player uses the colors from your Theme.
final ChewieProgressColors? materialProgressColors;
/// The placeholder is displayed underneath the Video before it is initialized
/// or played.
final Widget? placeholder;
/// A widget which is placed between the video and the controls
final Widget? overlay;
/// Defines if the player will start in fullscreen when play is pressed
final bool fullScreenByDefault;
/// Defines if the player will sleep in fullscreen or not
final bool allowedScreenSleep;
/// Defines if the controls should be shown for live stream video
final bool isLive;
/// Defines if the fullscreen control should be shown
final bool allowFullScreen;
/// Defines if the mute control should be shown
final bool allowMuting;
/// Defines if the playback speed control should be shown
final bool allowPlaybackSpeedChanging;
/// Defines if push/pop navigations use the rootNavigator
final bool useRootNavigator;
/// Defines the [Duration] before the video controls are hidden. By default, this is set to three seconds.
final Duration hideControlsTimer;
/// Defines the set of allowed playback speeds user can change
final List<double> playbackSpeeds;
/// Defines the system overlays visible on entering fullscreen
final List<SystemUiOverlay>? systemOverlaysOnEnterFullScreen;
/// Defines the set of allowed device orientations on entering fullscreen
final List<DeviceOrientation>? deviceOrientationsOnEnterFullScreen;
/// Defines the system overlays visible after exiting fullscreen
final List<SystemUiOverlay> systemOverlaysAfterFullScreen;
/// Defines the set of allowed device orientations after exiting fullscreen
final List<DeviceOrientation> deviceOrientationsAfterFullScreen;
/// Defines a custom RoutePageBuilder for the fullscreen
final ChewieRoutePageBuilder? routePageBuilder;
/// Defines a delay in milliseconds between entering buffering state and displaying the loading spinner. Set null (default) to disable it.
final Duration? progressIndicatorDelay;
static ChewieController of(BuildContext context) {
final chewieControllerProvider =
context.dependOnInheritedWidgetOfExactType<ChewieControllerProvider>()!;
return chewieControllerProvider.controller;
}
bool _isFullScreen = false;
bool get isFullScreen => _isFullScreen;
bool get isPlaying => videoPlayerController.value.isPlaying;
Future _initialize() async {
await videoPlayerController.setLooping(looping);
if ((autoInitialize || autoPlay) &&
!videoPlayerController.value.isInitialized) {
await videoPlayerController.initialize();
}
if (autoPlay) {
if (fullScreenByDefault) {
enterFullScreen();
}
await videoPlayerController.play();
}
if (startAt != null) {
await videoPlayerController.seekTo(startAt!);
}
if (fullScreenByDefault) {
videoPlayerController.addListener(_fullScreenListener);
}
}
Future<void> _fullScreenListener() async {
if (videoPlayerController.value.isPlaying && !_isFullScreen) {
enterFullScreen();
videoPlayerController.removeListener(_fullScreenListener);
}
}
void enterFullScreen() {
_isFullScreen = true;
notifyListeners();
}
void exitFullScreen() {
_isFullScreen = false;
SystemChrome.setPreferredOrientations([
// 强制竖屏
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown
]);
notifyListeners();
}
void toggleFullScreen() {
_isFullScreen = !_isFullScreen;
notifyListeners();
}
void togglePause() {
isPlaying ? pause() : play();
}
Future<void> play() async {
await videoPlayerController.play();
}
// ignore: avoid_positional_boolean_parameters
Future<void> setLooping(bool looping) async {
await videoPlayerController.setLooping(looping);
}
Future<void> pause() async {
await videoPlayerController.pause();
}
Future<void> seekTo(Duration moment) async {
await videoPlayerController.seekTo(moment);
}
Future<void> setVolume(double volume) async {
await videoPlayerController.setVolume(volume);
}
void setSubtitle(List<Subtitle> newSubtitle) {
subtitle = Subtitles(newSubtitle);
}
}
class ChewieControllerProvider extends InheritedWidget {
const ChewieControllerProvider({
Key? key,
required this.controller,
required Widget child,
}) : super(key: key, child: child);
final ChewieController controller;
@override
bool updateShouldNotify(ChewieControllerProvider oldWidget) =>
controller != oldWidget.controller;
}

View File

@@ -0,0 +1,18 @@
import 'package:flutter/rendering.dart';
class ChewieProgressColors {
ChewieProgressColors({
Color playedColor = const Color.fromRGBO(255, 0, 0, 0.7),
Color bufferedColor = const Color.fromRGBO(30, 30, 200, 0.2),
Color handleColor = const Color.fromRGBO(200, 200, 200, 1.0),
Color backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5),
}) : playedPaint = Paint()..color = playedColor,
bufferedPaint = Paint()..color = bufferedColor,
handlePaint = Paint()..color = handleColor,
backgroundPaint = Paint()..color = backgroundColor;
final Paint playedPaint;
final Paint bufferedPaint;
final Paint handlePaint;
final Paint backgroundPaint;
}

View File

@@ -0,0 +1,955 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// import 'package:perfect_volume_control/perfect_volume_control.dart';
import 'package:provider/provider.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:video_player/video_player.dart';
import '../../chewie.dart';
import '../animated_play_pause.dart';
import '../center_play_button.dart';
import '../helpers/utils.dart';
import '../notifiers/index.dart';
import 'cupertino_progress_bar.dart';
import 'widgets/cupertino_options_dialog.dart';
class CupertinoControls extends StatefulWidget {
const CupertinoControls({
required this.backgroundColor,
required this.iconColor,
this.showPlayButton = true,
Key? key,
}) : super(key: key);
final Color backgroundColor;
final Color iconColor;
final bool showPlayButton;
@override
State<StatefulWidget> createState() {
return _CupertinoControlsState();
}
}
class _CupertinoControlsState extends State<CupertinoControls>
with SingleTickerProviderStateMixin {
late PlayerNotifier notifier;
late VideoPlayerValue _latestValue;
double? _latestVolume;
Timer? _hideTimer;
final marginSize = 5.0;
Timer? _expandCollapseTimer;
Timer? _initTimer;
bool _dragging = false;
Duration? _subtitlesPosition;
bool _subtitleOn = false;
Timer? _bufferingDisplayTimer;
bool _displayBufferingIndicator = false;
late VideoPlayerController controller;
// We know that _chewieController is set in didChangeDependencies
ChewieController get chewieController => _chewieController!;
ChewieController? _chewieController;
@override
void initState() {
super.initState();
notifier = Provider.of<PlayerNotifier>(context, listen: false);
}
@override
Widget build(BuildContext context) {
if (_latestValue.hasError) {
return chewieController.errorBuilder != null
? chewieController.errorBuilder!(
context,
chewieController.videoPlayerController.value.errorDescription!,
)
: const Center(
child: Icon(
CupertinoIcons.exclamationmark_circle,
color: Colors.white,
size: 42,
),
);
}
final backgroundColor = widget.backgroundColor;
final iconColor = widget.iconColor;
final orientation = MediaQuery.of(context).orientation;
final barHeight = orientation == Orientation.portrait ? 30.0 : 47.0;
final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0;
double volumeStart = 0.0; //开始手指位置
double volume = 0.0; //开始声音
double _brightness = 0.0; //屏幕亮度
double volumeDouble = 0.018;
return MouseRegion(
onHover: (_) => _cancelAndRestartTimer(),
child: GestureDetector(
onTap: () => _cancelAndRestartTimer(),
onDoubleTap: _playPause,
onLongPress: () {
///长按快进
///
printInfo(info: "长按快进");
},
onLongPressStart: (details) {
controller.setPlaybackSpeed(3);
},
onLongPressEnd: (details) {
controller.setPlaybackSpeed(1);
},
onVerticalDragCancel: () {
print("onVerticalDragCancel");
},
// onVerticalDragDown: (details) {
// print(
// "onVerticalDragDown---${details.globalPosition}---${details.localPosition}");
// },
onVerticalDragEnd: (details) {
// print(
// "onVerticalDragEnd---${details.velocity}---${details.primaryVelocity}");
},
onVerticalDragStart: (details) async {
volumeStart = details.localPosition.dy;
// PerfectVolumeControl.getVolume();
// volume = await PerfectVolumeControl.volume;
_brightness = await ScreenBrightness().current;
print("volumevolume---volume$volume---_brightness $_brightness");
},
onVerticalDragUpdate: (details) async {
double midle = MediaQuery.of(context).size.width / 2;
double width = details.localPosition.dx;
print("====== volume $volume");
if (midle > width) {
if (details.delta.direction > 0) {
_brightness =
_brightness - volumeDouble * details.primaryDelta!.obs.value;
if (_brightness <= 0) {
_brightness = 0;
}
print("====== 左边向下滑 $_brightness");
} else {
_brightness =
_brightness - volumeDouble * details.primaryDelta!.obs.value;
if (_brightness >= 1) {
_brightness = 1;
}
print("====== 左边向上滑 $_brightness");
}
await ScreenBrightness().setScreenBrightness(_brightness);
} else {
if (details.delta.direction > 0) {
volume = volume - volumeDouble * details.primaryDelta!.obs.value;
if (volume <= 0) {
volume = 0;
}
print("====== 右边向下滑 $volume");
} else {
print("====== 右边向上滑 之前 $volume");
volume = volume - volumeDouble * details.primaryDelta!.obs.value;
if (volume >= 1) {
volume = 1;
}
print("====== 右边向上滑 $volume");
}
// PerfectVolumeControl.setVolume(volume);
}
// print(
// "高度====== ${details.localPosition.dy} /250 ${details.localPosition.dy / 250}");
//
// print(
// "onVerticalDragUpdate---${details.globalPosition}---${details.localPosition}---${details.delta}");
},
child: AbsorbPointer(
absorbing: notifier.hideStuff,
child: Stack(
children: [
if (_displayBufferingIndicator)
const Center(
child: CircularProgressIndicator(),
)
else
_buildHitArea(),
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildTopBar(
backgroundColor,
iconColor,
barHeight,
buttonPadding,
),
const Spacer(),
if (_subtitleOn)
Transform.translate(
offset: Offset(
0.0,
notifier.hideStuff ? barHeight * 0.8 : 0.0,
),
child: _buildSubtitles(chewieController.subtitle!),
),
_buildBottomBar(backgroundColor, iconColor, barHeight),
],
),
],
),
),
),
);
}
@override
void dispose() {
_dispose();
super.dispose();
}
void _dispose() {
controller.removeListener(_updateState);
_hideTimer?.cancel();
_expandCollapseTimer?.cancel();
_initTimer?.cancel();
}
@override
void didChangeDependencies() {
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
controller = chewieController.videoPlayerController;
if (oldController != chewieController) {
_dispose();
_initialize();
}
super.didChangeDependencies();
}
GestureDetector _buildOptionsButton(
Color iconColor,
double barHeight,
) {
final options = <OptionItem>[];
if (chewieController.additionalOptions != null &&
chewieController.additionalOptions!(context).isNotEmpty) {
options.addAll(chewieController.additionalOptions!(context));
}
return GestureDetector(
onTap: () async {
_hideTimer?.cancel();
if (chewieController.optionsBuilder != null) {
await chewieController.optionsBuilder!(context, options);
} else {
await showCupertinoModalPopup<OptionItem>(
context: context,
semanticsDismissible: true,
useRootNavigator: chewieController.useRootNavigator,
builder: (context) => CupertinoOptionsDialog(
options: options,
cancelButtonText:
chewieController.optionsTranslation?.cancelButtonText,
),
);
if (_latestValue.isPlaying) {
_startHideTimer();
}
}
},
child: Container(
height: barHeight,
color: Colors.transparent,
padding: const EdgeInsets.only(left: 4.0, right: 8.0),
margin: const EdgeInsets.only(right: 6.0),
child: Icon(
Icons.more_vert,
color: iconColor,
size: 18,
),
),
);
}
Widget _buildSubtitles(Subtitles subtitles) {
if (!_subtitleOn) {
return const SizedBox();
}
if (_subtitlesPosition == null) {
return const SizedBox();
}
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition!);
if (currentSubtitle.isEmpty) {
return const SizedBox();
}
if (chewieController.subtitleBuilder != null) {
return chewieController.subtitleBuilder!(
context,
currentSubtitle.first!.text,
);
}
return Padding(
padding: EdgeInsets.only(left: marginSize, right: marginSize),
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: const Color(0x96000000),
borderRadius: BorderRadius.circular(10.0),
),
child: Text(
currentSubtitle.first!.text.toString(),
style: const TextStyle(
fontSize: 18,
),
textAlign: TextAlign.center,
),
),
);
}
Widget _buildBottomBar(
Color backgroundColor,
Color iconColor,
double barHeight,
) {
return SafeArea(
bottom: chewieController.isFullScreen,
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: Container(
color: Colors.transparent,
alignment: Alignment.bottomCenter,
margin: EdgeInsets.all(marginSize),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: 10.0,
sigmaY: 10.0,
),
child: Container(
height: barHeight,
color: backgroundColor,
child: chewieController.isLive
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildPlayPause(controller, iconColor, barHeight),
_buildLive(iconColor),
],
)
: Row(
children: <Widget>[
// _buildSkipBack(iconColor, barHeight),
_buildPlayPause(controller, iconColor, barHeight),
// _buildSkipForward(iconColor, barHeight),
_buildPosition(iconColor),
_buildProgressBar(),
_buildRemaining(iconColor),
_buildSubtitleToggle(iconColor, barHeight),
// if (chewieController.allowPlaybackSpeedChanging)
// _buildSpeedButton(controller, iconColor, barHeight),
if (chewieController.additionalOptions != null &&
chewieController
.additionalOptions!(context).isNotEmpty)
_buildOptionsButton(iconColor, barHeight),
// if (chewieController.allowFullScreen)
// _buildExpandButton(
// backgroundColor,
// iconColor,
// barHeight,
// MediaQuery.of(context).orientation ==
// Orientation.portrait
// ? 16.0
// : 24.0),
],
),
),
),
),
),
),
);
}
Widget _buildLive(Color iconColor) {
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Text(
'LIVE',
style: TextStyle(color: iconColor, fontSize: 12.0),
),
);
}
GestureDetector _buildExpandButton(
Color backgroundColor,
Color iconColor,
double barHeight,
double buttonPadding, {
bool? showRight,
}) {
return GestureDetector(
onTap: _onExpandCollapse,
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10.0),
child: Container(
height: barHeight,
padding: EdgeInsets.only(
left: buttonPadding,
right: buttonPadding,
),
color: backgroundColor,
child: Center(
child: showRight == true
? const Icon(
Icons.keyboard_arrow_left,
size: 22,
color: Colors.white,
)
: const Icon(
Icons.fullscreen,
size: 22,
color: Colors.white,
)
// 'Image.asset(
// Assets.imagesFullScreen,
// width: 22,
// height: 22,
// ),
// child: Icon(
// chewieController.isFullScreen
// ? CupertinoIcons.arrow_down_right_arrow_up_left
// : CupertinoIcons.arrow_up_left_arrow_down_right,
// color: iconColor,
// size: 16,
// ),
),
),
),
),
),
);
}
Widget _buildHitArea() {
final bool isFinished = _latestValue.position >= _latestValue.duration;
final bool showPlayButton =
widget.showPlayButton && !_latestValue.isPlaying && !_dragging;
return GestureDetector(
onTap: _latestValue.isPlaying
? _cancelAndRestartTimer
: () {
_hideTimer?.cancel();
setState(() {
notifier.hideStuff = false;
});
},
child: CenterPlayButton(
backgroundColor: widget.backgroundColor,
iconColor: widget.iconColor,
isFinished: isFinished,
isPlaying: controller.value.isPlaying,
show: showPlayButton,
onPressed: _playPause,
),
);
}
GestureDetector _buildMuteButton(
VideoPlayerController controller,
Color backgroundColor,
Color iconColor,
double barHeight,
double buttonPadding,
) {
return GestureDetector(
onTap: () {
_cancelAndRestartTimer();
if (_latestValue.volume == 0) {
controller.setVolume(_latestVolume ?? 0.5);
} else {
_latestVolume = controller.value.volume;
controller.setVolume(0.0);
}
},
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10.0),
child: ColoredBox(
color: backgroundColor,
child: Container(
height: barHeight,
padding: EdgeInsets.only(
left: buttonPadding,
right: buttonPadding,
),
child: Icon(
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
color: iconColor,
size: 16,
),
),
),
),
),
),
);
}
GestureDetector _buildPlayPause(
VideoPlayerController controller,
Color iconColor,
double barHeight,
) {
return GestureDetector(
onTap: _playPause,
child: Container(
height: barHeight,
color: Colors.transparent,
padding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
),
child: AnimatedPlayPause(
color: widget.iconColor,
playing: controller.value.isPlaying,
),
),
);
}
Widget _buildPosition(Color iconColor) {
final position = _latestValue.position;
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Text(
formatDuration(position),
style: TextStyle(
color: iconColor,
fontSize: 12.0,
),
),
);
}
Widget _buildRemaining(Color iconColor) {
final position = _latestValue.duration - _latestValue.position;
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Text(
'-${formatDuration(position)}',
style: TextStyle(color: iconColor, fontSize: 12.0),
),
);
}
Widget _buildSubtitleToggle(Color iconColor, double barHeight) {
//if don't have subtitle hiden button
if (chewieController.subtitle?.isEmpty ?? true) {
return const SizedBox();
}
return GestureDetector(
onTap: _subtitleToggle,
child: Container(
height: barHeight,
color: Colors.transparent,
margin: const EdgeInsets.only(right: 10.0),
padding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
),
child: Icon(
Icons.subtitles,
color: _subtitleOn ? iconColor : Colors.grey[700],
size: 16.0,
),
),
);
}
void _subtitleToggle() {
setState(() {
_subtitleOn = !_subtitleOn;
});
}
GestureDetector _buildSkipBack(Color iconColor, double barHeight) {
return GestureDetector(
onTap: _skipBack,
child: Container(
height: barHeight,
color: Colors.transparent,
margin: const EdgeInsets.only(left: 10.0),
padding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
),
child: Icon(
CupertinoIcons.gobackward_15,
color: iconColor,
size: 18.0,
),
),
);
}
GestureDetector _buildSkipForward(Color iconColor, double barHeight) {
return GestureDetector(
onTap: _skipForward,
child: Container(
height: barHeight,
color: Colors.transparent,
padding: const EdgeInsets.only(
left: 6.0,
right: 8.0,
),
margin: const EdgeInsets.only(
right: 8.0,
),
child: Icon(
CupertinoIcons.goforward_15,
color: iconColor,
size: 18.0,
),
),
);
}
GestureDetector _buildSpeedButton(
VideoPlayerController controller,
Color iconColor,
double barHeight,
) {
return GestureDetector(
onTap: () async {
_hideTimer?.cancel();
final chosenSpeed = await showCupertinoModalPopup<double>(
context: context,
semanticsDismissible: true,
useRootNavigator: chewieController.useRootNavigator,
builder: (context) => _PlaybackSpeedDialog(
speeds: chewieController.playbackSpeeds,
selected: _latestValue.playbackSpeed,
),
);
if (chosenSpeed != null) {
controller.setPlaybackSpeed(chosenSpeed);
}
if (_latestValue.isPlaying) {
_startHideTimer();
}
},
child: Container(
height: barHeight,
color: Colors.transparent,
padding: const EdgeInsets.only(
left: 6.0,
right: 8.0,
),
margin: const EdgeInsets.only(
right: 8.0,
),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.skewY(0.0)
..rotateX(math.pi)
..rotateZ(math.pi * 0.8),
child: Icon(
Icons.speed,
color: iconColor,
size: 18.0,
),
),
),
);
}
Widget _buildTopBar(
Color backgroundColor,
Color iconColor,
double barHeight,
double buttonPadding,
) {
return Container(
height: barHeight,
margin: EdgeInsets.only(
top: marginSize,
right: marginSize,
left: marginSize,
),
child: Row(
children: <Widget>[
if (chewieController.allowFullScreen)
if (chewieController.isFullScreen)
_buildExpandButton(
backgroundColor,
iconColor,
barHeight,
buttonPadding,
showRight: true,
),
const Spacer(),
// if (chewieController.allowMuting)
// _buildMuteButton(
// controller,
// backgroundColor,
// iconColor,
// barHeight,
// buttonPadding,
// ),
],
),
);
}
void _cancelAndRestartTimer() {
_hideTimer?.cancel();
setState(() {
notifier.hideStuff = false;
_startHideTimer();
});
}
Future<void> _initialize() async {
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
controller.addListener(_updateState);
_updateState();
if (controller.value.isPlaying || chewieController.autoPlay) {
_startHideTimer();
}
if (chewieController.showControlsOnInitialize) {
_initTimer = Timer(const Duration(milliseconds: 200), () {
setState(() {
notifier.hideStuff = false;
});
});
}
}
void _onExpandCollapse() {
Navigator.of(context).pop();
setState(() {
Navigator.of(context).pop();
// notifier.hideStuff = true;
//
// chewieController.toggleFullScreen();
// _expandCollapseTimer = Timer(const Duration(milliseconds: 300), () {
// setState(() {
// _cancelAndRestartTimer();
// });
// });
});
}
Widget _buildProgressBar() {
return Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 12.0),
child: CupertinoVideoProgressBar(
controller,
onDragStart: () {
setState(() {
_dragging = true;
});
_hideTimer?.cancel();
},
onDragEnd: () {
setState(() {
_dragging = false;
});
_startHideTimer();
},
colors: chewieController.cupertinoProgressColors ??
ChewieProgressColors(
playedColor: const Color.fromARGB(
120,
255,
255,
255,
),
handleColor: const Color.fromARGB(
255,
255,
255,
255,
),
bufferedColor: const Color.fromARGB(
60,
255,
255,
255,
),
backgroundColor: const Color.fromARGB(
20,
255,
255,
255,
),
),
),
),
);
}
void _playPause() {
final isFinished = _latestValue.position >= _latestValue.duration;
setState(() {
if (controller.value.isPlaying) {
notifier.hideStuff = false;
_hideTimer?.cancel();
controller.pause();
} else {
_cancelAndRestartTimer();
if (!controller.value.isInitialized) {
controller.initialize().then((_) {
controller.play();
});
} else {
if (isFinished) {
controller.seekTo(Duration.zero);
}
controller.play();
}
}
});
}
void _skipBack() {
_cancelAndRestartTimer();
final beginning = Duration.zero.inMilliseconds;
final skip =
(_latestValue.position - const Duration(seconds: 15)).inMilliseconds;
controller.seekTo(Duration(milliseconds: math.max(skip, beginning)));
}
void _skipForward() {
_cancelAndRestartTimer();
final end = _latestValue.duration.inMilliseconds;
final skip =
(_latestValue.position + const Duration(seconds: 15)).inMilliseconds;
controller.seekTo(Duration(milliseconds: math.min(skip, end)));
}
void _startHideTimer() {
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
? ChewieController.defaultHideControlsTimer
: chewieController.hideControlsTimer;
_hideTimer = Timer(hideControlsTimer, () {
setState(() {
notifier.hideStuff = true;
});
});
}
void _bufferingTimerTimeout() {
_displayBufferingIndicator = true;
if (mounted) {
setState(() {});
}
}
void _updateState() {
if (!mounted) return;
// display the progress bar indicator only after the buffering delay if it has been set
if (chewieController.progressIndicatorDelay != null) {
if (controller.value.isBuffering) {
_bufferingDisplayTimer ??= Timer(
chewieController.progressIndicatorDelay!,
_bufferingTimerTimeout,
);
} else {
_bufferingDisplayTimer?.cancel();
_bufferingDisplayTimer = null;
_displayBufferingIndicator = false;
}
} else {
_displayBufferingIndicator = controller.value.isBuffering;
}
setState(() {
_latestValue = controller.value;
_subtitlesPosition = controller.value.position;
});
}
}
class _PlaybackSpeedDialog extends StatelessWidget {
const _PlaybackSpeedDialog({
Key? key,
required List<double> speeds,
required double selected,
}) : _speeds = speeds,
_selected = selected,
super(key: key);
final List<double> _speeds;
final double _selected;
@override
Widget build(BuildContext context) {
final selectedColor = CupertinoTheme.of(context).primaryColor;
return CupertinoActionSheet(
actions: _speeds
.map(
(e) => CupertinoActionSheetAction(
onPressed: () {
Navigator.of(context).pop(e);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (e == _selected)
Icon(Icons.check, size: 20.0, color: selectedColor),
Text(e.toString()),
],
),
),
)
.toList(),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:video_player/video_player.dart';
import '../../chewie.dart';
import '../progress_bar.dart';
class CupertinoVideoProgressBar extends StatelessWidget {
CupertinoVideoProgressBar(
this.controller, {
ChewieProgressColors? colors,
this.onDragEnd,
this.onDragStart,
this.onDragUpdate,
Key? key,
}) : colors = colors ?? ChewieProgressColors(),
super(key: key);
final VideoPlayerController controller;
final ChewieProgressColors colors;
final Function()? onDragStart;
final Function()? onDragEnd;
final Function()? onDragUpdate;
@override
Widget build(BuildContext context) {
return VideoProgressBar(
controller,
barHeight: 5,
handleHeight: 6,
drawShadow: true,
colors: colors,
onDragEnd: onDragEnd,
onDragStart: onDragStart,
onDragUpdate: onDragUpdate,
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/cupertino.dart';
import '../../../chewie.dart';
class CupertinoOptionsDialog extends StatefulWidget {
const CupertinoOptionsDialog({
Key? key,
required this.options,
this.cancelButtonText,
}) : super(key: key);
final List<OptionItem> options;
final String? cancelButtonText;
@override
// ignore: library_private_types_in_public_api
_CupertinoOptionsDialogState createState() => _CupertinoOptionsDialogState();
}
class _CupertinoOptionsDialogState extends State<CupertinoOptionsDialog> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: CupertinoActionSheet(
actions: widget.options
.map(
(option) => CupertinoActionSheetAction(
onPressed: () => option.onTap!(),
child: Text(option.title),
),
)
.toList(),
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.pop(context),
isDestructiveAction: true,
child: Text(widget.cancelButtonText ?? 'Cancel'),
),
),
);
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import '../../chewie.dart';
class AdaptiveControls extends StatelessWidget {
const AdaptiveControls({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return const CupertinoControls(
backgroundColor: Color.fromRGBO(41, 41, 41, 0.7),
iconColor: Color.fromARGB(255, 200, 200, 200),
);
return const MaterialControls();
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
return const MaterialDesktopControls();
case TargetPlatform.iOS:
return const CupertinoControls(
backgroundColor: Color.fromRGBO(41, 41, 41, 0.7),
iconColor: Color.fromARGB(255, 200, 200, 200),
);
default:
return const MaterialControls();
}
}
}

View File

@@ -0,0 +1,32 @@
String formatDuration(Duration position) {
final ms = position.inMilliseconds;
int seconds = ms ~/ 1000;
final int hours = seconds ~/ 3600;
seconds = seconds % 3600;
final minutes = seconds ~/ 60;
seconds = seconds % 60;
final hoursString = hours >= 10
? '$hours'
: hours == 0
? '00'
: '0$hours';
final minutesString = minutes >= 10
? '$minutes'
: minutes == 0
? '00'
: '0$minutes';
final secondsString = seconds >= 10
? '$seconds'
: seconds == 0
? '00'
: '0$seconds';
final formattedTime =
'${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString';
return formattedTime;
}

View File

@@ -0,0 +1,623 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
import '../../chewie.dart';
import '../center_play_button.dart';
import '../helpers/utils.dart';
import '../notifiers/index.dart';
import 'material_progress_bar.dart';
import 'widgets/options_dialog.dart';
import 'widgets/playback_speed_dialog.dart';
class MaterialControls extends StatefulWidget {
const MaterialControls({
this.showPlayButton = true,
Key? key,
}) : super(key: key);
final bool showPlayButton;
@override
State<StatefulWidget> createState() {
return _MaterialControlsState();
}
}
class _MaterialControlsState extends State<MaterialControls>
with SingleTickerProviderStateMixin {
late PlayerNotifier notifier;
late VideoPlayerValue _latestValue;
double? _latestVolume;
Timer? _hideTimer;
Timer? _initTimer;
late var _subtitlesPosition = Duration.zero;
bool _subtitleOn = false;
Timer? _showAfterExpandCollapseTimer;
bool _dragging = false;
bool _displayTapped = false;
Timer? _bufferingDisplayTimer;
bool _displayBufferingIndicator = false;
final barHeight = 48.0 * 1.5;
final marginSize = 5.0;
late VideoPlayerController controller;
ChewieController? _chewieController;
// We know that _chewieController is set in didChangeDependencies
ChewieController get chewieController => _chewieController!;
@override
void initState() {
super.initState();
notifier = Provider.of<PlayerNotifier>(context, listen: false);
}
@override
Widget build(BuildContext context) {
if (_latestValue.hasError) {
return chewieController.errorBuilder?.call(
context,
chewieController.videoPlayerController.value.errorDescription!,
) ??
const Center(
child: Icon(
Icons.error,
color: Colors.white,
size: 42,
),
);
}
return MouseRegion(
onHover: (_) {
_cancelAndRestartTimer();
},
child: GestureDetector(
onTap: () => _cancelAndRestartTimer(),
onLongPress: () {
///长按快进
///
},
onLongPressStart: (details) {
controller.setPlaybackSpeed(3);
},
onLongPressEnd: (details) {
controller.setPlaybackSpeed(1);
},
child: AbsorbPointer(
absorbing: notifier.hideStuff,
child: Stack(
children: [
if (_displayBufferingIndicator)
const Center(
child: CircularProgressIndicator(),
)
else
_buildHitArea(),
_buildActionBar(),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
if (_subtitleOn)
Transform.translate(
offset: Offset(
0.0,
notifier.hideStuff ? barHeight * 0.8 : 0.0,
),
child:
_buildSubtitles(context, chewieController.subtitle!),
),
_buildBottomBar(context),
],
),
],
),
),
),
);
}
@override
void dispose() {
_dispose();
super.dispose();
}
void _dispose() {
controller.removeListener(_updateState);
_hideTimer?.cancel();
_initTimer?.cancel();
_showAfterExpandCollapseTimer?.cancel();
}
@override
void didChangeDependencies() {
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
controller = chewieController.videoPlayerController;
if (oldController != chewieController) {
_dispose();
_initialize();
}
super.didChangeDependencies();
}
Widget _buildActionBar() {
return Positioned(
top: 0,
right: 0,
child: SafeArea(
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 250),
child: Row(
children: [
_buildSubtitleToggle(),
if (chewieController.showOptions) _buildOptionsButton(),
],
),
),
),
);
}
Widget _buildOptionsButton() {
final options = <OptionItem>[
OptionItem(
onTap: () async {
Navigator.pop(context);
_onSpeedButtonTap();
},
iconData: Icons.speed,
title: chewieController.optionsTranslation?.playbackSpeedButtonText ??
'Playback speed',
)
];
if (chewieController.additionalOptions != null &&
chewieController.additionalOptions!(context).isNotEmpty) {
options.addAll(chewieController.additionalOptions!(context));
}
return AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 250),
child: IconButton(
onPressed: () async {
_hideTimer?.cancel();
if (chewieController.optionsBuilder != null) {
await chewieController.optionsBuilder!(context, options);
} else {
await showModalBottomSheet<OptionItem>(
context: context,
isScrollControlled: true,
useRootNavigator: chewieController.useRootNavigator,
builder: (context) => OptionsDialog(
options: options,
cancelButtonText:
chewieController.optionsTranslation?.cancelButtonText,
),
);
}
if (_latestValue.isPlaying) {
_startHideTimer();
}
},
icon: const Icon(
Icons.more_vert,
color: Colors.white,
),
),
);
}
Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
if (!_subtitleOn) {
return const SizedBox();
}
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition);
if (currentSubtitle.isEmpty) {
return const SizedBox();
}
if (chewieController.subtitleBuilder != null) {
return chewieController.subtitleBuilder!(
context,
currentSubtitle.first!.text,
);
}
return Padding(
padding: EdgeInsets.all(marginSize),
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: const Color(0x96000000),
borderRadius: BorderRadius.circular(10.0),
),
child: Text(
currentSubtitle.first!.text.toString(),
style: const TextStyle(
fontSize: 18,
),
textAlign: TextAlign.center,
),
),
);
}
AnimatedOpacity _buildBottomBar(
BuildContext context,
) {
final iconColor = Theme.of(context).textTheme.labelMedium!.color;
return AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: Container(
height: barHeight + (chewieController.isFullScreen ? 10.0 : 0),
padding: EdgeInsets.only(
left: 20,
bottom: !chewieController.isFullScreen ? 10.0 : 0,
),
child: SafeArea(
bottom: chewieController.isFullScreen,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
if (chewieController.isLive)
const Expanded(child: Text('LIVE'))
else
_buildPosition(iconColor),
if (chewieController.allowMuting)
_buildMuteButton(controller),
const Spacer(),
if (chewieController.allowFullScreen) _buildExpandButton(),
],
),
),
SizedBox(
height: chewieController.isFullScreen ? 15.0 : 0,
),
if (!chewieController.isLive)
Expanded(
child: Container(
padding: const EdgeInsets.only(right: 20),
child: Row(
children: [
_buildProgressBar(),
],
),
),
),
],
),
),
),
);
}
GestureDetector _buildMuteButton(
VideoPlayerController controller,
) {
return GestureDetector(
onTap: () {
_cancelAndRestartTimer();
if (_latestValue.volume == 0) {
controller.setVolume(_latestVolume ?? 0.5);
} else {
_latestVolume = controller.value.volume;
controller.setVolume(0.0);
}
},
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: ClipRect(
child: Container(
height: barHeight,
padding: const EdgeInsets.only(
left: 6.0,
),
child: Icon(
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
color: Colors.white,
),
),
),
),
);
}
GestureDetector _buildExpandButton() {
return GestureDetector(
onTap: _onExpandCollapse,
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: Container(
height: barHeight + (chewieController.isFullScreen ? 15.0 : 0),
margin: const EdgeInsets.only(right: 12.0),
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
),
child: Center(
child: Icon(
chewieController.isFullScreen
? Icons.fullscreen_exit
: Icons.fullscreen,
color: Colors.white,
),
),
),
),
);
}
Widget _buildHitArea() {
final bool isFinished = _latestValue.position >= _latestValue.duration;
final bool showPlayButton =
widget.showPlayButton && !_dragging && !notifier.hideStuff;
return GestureDetector(
onTap: () {
if (_latestValue.isPlaying) {
if (_displayTapped) {
setState(() {
notifier.hideStuff = true;
});
} else {
_cancelAndRestartTimer();
}
} else {
_playPause();
setState(() {
notifier.hideStuff = true;
});
}
},
child: CenterPlayButton(
backgroundColor: Colors.black54,
iconColor: Colors.white,
isFinished: isFinished,
isPlaying: controller.value.isPlaying,
show: showPlayButton,
onPressed: _playPause,
),
);
}
Future<void> _onSpeedButtonTap() async {
_hideTimer?.cancel();
final chosenSpeed = await showModalBottomSheet<double>(
context: context,
isScrollControlled: true,
useRootNavigator: chewieController.useRootNavigator,
builder: (context) => PlaybackSpeedDialog(
speeds: chewieController.playbackSpeeds,
selected: _latestValue.playbackSpeed,
),
);
if (chosenSpeed != null) {
controller.setPlaybackSpeed(chosenSpeed);
}
if (_latestValue.isPlaying) {
_startHideTimer();
}
}
Widget _buildPosition(Color? iconColor) {
final position = _latestValue.position;
final duration = _latestValue.duration;
return RichText(
text: TextSpan(
text: '${formatDuration(position)} ',
children: <InlineSpan>[
TextSpan(
text: '/ ${formatDuration(duration)}',
style: TextStyle(
fontSize: 14.0,
color: Colors.white.withOpacity(.75),
fontWeight: FontWeight.normal,
),
)
],
style: const TextStyle(
fontSize: 14.0,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _buildSubtitleToggle() {
//if don't have subtitle hiden button
if (chewieController.subtitle?.isEmpty ?? true) {
return const SizedBox();
}
return GestureDetector(
onTap: _onSubtitleTap,
child: Container(
height: barHeight,
color: Colors.transparent,
padding: const EdgeInsets.only(
left: 12.0,
right: 12.0,
),
child: Icon(
_subtitleOn
? Icons.closed_caption
: Icons.closed_caption_off_outlined,
color: _subtitleOn ? Colors.white : Colors.grey[700],
),
),
);
}
void _onSubtitleTap() {
setState(() {
_subtitleOn = !_subtitleOn;
});
}
void _cancelAndRestartTimer() {
_hideTimer?.cancel();
_startHideTimer();
setState(() {
notifier.hideStuff = false;
_displayTapped = true;
});
}
Future<void> _initialize() async {
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
controller.addListener(_updateState);
_updateState();
if (controller.value.isPlaying || chewieController.autoPlay) {
_startHideTimer();
}
if (chewieController.showControlsOnInitialize) {
_initTimer = Timer(const Duration(milliseconds: 200), () {
setState(() {
notifier.hideStuff = false;
});
});
}
}
void _onExpandCollapse() {
setState(() {
notifier.hideStuff = true;
chewieController.toggleFullScreen();
_showAfterExpandCollapseTimer =
Timer(const Duration(milliseconds: 300), () {
setState(() {
_cancelAndRestartTimer();
});
});
});
}
void _playPause() {
final isFinished = _latestValue.position >= _latestValue.duration;
setState(() {
if (controller.value.isPlaying) {
notifier.hideStuff = false;
_hideTimer?.cancel();
controller.pause();
} else {
_cancelAndRestartTimer();
if (!controller.value.isInitialized) {
controller.initialize().then((_) {
controller.play();
});
} else {
if (isFinished) {
controller.seekTo(Duration.zero);
}
controller.play();
}
}
});
}
void _startHideTimer() {
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
? ChewieController.defaultHideControlsTimer
: chewieController.hideControlsTimer;
_hideTimer = Timer(hideControlsTimer, () {
setState(() {
notifier.hideStuff = true;
});
});
}
void _bufferingTimerTimeout() {
_displayBufferingIndicator = true;
if (mounted) {
setState(() {});
}
}
void _updateState() {
if (!mounted) return;
// display the progress bar indicator only after the buffering delay if it has been set
if (chewieController.progressIndicatorDelay != null) {
if (controller.value.isBuffering) {
_bufferingDisplayTimer ??= Timer(
chewieController.progressIndicatorDelay!,
_bufferingTimerTimeout,
);
} else {
_bufferingDisplayTimer?.cancel();
_bufferingDisplayTimer = null;
_displayBufferingIndicator = false;
}
} else {
_displayBufferingIndicator = controller.value.isBuffering;
}
setState(() {
_latestValue = controller.value;
_subtitlesPosition = controller.value.position;
});
}
Widget _buildProgressBar() {
return Expanded(
child: MaterialVideoProgressBar(
controller,
onDragStart: () {
setState(() {
_dragging = true;
});
_hideTimer?.cancel();
},
onDragEnd: () {
setState(() {
_dragging = false;
});
_startHideTimer();
},
colors: chewieController.materialProgressColors ??
ChewieProgressColors(
playedColor: Theme.of(context).colorScheme.secondary,
handleColor: Theme.of(context).colorScheme.secondary,
bufferedColor: Theme.of(context).disabledColor.withOpacity(0.5),
backgroundColor: Theme.of(context).disabledColor.withOpacity(.5),
),
),
);
}
}

View File

@@ -0,0 +1,593 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/file/video/lib/chewie.dart';
import 'package:flutter_common/utils/file/video/lib/src/chewie_player.dart';
import 'package:flutter_common/utils/file/video/lib/src/material/widgets/options_dialog.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
import '../animated_play_pause.dart';
import '../center_play_button.dart';
import '../helpers/utils.dart';
import '../notifiers/index.dart';
import 'material_progress_bar.dart';
import 'widgets/playback_speed_dialog.dart';
class MaterialDesktopControls extends StatefulWidget {
const MaterialDesktopControls({
this.showPlayButton = true,
Key? key,
}) : super(key: key);
final bool showPlayButton;
@override
State<StatefulWidget> createState() {
return _MaterialDesktopControlsState();
}
}
class _MaterialDesktopControlsState extends State<MaterialDesktopControls>
with SingleTickerProviderStateMixin {
late PlayerNotifier notifier;
late VideoPlayerValue _latestValue;
double? _latestVolume;
Timer? _hideTimer;
Timer? _initTimer;
late var _subtitlesPosition = Duration.zero;
bool _subtitleOn = false;
Timer? _showAfterExpandCollapseTimer;
bool _dragging = false;
bool _displayTapped = false;
Timer? _bufferingDisplayTimer;
bool _displayBufferingIndicator = false;
final barHeight = 48.0 * 1.5;
final marginSize = 5.0;
late VideoPlayerController controller;
ChewieController? _chewieController;
// We know that _chewieController is set in didChangeDependencies
ChewieController get chewieController => _chewieController!;
@override
void initState() {
super.initState();
notifier = Provider.of<PlayerNotifier>(context, listen: false);
}
@override
Widget build(BuildContext context) {
if (_latestValue.hasError) {
return chewieController.errorBuilder?.call(
context,
chewieController.videoPlayerController.value.errorDescription!,
) ??
const Center(
child: Icon(
Icons.error,
color: Colors.white,
size: 42,
),
);
}
return MouseRegion(
onHover: (_) {
_cancelAndRestartTimer();
},
child: GestureDetector(
onTap: () => _cancelAndRestartTimer(),
child: AbsorbPointer(
absorbing: notifier.hideStuff,
child: Stack(
children: [
if (_displayBufferingIndicator)
const Center(
child: CircularProgressIndicator(),
)
else
_buildHitArea(),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
if (_subtitleOn)
Transform.translate(
offset: Offset(
0.0,
notifier.hideStuff ? barHeight * 0.8 : 0.0,
),
child:
_buildSubtitles(context, chewieController.subtitle!),
),
_buildBottomBar(context),
],
),
],
),
),
),
);
}
@override
void dispose() {
_dispose();
super.dispose();
}
void _dispose() {
controller.removeListener(_updateState);
_hideTimer?.cancel();
_initTimer?.cancel();
_showAfterExpandCollapseTimer?.cancel();
}
@override
void didChangeDependencies() {
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
controller = chewieController.videoPlayerController;
if (oldController != chewieController) {
_dispose();
_initialize();
}
super.didChangeDependencies();
}
Widget _buildSubtitleToggle({IconData? icon, bool isPadded = false}) {
return IconButton(
padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero,
icon: Icon(icon, color: _subtitleOn ? Colors.white : Colors.grey[700]),
onPressed: _onSubtitleTap,
);
}
Widget _buildOptionsButton({
IconData? icon,
bool isPadded = false,
}) {
final options = <OptionItem>[
OptionItem(
onTap: () async {
Navigator.pop(context);
_onSpeedButtonTap();
},
iconData: Icons.speed,
title: chewieController.optionsTranslation?.playbackSpeedButtonText ??
'Playback speed',
)
];
if (chewieController.additionalOptions != null &&
chewieController.additionalOptions!(context).isNotEmpty) {
options.addAll(chewieController.additionalOptions!(context));
}
return AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 250),
child: IconButton(
padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero,
onPressed: () async {
_hideTimer?.cancel();
if (chewieController.optionsBuilder != null) {
await chewieController.optionsBuilder!(context, options);
} else {
await showModalBottomSheet<OptionItem>(
context: context,
isScrollControlled: true,
useRootNavigator: chewieController.useRootNavigator,
builder: (context) => OptionsDialog(
options: options,
cancelButtonText:
chewieController.optionsTranslation?.cancelButtonText,
),
);
}
if (_latestValue.isPlaying) {
_startHideTimer();
}
},
icon: Icon(
icon ?? Icons.more_vert,
color: Colors.white,
),
),
);
}
Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
if (!_subtitleOn) {
return const SizedBox();
}
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition);
if (currentSubtitle.isEmpty) {
return const SizedBox();
}
if (chewieController.subtitleBuilder != null) {
return chewieController.subtitleBuilder!(
context,
currentSubtitle.first!.text,
);
}
return Padding(
padding: EdgeInsets.all(marginSize),
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: const Color(0x96000000),
borderRadius: BorderRadius.circular(10.0),
),
child: Text(
currentSubtitle.first!.text.toString(),
style: const TextStyle(
fontSize: 18,
),
textAlign: TextAlign.center,
),
),
);
}
AnimatedOpacity _buildBottomBar(
BuildContext context,
) {
final iconColor = Theme.of(context).textTheme.labelMedium!.color;
return AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: Container(
height: barHeight + (chewieController.isFullScreen ? 20.0 : 0),
padding:
EdgeInsets.only(bottom: chewieController.isFullScreen ? 10.0 : 15),
child: SafeArea(
bottom: chewieController.isFullScreen,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
verticalDirection: VerticalDirection.up,
children: [
Flexible(
child: Row(
children: <Widget>[
_buildPlayPause(controller),
_buildMuteButton(controller),
if (chewieController.isLive)
const Expanded(child: Text('LIVE'))
else
_buildPosition(iconColor),
const Spacer(),
if (chewieController.showControls &&
chewieController.subtitle != null &&
chewieController.subtitle!.isNotEmpty)
_buildSubtitleToggle(icon: Icons.subtitles),
if (chewieController.showOptions)
_buildOptionsButton(icon: Icons.settings),
if (chewieController.allowFullScreen) _buildExpandButton(),
],
),
),
if (!chewieController.isLive)
Expanded(
child: Container(
padding: EdgeInsets.only(
right: 20,
left: 20,
bottom: chewieController.isFullScreen ? 5.0 : 0,
),
child: Row(
children: [
_buildProgressBar(),
],
),
),
),
],
),
),
),
);
}
GestureDetector _buildExpandButton() {
return GestureDetector(
onTap: _onExpandCollapse,
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: Container(
height: barHeight + (chewieController.isFullScreen ? 15.0 : 0),
margin: const EdgeInsets.only(right: 12.0),
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
),
child: Center(
child: Icon(
chewieController.isFullScreen
? Icons.fullscreen_exit
: Icons.fullscreen,
color: Colors.white,
),
),
),
),
);
}
Widget _buildHitArea() {
final bool isFinished = _latestValue.position >= _latestValue.duration;
final bool showPlayButton =
widget.showPlayButton && !_dragging && !notifier.hideStuff;
return GestureDetector(
onTap: () {
if (_latestValue.isPlaying) {
if (_displayTapped) {
setState(() {
notifier.hideStuff = true;
});
} else {
_cancelAndRestartTimer();
}
} else {
_playPause();
setState(() {
notifier.hideStuff = true;
});
}
},
child: CenterPlayButton(
backgroundColor: Colors.black54,
iconColor: Colors.white,
isFinished: isFinished,
isPlaying: controller.value.isPlaying,
show: showPlayButton,
onPressed: _playPause,
),
);
}
Future<void> _onSpeedButtonTap() async {
_hideTimer?.cancel();
final chosenSpeed = await showModalBottomSheet<double>(
context: context,
isScrollControlled: true,
useRootNavigator: chewieController.useRootNavigator,
builder: (context) => PlaybackSpeedDialog(
speeds: chewieController.playbackSpeeds,
selected: _latestValue.playbackSpeed,
),
);
if (chosenSpeed != null) {
controller.setPlaybackSpeed(chosenSpeed);
}
if (_latestValue.isPlaying) {
_startHideTimer();
}
}
GestureDetector _buildMuteButton(
VideoPlayerController controller,
) {
return GestureDetector(
onTap: () {
_cancelAndRestartTimer();
if (_latestValue.volume == 0) {
controller.setVolume(_latestVolume ?? 0.5);
} else {
_latestVolume = controller.value.volume;
controller.setVolume(0.0);
}
},
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: ClipRect(
child: Container(
height: barHeight,
padding: const EdgeInsets.only(
right: 15.0,
),
child: Icon(
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
color: Colors.white,
),
),
),
),
);
}
GestureDetector _buildPlayPause(VideoPlayerController controller) {
return GestureDetector(
onTap: _playPause,
child: Container(
height: barHeight,
color: Colors.transparent,
margin: const EdgeInsets.only(left: 8.0, right: 4.0),
padding: const EdgeInsets.only(
left: 12.0,
right: 12.0,
),
child: AnimatedPlayPause(
playing: controller.value.isPlaying,
color: Colors.white,
),
),
);
}
Widget _buildPosition(Color? iconColor) {
final position = _latestValue.position;
final duration = _latestValue.duration;
return Text(
'${formatDuration(position)} / ${formatDuration(duration)}',
style: const TextStyle(
fontSize: 14.0,
color: Colors.white,
),
);
}
void _onSubtitleTap() {
setState(() {
_subtitleOn = !_subtitleOn;
});
}
void _cancelAndRestartTimer() {
_hideTimer?.cancel();
_startHideTimer();
setState(() {
notifier.hideStuff = false;
_displayTapped = true;
});
}
Future<void> _initialize() async {
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
controller.addListener(_updateState);
_updateState();
if (controller.value.isPlaying || chewieController.autoPlay) {
_startHideTimer();
}
if (chewieController.showControlsOnInitialize) {
_initTimer = Timer(const Duration(milliseconds: 200), () {
setState(() {
notifier.hideStuff = false;
});
});
}
}
void _onExpandCollapse() {
setState(() {
notifier.hideStuff = true;
chewieController.toggleFullScreen();
_showAfterExpandCollapseTimer =
Timer(const Duration(milliseconds: 300), () {
setState(() {
_cancelAndRestartTimer();
});
});
});
}
void _playPause() {
final isFinished = _latestValue.position >= _latestValue.duration;
setState(() {
if (controller.value.isPlaying) {
notifier.hideStuff = false;
_hideTimer?.cancel();
controller.pause();
} else {
_cancelAndRestartTimer();
if (!controller.value.isInitialized) {
controller.initialize().then((_) {
controller.play();
});
} else {
if (isFinished) {
controller.seekTo(Duration.zero);
}
controller.play();
}
}
});
}
void _startHideTimer() {
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
? ChewieController.defaultHideControlsTimer
: chewieController.hideControlsTimer;
_hideTimer = Timer(hideControlsTimer, () {
setState(() {
notifier.hideStuff = true;
});
});
}
void _bufferingTimerTimeout() {
_displayBufferingIndicator = true;
if (mounted) {
setState(() {});
}
}
void _updateState() {
if (!mounted) return;
// display the progress bar indicator only after the buffering delay if it has been set
if (chewieController.progressIndicatorDelay != null) {
if (controller.value.isBuffering) {
_bufferingDisplayTimer ??= Timer(
chewieController.progressIndicatorDelay!,
_bufferingTimerTimeout,
);
} else {
_bufferingDisplayTimer?.cancel();
_bufferingDisplayTimer = null;
_displayBufferingIndicator = false;
}
} else {
_displayBufferingIndicator = controller.value.isBuffering;
}
setState(() {
_latestValue = controller.value;
_subtitlesPosition = controller.value.position;
});
}
Widget _buildProgressBar() {
return Expanded(
child: MaterialVideoProgressBar(
controller,
onDragStart: () {
setState(() {
_dragging = true;
});
_hideTimer?.cancel();
},
onDragEnd: () {
setState(() {
_dragging = false;
});
_startHideTimer();
},
colors: chewieController.materialProgressColors ??
ChewieProgressColors(
playedColor: Theme.of(context).colorScheme.secondary,
handleColor: Theme.of(context).colorScheme.secondary,
bufferedColor: Theme.of(context).disabledColor.withOpacity(0.5),
backgroundColor: Theme.of(context).disabledColor.withOpacity(.5),
),
),
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/file/video/lib/src/progress_bar.dart';
import 'package:video_player/video_player.dart';
import '../../chewie.dart';
class MaterialVideoProgressBar extends StatelessWidget {
MaterialVideoProgressBar(
this.controller, {
this.height = kToolbarHeight,
ChewieProgressColors? colors,
this.onDragEnd,
this.onDragStart,
this.onDragUpdate,
Key? key,
}) : colors = colors ?? ChewieProgressColors(),
super(key: key);
final double height;
final VideoPlayerController controller;
final ChewieProgressColors colors;
final Function()? onDragStart;
final Function()? onDragEnd;
final Function()? onDragUpdate;
@override
Widget build(BuildContext context) {
return VideoProgressBar(
controller,
barHeight: 10,
handleHeight: 6,
drawShadow: true,
colors: colors,
onDragEnd: onDragEnd,
onDragStart: onDragStart,
onDragUpdate: onDragUpdate,
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import '../../../chewie.dart';
class OptionsDialog extends StatefulWidget {
const OptionsDialog({
Key? key,
required this.options,
this.cancelButtonText,
}) : super(key: key);
final List<OptionItem> options;
final String? cancelButtonText;
@override
// ignore: library_private_types_in_public_api
_OptionsDialogState createState() => _OptionsDialogState();
}
class _OptionsDialogState extends State<OptionsDialog> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListView.builder(
shrinkWrap: true,
itemCount: widget.options.length,
itemBuilder: (context, i) {
return ListTile(
onTap: widget.options[i].onTap != null
? widget.options[i].onTap!
: null,
leading: Icon(widget.options[i].iconData),
title: Text(widget.options[i].title),
subtitle: widget.options[i].subtitle != null
? Text(widget.options[i].subtitle!)
: null,
);
},
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Divider(
thickness: 1.0,
),
),
ListTile(
onTap: () => Navigator.pop(context),
leading: const Icon(Icons.close),
title: Text(
widget.cancelButtonText ?? 'Cancel',
),
),
],
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
class PlaybackSpeedDialog extends StatelessWidget {
const PlaybackSpeedDialog({
Key? key,
required List<double> speeds,
required double selected,
}) : _speeds = speeds,
_selected = selected,
super(key: key);
final List<double> _speeds;
final double _selected;
@override
Widget build(BuildContext context) {
final Color selectedColor = Theme.of(context).primaryColor;
return ListView.builder(
shrinkWrap: true,
physics: const ScrollPhysics(),
itemBuilder: (context, index) {
final speed = _speeds[index];
return ListTile(
dense: true,
title: Row(
children: [
if (speed == _selected)
Icon(
Icons.check,
size: 20.0,
color: selectedColor,
)
else
Container(width: 20.0),
const SizedBox(width: 16.0),
Text(speed.toString()),
],
),
selected: speed == _selected,
onTap: () {
Navigator.of(context).pop(speed);
},
);
},
itemCount: _speeds.length,
);
}
}

View File

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

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
class OptionItem {
OptionItem({
required this.onTap,
required this.iconData,
required this.title,
this.subtitle,
});
Function()? onTap;
IconData iconData;
String title;
String? subtitle;
OptionItem copyWith({
Function()? onTap,
IconData? iconData,
String? title,
String? subtitle,
}) {
return OptionItem(
onTap: onTap ?? this.onTap,
iconData: iconData ?? this.iconData,
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
);
}
@override
String toString() =>
'OptionItem(onTap: $onTap, iconData: $iconData, title: $title, subtitle: $subtitle)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is OptionItem &&
other.onTap == onTap &&
other.iconData == iconData &&
other.title == title &&
other.subtitle == subtitle;
}
@override
int get hashCode =>
onTap.hashCode ^ iconData.hashCode ^ title.hashCode ^ subtitle.hashCode;
}

View File

@@ -0,0 +1,44 @@
class OptionsTranslation {
OptionsTranslation({
this.playbackSpeedButtonText,
this.subtitlesButtonText,
this.cancelButtonText,
});
String? playbackSpeedButtonText;
String? subtitlesButtonText;
String? cancelButtonText;
OptionsTranslation copyWith({
String? playbackSpeedButtonText,
String? subtitlesButtonText,
String? cancelButtonText,
}) {
return OptionsTranslation(
playbackSpeedButtonText:
playbackSpeedButtonText ?? this.playbackSpeedButtonText,
subtitlesButtonText: subtitlesButtonText ?? this.subtitlesButtonText,
cancelButtonText: cancelButtonText ?? this.cancelButtonText,
);
}
@override
String toString() =>
'OptionsTranslation(playbackSpeedButtonText: $playbackSpeedButtonText, subtitlesButtonText: $subtitlesButtonText, cancelButtonText: $cancelButtonText)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is OptionsTranslation &&
other.playbackSpeedButtonText == playbackSpeedButtonText &&
other.subtitlesButtonText == subtitlesButtonText &&
other.cancelButtonText == cancelButtonText;
}
@override
int get hashCode =>
playbackSpeedButtonText.hashCode ^
subtitlesButtonText.hashCode ^
cancelButtonText.hashCode;
}

View File

@@ -0,0 +1,67 @@
class Subtitles {
Subtitles(this.subtitle);
final List<Subtitle?> subtitle;
bool get isEmpty => subtitle.isEmpty;
bool get isNotEmpty => !isEmpty;
List<Subtitle?> getByPosition(Duration position) {
final found = subtitle.where((item) {
if (item != null) return position >= item.start && position <= item.end;
return false;
}).toList();
return found;
}
}
class Subtitle {
Subtitle({
required this.index,
required this.start,
required this.end,
required this.text,
});
Subtitle copyWith({
int? index,
Duration? start,
Duration? end,
dynamic text,
}) {
return Subtitle(
index: index ?? this.index,
start: start ?? this.start,
end: end ?? this.end,
text: text ?? this.text,
);
}
final int index;
final Duration start;
final Duration end;
final dynamic text;
@override
String toString() {
return 'Subtitle(index: $index, start: $start, end: $end, text: $text)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Subtitle &&
other.index == index &&
other.start == start &&
other.end == end &&
other.text == text;
}
@override
int get hashCode {
return index.hashCode ^ start.hashCode ^ end.hashCode ^ text.hashCode;
}
}

View File

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

View File

@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
///
/// The new State-Manager for Chewie!
/// Has to be an instance of Singleton to survive
/// over all State-Changes inside chewie
///
class PlayerNotifier extends ChangeNotifier {
PlayerNotifier._(
bool hideStuff,
) : _hideStuff = hideStuff;
bool _hideStuff;
bool get hideStuff => _hideStuff;
set hideStuff(bool value) {
_hideStuff = value;
notifyListeners();
}
// ignore: prefer_constructors_over_static_methods
static PlayerNotifier init() {
return PlayerNotifier._(
true,
);
}
}

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
import '../chewie.dart';
import 'helpers/adaptive_controls.dart';
import 'notifiers/index.dart';
class PlayerWithControls extends StatelessWidget {
const PlayerWithControls({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final ChewieController chewieController = ChewieController.of(context);
double calculateAspectRatio(BuildContext context) {
final size = MediaQuery.of(context).size;
final width = size.width;
final height = size.height;
return width > height ? width / height : height / width;
}
Widget buildControls(
BuildContext context,
ChewieController chewieController,
) {
return chewieController.showControls
? chewieController.customControls ?? const AdaptiveControls()
: const SizedBox();
}
Widget buildPlayerWithControls(
ChewieController chewieController,
BuildContext context,
) {
return Stack(
children: <Widget>[
if (chewieController.placeholder != null)
chewieController.placeholder!,
InteractiveViewer(
transformationController: chewieController.transformationController,
maxScale: chewieController.maxScale,
panEnabled: chewieController.zoomAndPan,
scaleEnabled: chewieController.zoomAndPan,
child: Center(
child: AspectRatio(
aspectRatio:
chewieController.videoPlayerController.value.aspectRatio,
child: VideoPlayer(chewieController.videoPlayerController),
),
),
),
if (chewieController.overlay != null) chewieController.overlay!,
if (Theme.of(context).platform != TargetPlatform.iOS)
Consumer<PlayerNotifier>(
builder: (
BuildContext context,
PlayerNotifier notifier,
Widget? widget,
) =>
Visibility(
visible: !notifier.hideStuff,
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 0.8,
duration: const Duration(
milliseconds: 250,
),
child: const DecoratedBox(
decoration: BoxDecoration(color: Colors.black54),
child: SizedBox(),
),
),
),
),
if (!chewieController.isFullScreen)
buildControls(context, chewieController)
else
SafeArea(
bottom: false,
child: buildControls(context, chewieController),
),
],
);
}
return Center(
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: AspectRatio(
aspectRatio: calculateAspectRatio(context),
child: buildPlayerWithControls(chewieController, context),
),
),
);
}
}

View File

@@ -0,0 +1,218 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import '../chewie.dart';
class VideoProgressBar extends StatefulWidget {
VideoProgressBar(
this.controller, {
ChewieProgressColors? colors,
this.onDragEnd,
this.onDragStart,
this.onDragUpdate,
Key? key,
required this.barHeight,
required this.handleHeight,
required this.drawShadow,
}) : colors = colors ?? ChewieProgressColors(),
super(key: key);
final VideoPlayerController controller;
final ChewieProgressColors colors;
final Function()? onDragStart;
final Function()? onDragEnd;
final Function()? onDragUpdate;
final double barHeight;
final double handleHeight;
final bool drawShadow;
@override
// ignore: library_private_types_in_public_api
_VideoProgressBarState createState() {
return _VideoProgressBarState();
}
}
class _VideoProgressBarState extends State<VideoProgressBar> {
void listener() {
if (!mounted) return;
setState(() {});
}
bool _controllerWasPlaying = false;
VideoPlayerController get controller => widget.controller;
@override
void initState() {
super.initState();
controller.addListener(listener);
}
@override
void deactivate() {
controller.removeListener(listener);
super.deactivate();
}
void _seekToRelativePosition(Offset globalPosition) {
final box = context.findRenderObject()! as RenderBox;
final Offset tapPos = box.globalToLocal(globalPosition);
final double relative = tapPos.dx / box.size.width;
final Duration position = controller.value.duration * relative;
controller.seekTo(position);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onHorizontalDragStart: (DragStartDetails details) {
if (!controller.value.isInitialized) {
return;
}
_controllerWasPlaying = controller.value.isPlaying;
if (_controllerWasPlaying) {
controller.pause();
}
widget.onDragStart?.call();
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
if (!controller.value.isInitialized) {
return;
}
// Should only seek if it's not running on Android, or if it is,
// then the VideoPlayerController cannot be buffering.
// On Android, we need to let the player buffer when scrolling
// in order to let the player buffer. https://github.com/flutter/flutter/issues/101409
final shouldSeekToRelativePosition =
!Platform.isAndroid || !controller.value.isBuffering;
if (shouldSeekToRelativePosition) {
_seekToRelativePosition(details.globalPosition);
}
widget.onDragUpdate?.call();
},
onHorizontalDragEnd: (DragEndDetails details) {
if (_controllerWasPlaying) {
controller.play();
}
widget.onDragEnd?.call();
},
onTapDown: (TapDownDetails details) {
if (!controller.value.isInitialized) {
return;
}
_seekToRelativePosition(details.globalPosition);
},
child: Center(
child: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
color: Colors.transparent,
child: CustomPaint(
painter: _ProgressBarPainter(
value: controller.value,
colors: widget.colors,
barHeight: widget.barHeight,
handleHeight: widget.handleHeight,
drawShadow: widget.drawShadow,
),
),
),
),
);
}
}
class _ProgressBarPainter extends CustomPainter {
_ProgressBarPainter({
required this.value,
required this.colors,
required this.barHeight,
required this.handleHeight,
required this.drawShadow,
});
VideoPlayerValue value;
ChewieProgressColors colors;
final double barHeight;
final double handleHeight;
final bool drawShadow;
@override
bool shouldRepaint(CustomPainter painter) {
return true;
}
@override
void paint(Canvas canvas, Size size) {
final baseOffset = size.height / 2 - barHeight / 2;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(0.0, baseOffset),
Offset(size.width, baseOffset + barHeight),
),
const Radius.circular(4.0),
),
colors.backgroundPaint,
);
if (!value.isInitialized) {
return;
}
final double playedPartPercent =
value.position.inMilliseconds / value.duration.inMilliseconds;
final double playedPart =
playedPartPercent > 1 ? size.width : playedPartPercent * size.width;
for (final DurationRange range in value.buffered) {
final double start = range.startFraction(value.duration) * size.width;
final double end = range.endFraction(value.duration) * size.width;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(start, baseOffset),
Offset(end, baseOffset + barHeight),
),
const Radius.circular(4.0),
),
colors.bufferedPaint,
);
}
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(0.0, baseOffset),
Offset(playedPart, baseOffset + barHeight),
),
const Radius.circular(4.0),
),
colors.playedPaint,
);
if (drawShadow) {
final Path shadowPath = Path()
..addOval(
Rect.fromCircle(
center: Offset(playedPart, baseOffset + barHeight / 2),
radius: handleHeight,
),
);
canvas.drawShadow(shadowPath, Colors.black, 0.2, false);
}
canvas.drawCircle(
Offset(playedPart, baseOffset + barHeight / 2),
handleHeight,
colors.handlePaint,
);
}
}

View File

@@ -0,0 +1,89 @@
// import 'dart:async';
//
// import 'package:flutter/material.dart';
// import 'package:flutter_cached_pdfview/flutter_cached_pdfview.dart';
//
// class CustomerPDFPage extends StatefulWidget {
// final String filePath;
// const CustomerPDFPage({
// super.key,
// required this.filePath,
// });
//
// @override
// State<CustomerPDFPage> createState() => _CustomerPDFPageState();
// }
//
// class _CustomerPDFPageState extends State<CustomerPDFPage> {
// final StreamController<String> _pageCountController =
// StreamController<String>();
// final Completer<PDFViewController> _pdfViewController =
// Completer<PDFViewController>();
//
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// body: SafeArea(
// child: Stack(
// children: [
// PDF(
// onPageChanged: (int? current, int? total) =>
// _pageCountController.add('${current! + 1} - $total'),
// onViewCreated: (PDFViewController pdfViewController) async {
// _pdfViewController.complete(pdfViewController);
// final int currentPage =
// await pdfViewController.getCurrentPage() ?? 0;
// final int? pageCount = await pdfViewController.getPageCount();
// _pageCountController.add('${currentPage + 1} - $pageCount');
// },
// ).cachedFromUrl(
// widget.filePath,
// placeholder: (progress) => Center(child: Text('$progress %')),
// errorWidget: (error) => Center(child: Text(error.toString())),
// ),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// GestureDetector(
// onTap: () => Navigator.pop(context),
// child: Container(
// margin: const EdgeInsets.only(left: 16, top: 16),
// width: 44,
// height: 44,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(22),
// color: Colors.grey.withOpacity(0.3),
// ),
// child: const Icon(
// Icons.navigate_before,
// color: Colors.white,
// ),
// ),
// ),
// Container(
// margin: const EdgeInsets.only(right: 16, top: 16),
// width: 88,
// height: 44,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(22),
// color: Colors.grey.withOpacity(0.3),
// ),
// child: StreamBuilder<String>(
// stream: _pageCountController.stream,
// builder: (_, AsyncSnapshot<String> snapshot) {
// if (snapshot.hasData) {
// return Center(
// child: Text(snapshot.data!),
// );
// }
// return const SizedBox();
// }),
// ),
// ],
// ),
// ],
// ),
// ),
// );
// }
// }

View File

@@ -0,0 +1,149 @@
import 'dart:collection';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class CustomerWebView extends StatefulWidget {
final String url;
final bool? hideBack;
const CustomerWebView({
super.key,
required this.url,
this.hideBack,
});
@override
State<CustomerWebView> createState() => _CustomerWebViewState();
}
class _CustomerWebViewState extends State<CustomerWebView> {
final GlobalKey webViewKey = GlobalKey();
InAppWebViewController? webViewController;
InAppWebViewSettings settings = InAppWebViewSettings(
isInspectable: kDebugMode,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
iframeAllow: "camera; microphone",
iframeAllowFullscreen: true);
@override
void initState() {
super.initState();
webViewController?.loadUrl(urlRequest: URLRequest(url: WebUri(widget.url)));
}
@override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: [SystemUiOverlay.bottom]);
return Stack(
alignment: Alignment.topLeft,
children: [
InAppWebView(
key: webViewKey,
// webViewEnvironment: webViewEnvironment,
initialUrlRequest: URLRequest(url: WebUri(widget.url)),
// initialUrlRequest:
// URLRequest(url: WebUri(Uri.base.toString().replaceFirst("/#/", "/") + 'page.html')),
// initialFile: "assets/index.html",
initialUserScripts: UnmodifiableListView<UserScript>([]),
initialSettings: settings,
// contextMenu: contextMenu,
// pullToRefreshController: pullToRefreshController,
onWebViewCreated: (controller) async {
webViewController = controller;
},
onLoadStart: (controller, url) {
setState(() {
// this.url = url.toString();
// urlController.text = this.url;
});
},
// onPermissionRequest: (controller, request) {
// return PermissionResponse(
// resources: request.resources,
// action: PermissionResponseAction.GRANT);
// },
// shouldOverrideUrlLoading:
// (controller, navigationAction) async {
// var uri = navigationAction.request.url!;
//
// if (![
// "http",
// "https",
// "file",
// "chrome",
// "data",
// "javascript",
// "about"
// ].contains(uri.scheme)) {
// if (await canLaunchUrl(uri)) {
// // Launch the App
// await launchUrl(
// uri,
// );
// // and cancel the request
// return NavigationActionPolicy.CANCEL;
// }
// }
//
// return NavigationActionPolicy.ALLOW;
// },
// onLoadStop: (controller, url) {
// pullToRefreshController?.endRefreshing();
// setState(() {
// this.url = url.toString();
// urlController.text = this.url;
// });
// },
// onReceivedError: (controller, request, error) {
// pullToRefreshController?.endRefreshing();
// },
// onProgressChanged: (controller, progress) {
// if (progress == 100) {
// pullToRefreshController?.endRefreshing();
// }
// setState(() {
// this.progress = progress / 100;
// urlController.text = this.url;
// });
// },
// onUpdateVisitedHistory: (controller, url, isReload) {
// setState(() {
// this.url = url.toString();
// urlController.text = this.url;
// });
// },
onConsoleMessage: (controller, consoleMessage) {
print(consoleMessage);
},
),
Visibility(
visible: widget.hideBack != true,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
margin: const EdgeInsets.only(left: 16, top: 16),
width: 44,
height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
color: Colors.grey.withOpacity(0.3),
),
child: const Icon(
Icons.navigate_before,
color: Colors.white,
),
),
),
)
],
);
}
}

View File

@@ -0,0 +1,53 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class FrostedGlassEffectWidget extends StatelessWidget {
final Widget? child;
final double? width;
final double? height;
final double? borderRadius;
final Widget? backgroundChild;
final Color? backgroundColor;
const FrostedGlassEffectWidget({
super.key,
this.child,
this.width,
this.height,
this.borderRadius,
this.backgroundChild,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: Stack(
children: [
// 背景内容(可以放图片或其他内容)
backgroundChild ?? SizedBox(),
// 毛玻璃效果
ClipRRect(
borderRadius: BorderRadius.circular(borderRadius ?? 20.h),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10.0,
sigmaY: 10.0,
),
child: Container(
decoration: BoxDecoration(
color: backgroundColor ?? Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(borderRadius ?? 20.h),
),
child: child,
),
),
),
],
),
);
}
}

42
lib/utils/launch_utils.dart Executable file
View File

@@ -0,0 +1,42 @@
import 'package:url_launcher/url_launcher.dart';
enum LaunchType { tel, sms, email, link }
const launchTypeValues = {
LaunchType.tel: "tel:",
LaunchType.sms: "sms:",
LaunchType.email: "mailto:",
LaunchType.link: ''
};
class LaunchUtils {
///自定义Launch方法
static Future<bool> customLaunch({
required String urlString,
LaunchType launchType = LaunchType.link,
bool enableJavaScript = false,
bool enableDomStorage = false,
Map<String, String> headers = const <String, String>{},
LaunchMode mode = LaunchMode.externalApplication,
String? webOnlyWindowName,
}) {
return launchUrl(
Uri.parse('${launchTypeValues[launchType]}$urlString'),
webViewConfiguration: WebViewConfiguration(
enableJavaScript: enableJavaScript,
enableDomStorage: enableDomStorage,
headers: headers,
),
mode: mode,
webOnlyWindowName: webOnlyWindowName,
);
}
///判断是否打开链接
static Future<bool> customCanLaunch(
String urlString, {
LaunchType launchType = LaunchType.link,
}) async {
return canLaunchUrl(Uri.parse('${launchTypeValues[launchType]}$urlString'));
}
}

View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
///封装刷新加载pull_to_refresh控件
class PullRefreshListWidget extends StatelessWidget {
final RefreshController controller;
// final NullableIndexedWidgetBuilder itemBuilder;
// final int itemCount;
final bool enablePullUp;
final bool enablePullDown;
final bool shrinkWrap;
final VoidCallback? onRefresh;
final VoidCallback? onLoading;
// final EdgeInsetsGeometry? padding;
final Widget? header;
final Widget? footer;
final Widget? placeholder;
// final ScrollPhysics? physics;
final bool isWhiteTheme;
final Widget? child;
const PullRefreshListWidget({
Key? key,
required this.controller,
// required this.itemBuilder,
// required this.itemCount,
this.enablePullUp = false,
this.enablePullDown = true,
this.shrinkWrap = true,
this.onRefresh,
this.onLoading,
// this.padding,
this.header,
this.footer,
this.placeholder,
// this.physics,
this.isWhiteTheme = false,
this.child,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SmartRefresher(
header: header ??
ClassicHeader(
releaseText: '松开刷新',
refreshingText: '正在刷新...',
completeText: '刷新成功',
failedText: '刷新失败',
idleText: '下拉刷新',
textStyle:
TextStyle(color: isWhiteTheme ? Colors.white : Colors.grey),
idleIcon: Icon(Icons.arrow_downward,
color: isWhiteTheme ? Colors.white : Colors.grey),
failedIcon: Icon(Icons.error,
color: isWhiteTheme ? Colors.white : Colors.grey),
completeIcon: Icon(Icons.done,
color: isWhiteTheme ? Colors.white : Colors.grey),
releaseIcon: Icon(Icons.refresh,
color: isWhiteTheme ? Colors.white : Colors.grey),
),
footer: footer ??
ClassicFooter(
loadingText: '正在加载...',
noDataText: '没有更多数据',
idleText: '加载更多',
failedText: '加载失败',
canLoadingText: '松开加载更多',
textStyle:
TextStyle(color: isWhiteTheme ? Colors.white : Colors.grey),
idleIcon: Icon(Icons.arrow_downward,
color: isWhiteTheme ? Colors.white : Colors.grey),
failedIcon: Icon(Icons.error,
color: isWhiteTheme ? Colors.white : Colors.grey),
),
enablePullDown: enablePullDown,
enablePullUp: enablePullUp,
controller: controller,
onRefresh: onRefresh,
onLoading: onLoading,
child: child ??
Column(
children: [
Expanded(
child: Container(
child: placeholder ?? const Text('暂无相关数据'),
),
),
],
),
);
}
}

134
lib/utils/string_utils.dart Executable file
View File

@@ -0,0 +1,134 @@
// import 'package:intl/intl.dart';
extension StringUtils on String? {
String get hidePhone => StringUtils._phoneString(phone: this);
String get hideUserName => StringUtils._userNameString(name: this);
bool get isNumeric => StringUtils._isNumeric(str: this);
String get toPrice => StringUtils._cutOutPrice(price: this);
String get toSplMoney => StringUtils._splNumberInsyo(str: this);
String get toThousandPrice => StringUtils._formatThousandPrice(price: this);
double get toDouble => StringUtils._strToDouble(str: this);
///隐藏手机号中间4位数
static String _phoneString({String? phone}) {
if (phone == null || phone == '') return '';
return phone.replaceFirst(RegExp(r'\d{4}'), '****', 3);
}
///隐藏姓名(只显示姓 隐藏名字)
static String _userNameString({String? name}) {
if (name == null || name == '') return '';
return name.replaceAll(name.substring(1, name.length), '**');
}
///判断是否是纯数字
static bool _isNumeric({String? str}) {
if (str == null) {
return false;
}
return double.tryParse(str) != null;
}
///将金额转换为小数点后两位
static String _cutOutPrice({String? price}) {
String cutOutPrice = '0.00';
if (price != null && price != '') {
var newPrice = double.tryParse(price);
if (newPrice != null) {
cutOutPrice = newPrice.toStringAsFixed(2);
}
}
return cutOutPrice;
}
///金额处理千分位金额
static String _formatThousandPrice({String? price}) {
String priceStr = price ?? '';
try {
num? money = num.tryParse(priceStr);
int truncateMoney = money?.truncate() ?? 0;
if (truncateMoney >= 1000) {
// NumberFormat format = NumberFormat('0,000');
return _formatNum(truncateMoney);
} else {
List<String> resultList = priceStr.split(".");
if (resultList.isNotEmpty) {
return priceStr.split(".").first;
} else {
return priceStr;
}
}
} catch (error) {
return '';
}
}
static String _formatNum(num, {point = 3}) {
if (num != null) {
String str = double.parse(num.toString()).toString();
// 分开截取
List<String> sub = str.split('.');
// 处理值
List val = List.from(sub[0].split(''));
// 处理点
List<String> points = List.from(sub[1].split(''));
//处理分割符
for (int index = 0, i = val.length - 1; i >= 0; index++, i--) {
// 除以三没有余数、不等于零并且不等于1 就加个逗号
if (index % 3 == 0 && index != 0 && i != 1) val[i] = val[i] + ',';
}
// 处理小数点
for (int i = 0; i <= point - points.length; i++) {
points.add('0');
}
//如果大于长度就截取
if (points.length > point) {
// 截取数组
points = points.sublist(0, point);
}
// 判断是否有长度
if (points.length > 0) {
return '${val.join('')}.${points.join('')}';
} else {
return val.join('');
}
} else {
return "0.0";
}
}
///将数字转换位千分位隔开
static String _splNumberInsyo({String? str}) {
if (str?.isEmpty == true) {
return '';
}
// num amount = num.parse(str ?? '0');
// final NumberFormat formatter = NumberFormat.currency(
// locale: 'en_US', // 设置为你的本地化Locale
// decimalDigits: 0,
// name: '', // 设置货币符号,例如 'USD' 或 'EUR'
// );
// return formatter.format(amount);
return str ?? '';
}
///将字符串转为double
static double _strToDouble({String? str}) {
String newStr = str ?? '';
try {
double? doubleValue = double.tryParse(newStr);
if (doubleValue != null) {
return doubleValue;
}
return 0;
} catch (error) {
return 0;
}
}
}

220
lib/utils/toast_utils.dart Executable file
View File

@@ -0,0 +1,220 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/custom_dialog.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
// import 'package:get/get.dart';
class ToastUtils {
///提示框
static showToast({
String? msg,
Color? backgroundColor,
TextStyle? textStyle = const TextStyle(color: Colors.white),
}) async {
EasyLoading.showToast(msg ?? '');
// _cancelToast();
}
///加载框
static showLoading({String? text}) async {
await EasyLoading.show(
// status: 'loading...',
maskType: EasyLoadingMaskType.black,
);
}
///成功弹窗提示
static successToast({String? successText}) {
EasyLoading.showError(
successText ?? '成功',
duration: const Duration(seconds: 2),
);
cancelToast();
}
///失败弹窗提示
static errorToast({String? errorText}) {
EasyLoading.showError(
errorText ?? '错误',
duration: const Duration(seconds: 2),
);
cancelToast();
}
///底部自适应高度弹窗
///底部弹窗
static showBottomSheet({
required BuildContext context,
bool isTime = false,
double? height,
Function? onConfirm,
String? title,
double? titleFontSize,
double? leftIconSize,
Widget? contentWidget,
Widget? header,
bool isShowConfirm = false,
Color? barrierColor,
EdgeInsetsGeometry? padding,
}) {
cancelToast();
return showDialog(
context: context,
builder: (BuildContext ctx) {
return Container(
width: double.infinity,
height: MediaQuery.of(context).size.height / 2,
margin: EdgeInsets.only(
top: height == null
? MediaQuery.of(context).size.height / 2
: (MediaQuery.of(context).size.height - height),
),
padding: padding ?? const EdgeInsets.only(bottom: 40),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Column(
children: [
header ??
Container(
padding: const EdgeInsets.only(bottom: 5),
decoration: const BoxDecoration(
border: Border(
bottom:
BorderSide(color: Color(0xffE1E1E1), width: 0.5),
),
),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
padding:
const EdgeInsets.only(left: 6, right: 10),
color: Colors.transparent,
child: Icon(
Icons.keyboard_arrow_down_rounded,
size: leftIconSize ?? 40,
),
),
),
Expanded(
child: Container(
alignment: Alignment.center,
child: Text(
title ?? '头部',
style: TextStyle(
color: const Color(0xff333333),
fontSize: titleFontSize ?? 18,
fontWeight: FontWeight.bold,
),
),
),
),
GestureDetector(
onTap: () {
if (isShowConfirm) {
if (onConfirm != null) {
onConfirm();
Navigator.pop(context);
}
}
},
child: Container(
padding: const EdgeInsets.only(
left: 10,
top: 8,
bottom: 8,
right: 18,
),
alignment: Alignment.center,
color: Colors.transparent,
child: Text(
'确定',
style: TextStyle(
color: isShowConfirm
? const Color(0xff4D6FD5)
: Colors.transparent,
fontSize: 16),
),
),
)
],
),
),
Expanded(child: contentWidget ?? const SizedBox())
],
),
);
});
}
static cancelToast() {
// 延时2秒
EasyLoading.dismiss();
}
///显示对话框
static showAlterDialog({
VoidCallback? confirmCallback,
VoidCallback? cancelCallback,
String? titleText,
String? contentText,
String? confirmText,
TextStyle? confirmStyle,
TextStyle? cancelStyle,
}) {
cancelToast();
return Get.dialog(
CustomDialog(
title: titleText ?? '温馨提示',
content: contentText ?? '您确定要退出当前登录吗?',
cancelText: "取消",
confirmText: "确定",
cancelTextStyle: cancelStyle,
confirmTextStyle: confirmStyle,
cancelCall: () {
Get.back();
Future.delayed(const Duration(milliseconds: 50)).then((value) {
if (cancelCallback != null) {
cancelCallback();
}
});
},
confirmCall: () {
Get.back();
if (confirmCallback != null) {
confirmCallback();
}
},
),
);
}
///错误信息弹窗
static showExceptionToast({String? title, String? msg}) {
_cancelToast();
return Get.snackbar(
title ?? '错误信息',
msg ?? '错误信息内容',
snackPosition: SnackPosition.BOTTOM,
colorText: Colors.white,
backgroundColor: Colors.red[800],
margin: const EdgeInsets.only(bottom: 10, left: 10, right: 10),
borderRadius: 4,
duration: const Duration(seconds: 3),
);
}
///取消弹窗
static _cancelToast() {
// Toast.dismissAllToast();
}
}