develop_sky #1
54
lib/blue/io_protocol/io_readVoicePackageFinalResult.dart
Normal file
54
lib/blue/io_protocol/io_readVoicePackageFinalResult.dart
Normal file
@ -0,0 +1,54 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
|
||||
import '../io_reply.dart';
|
||||
import '../io_sender.dart';
|
||||
import '../io_tool/io_tool.dart';
|
||||
import '../io_type.dart';
|
||||
import '../sm4Encipher/sm4.dart';
|
||||
|
||||
//oat升级
|
||||
class ReadLockCurrentVoicePacket extends SenderProtocol {
|
||||
ReadLockCurrentVoicePacket({
|
||||
this.lockID,
|
||||
}) : super(CommandType.readLockCurrentVoicePacket);
|
||||
String? lockID;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ReadLockCurrentVoicePacket{lockID: $lockID}';
|
||||
}
|
||||
|
||||
@override
|
||||
List<int> messageDetail() {
|
||||
List<int> data = <int>[];
|
||||
|
||||
// 指令类型
|
||||
final int type = commandType!.typeValue;
|
||||
final double typeDouble = type / 256;
|
||||
final int type1 = typeDouble.toInt();
|
||||
final int type2 = type % 256;
|
||||
data.add(type1);
|
||||
data.add(type2);
|
||||
|
||||
// 锁id 40
|
||||
final int lockIDLength = utf8.encode(lockID!).length;
|
||||
data.addAll(utf8.encode(lockID!));
|
||||
data = getFixedLengthList(data, 40 - lockIDLength);
|
||||
|
||||
printLog(data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class ReadLockCurrentVoicePacketReply extends Reply {
|
||||
ReadLockCurrentVoicePacketReply.parseData(
|
||||
CommandType commandType, List<int> dataDetail)
|
||||
: super.parseData(commandType, dataDetail) {
|
||||
data = dataDetail;
|
||||
status = data[6];
|
||||
errorWithStstus(status);
|
||||
}
|
||||
}
|
||||
61
lib/blue/io_protocol/io_setVoicePackageFinalResult.dart
Normal file
61
lib/blue/io_protocol/io_setVoicePackageFinalResult.dart
Normal file
@ -0,0 +1,61 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
|
||||
import '../io_reply.dart';
|
||||
import '../io_sender.dart';
|
||||
import '../io_tool/io_tool.dart';
|
||||
import '../io_type.dart';
|
||||
import '../sm4Encipher/sm4.dart';
|
||||
|
||||
//oat升级
|
||||
class SetVoicePackageFinalResult extends SenderProtocol {
|
||||
SetVoicePackageFinalResult({
|
||||
this.lockID,
|
||||
this.languageCode,
|
||||
}) : super(CommandType.setLockCurrentVoicePacket);
|
||||
String? lockID;
|
||||
String? languageCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SetVoicePackageFinalResult{lockID: $lockID, languageCode: $languageCode}';
|
||||
}
|
||||
|
||||
@override
|
||||
List<int> messageDetail() {
|
||||
List<int> data = <int>[];
|
||||
|
||||
// 指令类型
|
||||
final int type = commandType!.typeValue;
|
||||
final double typeDouble = type / 256;
|
||||
final int type1 = typeDouble.toInt();
|
||||
final int type2 = type % 256;
|
||||
data.add(type1);
|
||||
data.add(type2);
|
||||
|
||||
// 锁id 40
|
||||
final int lockIDLength = utf8.encode(lockID!).length;
|
||||
data.addAll(utf8.encode(lockID!));
|
||||
data = getFixedLengthList(data, 40 - lockIDLength);
|
||||
|
||||
//languageCode 20
|
||||
final int languageCodeLength = utf8.encode(languageCode!).length;
|
||||
data.addAll(utf8.encode(languageCode!));
|
||||
data = getFixedLengthList(data, 20 - languageCodeLength);
|
||||
|
||||
printLog(data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class SetVoicePackageFinalResultReply extends Reply {
|
||||
SetVoicePackageFinalResultReply.parseData(
|
||||
CommandType commandType, List<int> dataDetail)
|
||||
: super.parseData(commandType, dataDetail) {
|
||||
data = dataDetail;
|
||||
status = data[6];
|
||||
errorWithStstus(status);
|
||||
}
|
||||
}
|
||||
@ -44,7 +44,9 @@ enum CommandType {
|
||||
startVoicePackageConfigure, //语音包配置开始 0x30A1
|
||||
voicePackageConfigureProcess, //语音包配置过程 0x30A2
|
||||
voicePackageConfigureConfirmation, //语音包配置确认 0x30A3
|
||||
getDeviceModel, //获取设备型号 0x30A4
|
||||
readLockCurrentVoicePacket, //读取锁当前语音包 0x30A4
|
||||
setLockCurrentVoicePacket, //设置锁当前语音包 0x30A5
|
||||
getDeviceModel, //读取设备型号 (已废弃)0x30A4
|
||||
|
||||
gatewayConfiguringWifi, //网关配网 0x30F4
|
||||
gatewayConfiguringWifiResult, //网关配网结果 0x30F5
|
||||
@ -210,7 +212,12 @@ extension ExtensionCommandType on CommandType {
|
||||
break;
|
||||
case 0x30A4:
|
||||
{
|
||||
type = CommandType.getDeviceModel;
|
||||
type = CommandType.readLockCurrentVoicePacket;
|
||||
}
|
||||
break;
|
||||
case 0x30A5:
|
||||
{
|
||||
type = CommandType.setLockCurrentVoicePacket;
|
||||
}
|
||||
break;
|
||||
case 0x30F4:
|
||||
@ -340,9 +347,12 @@ extension ExtensionCommandType on CommandType {
|
||||
case CommandType.voicePackageConfigureConfirmation:
|
||||
type = 0x30A3;
|
||||
break;
|
||||
case CommandType.getDeviceModel:
|
||||
case CommandType.readLockCurrentVoicePacket:
|
||||
type = 0x30A4;
|
||||
break;
|
||||
case CommandType.setLockCurrentVoicePacket:
|
||||
type = 0x30A5;
|
||||
break;
|
||||
default:
|
||||
type = 0x300A;
|
||||
break;
|
||||
@ -362,7 +372,8 @@ extension ExtensionCommandType on CommandType {
|
||||
case CommandType.gatewayGetWifiList:
|
||||
case CommandType.gatewayConfiguringWifi:
|
||||
case CommandType.gatewayGetStatus:
|
||||
case CommandType.getDeviceModel:
|
||||
case CommandType.readLockCurrentVoicePacket:
|
||||
case CommandType.setLockCurrentVoicePacket:
|
||||
//不加密
|
||||
type = 0x20;
|
||||
break;
|
||||
@ -476,7 +487,10 @@ extension ExtensionCommandType on CommandType {
|
||||
t = '语音包配置确认';
|
||||
break;
|
||||
case 0x30A4:
|
||||
t = '获取设备型号';
|
||||
t = '读取锁当前语音包';
|
||||
break;
|
||||
case 0x30A5:
|
||||
t = '设置锁当前语音包';
|
||||
break;
|
||||
default:
|
||||
t = '读星锁状态信息';
|
||||
|
||||
@ -80,231 +80,236 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: EdgeInsets.only(top: 120.h, left: 40.w, right: 40.w),
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.all(10.w),
|
||||
child: Center(
|
||||
child: Image.asset('images/icon_main_sky_1024.png',
|
||||
width: 110.w, height: 110.w))),
|
||||
SizedBox(height: 50.w),
|
||||
Obx(() => CommonItem(
|
||||
leftTitel: '你所在的国家/地区'.tr,
|
||||
rightTitle: '',
|
||||
isHaveLine: true,
|
||||
isPadding: false,
|
||||
isHaveRightWidget: true,
|
||||
isHaveDirection: true,
|
||||
rightWidget: Text(
|
||||
'${state.countryName} +${state.countryCode.value}',
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp, color: AppColors.darkGrayTextColor),
|
||||
),
|
||||
action: () async {
|
||||
final result =
|
||||
await Get.toNamed(Routers.selectCountryRegionPage);
|
||||
if (result != null) {
|
||||
result as Map<String, dynamic>;
|
||||
state.countryCode.value = result['code'];
|
||||
state.countryKey.value = result['countryName'];
|
||||
logic.checkIpAction();
|
||||
}
|
||||
},
|
||||
)),
|
||||
LoginInput(
|
||||
focusNode: logic.state.emailOrPhoneFocusNode,
|
||||
controller: state.emailOrPhoneController,
|
||||
onchangeAction: (v) {
|
||||
logic.checkNext(state.emailOrPhoneController);
|
||||
},
|
||||
leftWidget: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 30.w, bottom: 20.w, right: 5.w, left: 5.w),
|
||||
child: Image.asset(
|
||||
'images/icon_login_account.png',
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
),
|
||||
),
|
||||
hintText: '请输入手机号或者邮箱'.tr,
|
||||
// keyboardType: TextInputType.number,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
// FilteringTextInputFormatter.allow(RegExp('[0-9]')),
|
||||
LengthLimitingTextInputFormatter(30),
|
||||
FilteringTextInputFormatter.singleLineFormatter
|
||||
]),
|
||||
SizedBox(height: 10.h),
|
||||
LoginInput(
|
||||
focusNode: logic.state.pwdFocusNode,
|
||||
controller: state.pwdController,
|
||||
onchangeAction: (v) {
|
||||
logic.checkNext(state.pwdController);
|
||||
},
|
||||
isPwd: true,
|
||||
// isSuffixIcon: 2,
|
||||
leftWidget: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 30.w, bottom: 20.w, right: 5.w, left: 5.w),
|
||||
child: Image.asset(
|
||||
'images/icon_login_password.png',
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
),
|
||||
),
|
||||
hintText: '请输入密码'.tr,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
LengthLimitingTextInputFormatter(20),
|
||||
]),
|
||||
// SizedBox(height: 15.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Obx(() => GestureDetector(
|
||||
onTap: () {
|
||||
state.agree.value = !state.agree.value;
|
||||
logic.changeAgreeState();
|
||||
},
|
||||
child: Container(
|
||||
// color: Colors.red,
|
||||
padding: EdgeInsets.only(
|
||||
left: 5.w, top: 20.w, right: 10.w, bottom: 20.h),
|
||||
child: Image.asset(
|
||||
state.agree.value
|
||||
? 'images/icon_round_select.png'
|
||||
: 'images/icon_round_unSelect.png',
|
||||
width: 35.w,
|
||||
height: 35.w,
|
||||
),
|
||||
))),
|
||||
// SizedBox(
|
||||
// width: 5.w,
|
||||
// ),
|
||||
Flexible(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: '我已阅读并同意'.tr,
|
||||
style: TextStyle(
|
||||
color: const Color(0xff333333), fontSize: 20.sp),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
child: Text('《${'用户协议'.tr}》',
|
||||
style: TextStyle(
|
||||
color: AppColors.mainColor,
|
||||
fontSize: 20.sp)),
|
||||
onTap: () {
|
||||
Get.toNamed(Routers.webviewShowPage,
|
||||
arguments: <String, String>{
|
||||
'url': XSConstantMacro.userAgreementURL,
|
||||
'title': '用户协议'.tr
|
||||
});
|
||||
},
|
||||
)),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
child: Text('《${'隐私政策'.tr}》',
|
||||
style: TextStyle(
|
||||
color: AppColors.mainColor,
|
||||
fontSize: 20.sp)),
|
||||
onTap: () {
|
||||
Get.toNamed(Routers.webviewShowPage,
|
||||
arguments: <String, String>{
|
||||
'url': XSConstantMacro.privacyPolicyURL,
|
||||
'title': '隐私政策'.tr
|
||||
});
|
||||
},
|
||||
)),
|
||||
],
|
||||
)),
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 50.w),
|
||||
Obx(() => SubmitBtn(
|
||||
btnName: '登录'.tr,
|
||||
fontSize: 28.sp,
|
||||
borderRadius: 20.w,
|
||||
padding: EdgeInsets.only(top: 25.w, bottom: 25.w),
|
||||
isDisabled: state.canNext.value,
|
||||
onClick: state.canNext.value
|
||||
? () {
|
||||
if (state.agree.value == false) {
|
||||
logic.showToast('请先同意用户协议及隐私政策'.tr);
|
||||
return;
|
||||
} else {
|
||||
logic.login();
|
||||
}
|
||||
body: GestureDetector(
|
||||
onTap: (){
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: ListView(
|
||||
padding: EdgeInsets.only(top: 120.h, left: 40.w, right: 40.w),
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.all(10.w),
|
||||
child: Center(
|
||||
child: Image.asset('images/icon_main_sky_1024.png',
|
||||
width: 110.w, height: 110.w))),
|
||||
SizedBox(height: 50.w),
|
||||
Obx(() => CommonItem(
|
||||
leftTitel: '你所在的国家/地区'.tr,
|
||||
rightTitle: '',
|
||||
isHaveLine: true,
|
||||
isPadding: false,
|
||||
isHaveRightWidget: true,
|
||||
isHaveDirection: true,
|
||||
rightWidget: Text(
|
||||
'${state.countryName} +${state.countryCode.value}',
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp, color: AppColors.darkGrayTextColor),
|
||||
),
|
||||
action: () async {
|
||||
final result =
|
||||
await Get.toNamed(Routers.selectCountryRegionPage);
|
||||
if (result != null) {
|
||||
result as Map<String, dynamic>;
|
||||
state.countryCode.value = result['code'];
|
||||
state.countryKey.value = result['countryName'];
|
||||
logic.checkIpAction();
|
||||
}
|
||||
: null)),
|
||||
// SizedBox(height: 20.w),
|
||||
// Obx(() => Visibility(
|
||||
// visible: state.isCheckVerifyEnable.value,
|
||||
// child: SubmitBtn(
|
||||
// btnName: '一键登录',
|
||||
// fontSize: 28.sp,
|
||||
// borderRadius: 20.w,
|
||||
// padding: EdgeInsets.only(top: 25.w, bottom: 25.w),
|
||||
// // isDisabled: state.canNext.value,
|
||||
// onClick: () {
|
||||
// if (state.agree.value == false) {
|
||||
// logic.showToast('请先同意用户协议及隐私政策'.tr);
|
||||
// return;
|
||||
// } else {
|
||||
// logic.oneClickLoginAction();
|
||||
// }
|
||||
// }),
|
||||
// )),
|
||||
SizedBox(height: 50.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
child: SizedBox(
|
||||
// width: 150.w,
|
||||
height: 50.h,
|
||||
// color: Colors.red,
|
||||
child: Center(
|
||||
child: Text('${'忘记密码'.tr}?',
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp, color: AppColors.mainColor)),
|
||||
},
|
||||
)),
|
||||
LoginInput(
|
||||
focusNode: logic.state.emailOrPhoneFocusNode,
|
||||
controller: state.emailOrPhoneController,
|
||||
onchangeAction: (v) {
|
||||
logic.checkNext(state.emailOrPhoneController);
|
||||
},
|
||||
leftWidget: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 30.w, bottom: 20.w, right: 5.w, left: 5.w),
|
||||
child: Image.asset(
|
||||
'images/icon_login_account.png',
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pushNamed(
|
||||
context, Routers.starLockForgetPasswordPage);
|
||||
hintText: '请输入手机号或者邮箱'.tr,
|
||||
// keyboardType: TextInputType.number,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
// FilteringTextInputFormatter.allow(RegExp('[0-9]')),
|
||||
LengthLimitingTextInputFormatter(30),
|
||||
FilteringTextInputFormatter.singleLineFormatter
|
||||
]),
|
||||
SizedBox(height: 10.h),
|
||||
LoginInput(
|
||||
focusNode: logic.state.pwdFocusNode,
|
||||
controller: state.pwdController,
|
||||
onchangeAction: (v) {
|
||||
logic.checkNext(state.pwdController);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
width: 10.sp,
|
||||
)),
|
||||
Obx(() => Visibility(
|
||||
visible: state.isCheckVerifyEnable.value &&
|
||||
state.currentLanguage == 'zh_CN',
|
||||
child: GestureDetector(
|
||||
child: SizedBox(
|
||||
// width: 150.w,
|
||||
height: 50.h,
|
||||
// color: Colors.red,
|
||||
child: Center(
|
||||
child: Text('一键登录'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
color: AppColors.mainColor)),
|
||||
),
|
||||
isPwd: true,
|
||||
// isSuffixIcon: 2,
|
||||
leftWidget: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 30.w, bottom: 20.w, right: 5.w, left: 5.w),
|
||||
child: Image.asset(
|
||||
'images/icon_login_password.png',
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
),
|
||||
),
|
||||
hintText: '请输入密码'.tr,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
LengthLimitingTextInputFormatter(20),
|
||||
]),
|
||||
// SizedBox(height: 15.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Obx(() => GestureDetector(
|
||||
onTap: () {
|
||||
state.agree.value = !state.agree.value;
|
||||
logic.changeAgreeState();
|
||||
},
|
||||
child: Container(
|
||||
// color: Colors.red,
|
||||
padding: EdgeInsets.only(
|
||||
left: 5.w, top: 20.w, right: 10.w, bottom: 20.h),
|
||||
child: Image.asset(
|
||||
state.agree.value
|
||||
? 'images/icon_round_select.png'
|
||||
: 'images/icon_round_unSelect.png',
|
||||
width: 35.w,
|
||||
height: 35.w,
|
||||
),
|
||||
onTap: () {
|
||||
logic.oneClickLoginAction(context);
|
||||
},
|
||||
))),
|
||||
// SizedBox(
|
||||
// width: 5.w,
|
||||
// ),
|
||||
Flexible(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: '我已阅读并同意'.tr,
|
||||
style: TextStyle(
|
||||
color: const Color(0xff333333), fontSize: 20.sp),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
child: Text('《${'用户协议'.tr}》',
|
||||
style: TextStyle(
|
||||
color: AppColors.mainColor,
|
||||
fontSize: 20.sp)),
|
||||
onTap: () {
|
||||
Get.toNamed(Routers.webviewShowPage,
|
||||
arguments: <String, String>{
|
||||
'url': XSConstantMacro.userAgreementURL,
|
||||
'title': '用户协议'.tr
|
||||
});
|
||||
},
|
||||
)),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
child: Text('《${'隐私政策'.tr}》',
|
||||
style: TextStyle(
|
||||
color: AppColors.mainColor,
|
||||
fontSize: 20.sp)),
|
||||
onTap: () {
|
||||
Get.toNamed(Routers.webviewShowPage,
|
||||
arguments: <String, String>{
|
||||
'url': XSConstantMacro.privacyPolicyURL,
|
||||
'title': '隐私政策'.tr
|
||||
});
|
||||
},
|
||||
)),
|
||||
],
|
||||
)),
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 50.w),
|
||||
Obx(() => SubmitBtn(
|
||||
btnName: '登录'.tr,
|
||||
fontSize: 28.sp,
|
||||
borderRadius: 20.w,
|
||||
padding: EdgeInsets.only(top: 25.w, bottom: 25.w),
|
||||
isDisabled: state.canNext.value,
|
||||
onClick: state.canNext.value
|
||||
? () {
|
||||
if (state.agree.value == false) {
|
||||
logic.showToast('请先同意用户协议及隐私政策'.tr);
|
||||
return;
|
||||
} else {
|
||||
logic.login();
|
||||
}
|
||||
}
|
||||
: null)),
|
||||
// SizedBox(height: 20.w),
|
||||
// Obx(() => Visibility(
|
||||
// visible: state.isCheckVerifyEnable.value,
|
||||
// child: SubmitBtn(
|
||||
// btnName: '一键登录',
|
||||
// fontSize: 28.sp,
|
||||
// borderRadius: 20.w,
|
||||
// padding: EdgeInsets.only(top: 25.w, bottom: 25.w),
|
||||
// // isDisabled: state.canNext.value,
|
||||
// onClick: () {
|
||||
// if (state.agree.value == false) {
|
||||
// logic.showToast('请先同意用户协议及隐私政策'.tr);
|
||||
// return;
|
||||
// } else {
|
||||
// logic.oneClickLoginAction();
|
||||
// }
|
||||
// }),
|
||||
// )),
|
||||
SizedBox(height: 50.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
child: SizedBox(
|
||||
// width: 150.w,
|
||||
height: 50.h,
|
||||
// color: Colors.red,
|
||||
child: Center(
|
||||
child: Text('${'忘记密码'.tr}?',
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp, color: AppColors.mainColor)),
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pushNamed(
|
||||
context, Routers.starLockForgetPasswordPage);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
width: 10.sp,
|
||||
)),
|
||||
Obx(() => Visibility(
|
||||
visible: state.isCheckVerifyEnable.value &&
|
||||
state.currentLanguage == 'zh_CN',
|
||||
child: GestureDetector(
|
||||
child: SizedBox(
|
||||
// width: 150.w,
|
||||
height: 50.h,
|
||||
// color: Colors.red,
|
||||
child: Center(
|
||||
child: Text('一键登录'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
color: AppColors.mainColor)),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
logic.oneClickLoginAction(context);
|
||||
},
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@ -62,10 +62,8 @@ FutureOr<void> main() async {
|
||||
}
|
||||
});
|
||||
|
||||
// //ToDo: 增加对讲调试、正式可删除
|
||||
// runApp(MultiProvider(providers: [
|
||||
// ChangeNotifierProvider(create: (_) => DebugInfoModel()),
|
||||
// ], child: MyApp(isLogin: isLogin)));
|
||||
// 如果是ios则初始化获取到voip token
|
||||
// 上报时判断是否属于国内用户,国内用户不上报token 既不触发callkit
|
||||
if (Platform.isIOS) {
|
||||
CallKitHandler.setupListener();
|
||||
String? token = await CallKitHandler.getVoipToken();
|
||||
@ -111,20 +109,4 @@ Future<void> privacySDKInitialization() async {
|
||||
await jpushProvider.initJPushService();
|
||||
NotificationService().init(); // 初始化通知服务
|
||||
|
||||
// /// 检查ip如果属于国内才进行初始化
|
||||
// final CheckIPEntity entity = await ApiRepository.to.checkIpAction(ip: '');
|
||||
// if (entity.errorCode!.codeIsSuccessful) {
|
||||
// String currentLanguage =
|
||||
// CurrentLocaleTool.getCurrentLocaleString(); // 当前选择语言
|
||||
// // 判断如果ip是国内的且选的是中文才初始化一键登录
|
||||
// if (entity.data!.abbreviation?.toLowerCase() == 'cn' &&
|
||||
// currentLanguage == 'zh_CN') {
|
||||
// // 初始化一键登录服务
|
||||
// final StarLockLoginLogic loginLogic = Get.put(StarLockLoginLogic());
|
||||
// await JverifyOneClickLoginManage();
|
||||
// loginLogic.state.isCheckVerifyEnable.value =
|
||||
// await JverifyOneClickLoginManage().checkVerifyEnable();
|
||||
// eventBus.fire(AgreePrivacyAgreement());
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
11
lib/main/lockDetail/doorLockLog/date_time_extensions.dart
Normal file
11
lib/main/lockDetail/doorLockLog/date_time_extensions.dart
Normal file
@ -0,0 +1,11 @@
|
||||
extension DateTimeExtensions on DateTime {
|
||||
/// 返回一个新的 DateTime,只保留年月日,时间部分设为 00:00:00.000
|
||||
DateTime get withoutTime {
|
||||
return DateTime(year, month, day);
|
||||
}
|
||||
|
||||
/// 判断两个日期是否是同一天(忽略时间)
|
||||
bool isSameDate(DateTime other) {
|
||||
return year == other.year && month == other.month && day == other.day;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/apm/apm_helper.dart';
|
||||
import 'package:star_lock/app_settings/app_settings.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/date_time_extensions.dart';
|
||||
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_state.dart';
|
||||
@ -235,13 +236,15 @@ class DoorLockLogLogic extends BaseGetXController {
|
||||
lockId: state.keyInfos.value.lockId!,
|
||||
lockEventType: state.dropdownValue.value,
|
||||
pageNo: pageNo,
|
||||
pageSize: int.parse(pageSize),
|
||||
pageSize: 1000,
|
||||
startDate: state.startDate.value,
|
||||
endDate: state.endDate.value);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
// 更新数据列表
|
||||
state.lockLogItemList.addAll(entity.data!.itemList!);
|
||||
state.lockLogItemList.refresh();
|
||||
state.weekEventList.addAll(entity.data!.itemList!);
|
||||
state.weekEventList.refresh();
|
||||
// 更新页码
|
||||
pageNo++;
|
||||
}
|
||||
@ -358,6 +361,7 @@ class DoorLockLogLogic extends BaseGetXController {
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
_setWeekRange();
|
||||
super.onInit();
|
||||
|
||||
// 获取是否是演示模式 演示模式不获取接口
|
||||
@ -370,6 +374,35 @@ class DoorLockLogLogic extends BaseGetXController {
|
||||
}
|
||||
}
|
||||
|
||||
void _setWeekRange() {
|
||||
final now = DateTime.now();
|
||||
|
||||
// 计算当前日期是星期几(1=周一,7=周日)
|
||||
int weekday = now.weekday; // 1-7
|
||||
|
||||
// 计算距离本周一有多少天(向后推)
|
||||
// 周一: 0天, 周二: 1天, ..., 周日: 6天
|
||||
int daysToSubtract = weekday - 1; // 减去1,因为周一就是基准
|
||||
|
||||
// 当前周的周一 00:00:00.000
|
||||
DateTime startOfWeek = DateTime(now.year, now.month, now.day)
|
||||
.subtract(Duration(days: daysToSubtract));
|
||||
|
||||
// 当前周的周日 23:59:59.999
|
||||
DateTime endOfWeek = startOfWeek
|
||||
.add(Duration(days: 6)) // 加6天到周日
|
||||
.add(Duration(hours: 23, minutes: 59, seconds: 59, milliseconds: 999));
|
||||
|
||||
// 更新响应式变量
|
||||
state.startDate.value = startOfWeek.millisecondsSinceEpoch;
|
||||
state.endDate.value = endOfWeek.millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
// 可选:提供一个方法来刷新周范围(比如切换周)
|
||||
void refreshWeek() {
|
||||
_setWeekRange();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onClose() async {
|
||||
super.onClose();
|
||||
|
||||
@ -5,10 +5,13 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:star_lock/appRouters.dart';
|
||||
import 'package:star_lock/app_settings/app_settings.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/date_time_extensions.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_logic.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_state.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/exportRecordDialog/exportRecordDialog_page.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/week_calendar_view.dart';
|
||||
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
|
||||
import 'package:star_lock/main/lockDetail/videoLog/widget/full_screenImage_page.dart';
|
||||
import 'package:star_lock/main/lockDetail/videoLog/widget/video_thumbnail_image.dart';
|
||||
@ -34,8 +37,39 @@ class DoorLockLogPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final DoorLockLogLogic logic = Get.put(DoorLockLogLogic());
|
||||
final DoorLockLogState state = Get.find<DoorLockLogLogic>().state;
|
||||
bool _isAtBottom = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
final max = _scrollController.position.maxScrollExtent;
|
||||
final current = _scrollController.position.pixels;
|
||||
|
||||
AppLog.log('current:${current}');
|
||||
// 判断是否接近底部(例如 5 像素内)
|
||||
if (current >= max - 5) {
|
||||
if (!_isAtBottom) {
|
||||
setState(() {
|
||||
_isAtBottom = true;
|
||||
});
|
||||
print('✅ 已滑动到 timelines 列表底部!');
|
||||
// 可以在这里触发加载更多、发送事件等
|
||||
}
|
||||
} else {
|
||||
if (_isAtBottom) {
|
||||
setState(() {
|
||||
_isAtBottom = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -97,16 +131,31 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
topAdvancedCalendarWidget(),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: AppColors.greyLineColor,
|
||||
indent: 30.w,
|
||||
endIndent: 30.w,
|
||||
),
|
||||
eventDropDownWidget(),
|
||||
Expanded(child: timeLineView())
|
||||
],
|
||||
),
|
||||
floatingActionButton: Visibility(
|
||||
visible: _isAtBottom,
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_scrollController.animateTo(
|
||||
0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(48.w),
|
||||
),
|
||||
backgroundColor: AppColors.mainColor,
|
||||
child: Icon(
|
||||
Icons.arrow_upward,
|
||||
color: Colors.white,
|
||||
size: 48.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -152,88 +201,14 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
||||
}
|
||||
}
|
||||
|
||||
// switch (value) {
|
||||
// case "读取记录".tr:
|
||||
// {
|
||||
// logic.mockNetworkDataRequest(isRefresh: true);
|
||||
// }
|
||||
// break;
|
||||
// case '清空记录'.tr:
|
||||
// {
|
||||
// ShowCupertinoAlertView().showClearOperationRecordAlert(
|
||||
// clearClick: () {
|
||||
// logic.clearOperationRecordRequest();
|
||||
// });
|
||||
// }
|
||||
// break;
|
||||
// case '导出记录':
|
||||
// {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (BuildContext context) {
|
||||
// return ExportRecordDialog(
|
||||
// onExport: (String filePath) {
|
||||
// Get.toNamed(Routers.exportSuccessPage,
|
||||
// arguments: <String, String>{'filePath': filePath});
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
//顶部日历小部件
|
||||
Widget topAdvancedCalendarWidget() {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Theme(
|
||||
data: theme.copyWith(
|
||||
textTheme: theme.textTheme.copyWith(
|
||||
titleMedium: theme.textTheme.titleMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
bodyLarge: theme.textTheme.bodyLarge!.copyWith(
|
||||
fontSize: 14,
|
||||
color: Colors.black54,
|
||||
),
|
||||
bodyMedium: theme.textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
primaryColor: AppColors.mainColor,
|
||||
highlightColor: Colors.yellow,
|
||||
disabledColor: Colors.grey,
|
||||
),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
AdvancedCalendar(
|
||||
controller: state.calendarControllerCustom,
|
||||
events: state.events,
|
||||
weekLineHeight: 48.0,
|
||||
startWeekDay: 1,
|
||||
innerDot: true,
|
||||
keepLineSize: true,
|
||||
calendarTextStyle: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.3125,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 8.0,
|
||||
right: 8.0,
|
||||
child: Obx(() => Text(
|
||||
'${state.currentSelectDate.value.year}${'年'.tr}${state.currentSelectDate.value.month}${'月'.tr}',
|
||||
style: theme.textTheme.titleMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
)),
|
||||
),
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 20.h, left: 30.w, bottom: 10.h, right: 20.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWeekCalendar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -269,38 +244,38 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16.w),
|
||||
),
|
||||
child: Obx(() => EasyRefreshTool(
|
||||
onRefresh: () async {
|
||||
logic.mockNetworkDataRequest(isRefresh: true);
|
||||
},
|
||||
onLoad: () async {
|
||||
logic.mockNetworkDataRequest(isRefresh: false);
|
||||
},
|
||||
child: state.lockLogItemList.isNotEmpty
|
||||
? Timeline.tileBuilder(
|
||||
builder: _timelineBuilderWidget(),
|
||||
theme: TimelineThemeData(
|
||||
nodePosition: 0.04, //居左侧距离
|
||||
connectorTheme: const ConnectorThemeData(
|
||||
thickness: 1.0,
|
||||
color: AppColors.greyLineColor,
|
||||
indent: 0.5,
|
||||
),
|
||||
indicatorTheme: const IndicatorThemeData(
|
||||
size: 8.0,
|
||||
color: AppColors.greyLineColor,
|
||||
position: 0.4,
|
||||
),
|
||||
child: Obx(
|
||||
() => state.lockLogItemList.isNotEmpty
|
||||
? Timeline.tileBuilder(
|
||||
controller: _scrollController,
|
||||
builder: _timelineBuilderWidget(),
|
||||
theme: TimelineThemeData(
|
||||
nodePosition: 0.04, //居左侧距离
|
||||
connectorTheme: const ConnectorThemeData(
|
||||
thickness: 1.0,
|
||||
color: AppColors.greyLineColor,
|
||||
indent: 0.5,
|
||||
),
|
||||
)
|
||||
: NoData())),
|
||||
indicatorTheme: const IndicatorThemeData(
|
||||
size: 8.0,
|
||||
color: AppColors.greyLineColor,
|
||||
position: 0.4,
|
||||
),
|
||||
),
|
||||
)
|
||||
: NoData(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String formatTimestampToHHmm(int timestampMs) {
|
||||
// 1. 将毫秒时间戳转换为秒(DateTime 需要秒级时间戳)
|
||||
int timestampSec = timestampMs ~/ 1000;
|
||||
String formatTimestampToDateTimeYYYYMMDD(int timestampMs) {
|
||||
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
|
||||
DateFormat formatter =
|
||||
DateFormat('MM${'月'.tr}dd${'日'.tr}'); // 格式:2025-08-18 14:30
|
||||
return formatter.format(dateTime);
|
||||
}
|
||||
|
||||
String formatTimestampToHHmm(int timestampMs) {
|
||||
// 2. 创建 DateTime 对象
|
||||
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
|
||||
|
||||
@ -325,7 +300,8 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
||||
return '${formatTimestampToHHmm(item.operateDate!)} ' +
|
||||
'密码'.tr +
|
||||
'开锁'.tr +
|
||||
'(${'昵称'.tr}:${item.username})'+'(${'密码'.tr}:${item.keyboardPwd})';
|
||||
'(${'昵称'.tr}:${item.username})' +
|
||||
'(${'密码'.tr}:${item.keyboardPwd})';
|
||||
case 30:
|
||||
return '${formatTimestampToHHmm(item.operateDate!)} ' +
|
||||
'卡'.tr +
|
||||
@ -444,6 +420,11 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${formatTimestampToDateTimeYYYYMMDD(timelineData.operateDate!)}',
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
)),
|
||||
// 使用 SingleChildScrollView 实现横向滚动
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal, // 横向滚动
|
||||
@ -622,4 +603,56 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
||||
}
|
||||
state.ifCurrentScreen.value = false;
|
||||
}
|
||||
|
||||
List<DateTime> getCurrentWeekDates() {
|
||||
final now = DateTime.now();
|
||||
// weekday: 1=周一, 2=周二, ..., 7=周日
|
||||
// 计算距离上一个周日相差的天数
|
||||
// 如果今天是周日,weekday == 7,偏移为 0
|
||||
final int daysSinceSunday = now.weekday % 7; // 周一=1 -> %7=1, 周日=7 -> %7=0
|
||||
|
||||
final List<DateTime> weekDates = [];
|
||||
for (int i = 0; i < 7; i++) {
|
||||
final DateTime day = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day - daysSinceSunday + i, // 从周日开始累加
|
||||
);
|
||||
weekDates.add(day);
|
||||
}
|
||||
|
||||
return weekDates;
|
||||
}
|
||||
|
||||
Widget _buildWeekCalendar() {
|
||||
return Obx(() {
|
||||
final list = state.weekEventList.value;
|
||||
final dateSet = list
|
||||
.map((e) => DateTime.fromMillisecondsSinceEpoch(e.operateDate!))
|
||||
.map((dt) => dt.withoutTime) // 转为年月日
|
||||
.toSet(); // 用 Set 提升查找性能
|
||||
AppLog.log('dateSet:${dateSet}');
|
||||
return WeekCalendarView(
|
||||
hasData: (DateTime date) {
|
||||
return dateSet.contains(date.withoutTime);
|
||||
},
|
||||
onDateSelected: (DateTime date) async {
|
||||
print('外部收到选中: $date');
|
||||
|
||||
state.operateDate = date.millisecondsSinceEpoch;
|
||||
state.startDate.value =
|
||||
DateTime(date.year, date.month, date.day).millisecondsSinceEpoch;
|
||||
state.endDate.value =
|
||||
DateTime(date.year, date.month, date.day, 23, 59, 59, 999)
|
||||
.millisecondsSinceEpoch;
|
||||
await logic.mockNetworkDataRequest(isRefresh: true);
|
||||
},
|
||||
onWeekChanged: (DateTime start, DateTime end) {
|
||||
state.startDate.value = start.millisecondsSinceEpoch;
|
||||
state.endDate.value = end.millisecondsSinceEpoch;
|
||||
logic.mockNetworkDataRequest(isRefresh: true);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/common/XSConstantMacro/XSConstantMacro.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
|
||||
@ -13,10 +12,15 @@ class DoorLockLogState {
|
||||
DoorLockLogState() {
|
||||
keyInfos.value = Get.arguments['keyInfo'];
|
||||
}
|
||||
|
||||
final Rx<DoorLockLogEntity> lockLogEntity = DoorLockLogEntity().obs;
|
||||
final Rx<LockListInfoItemEntity> keyInfos = LockListInfoItemEntity().obs;
|
||||
final RxList<DoorLockLogDataItem> lockLogItemList =
|
||||
<DoorLockLogDataItem>[].obs;
|
||||
final RxList<DoorLockLogDataItem> weekEventList =
|
||||
<DoorLockLogDataItem>[].obs;
|
||||
final RxList<DoorLockLogDataItem> dayEventList =
|
||||
<DoorLockLogDataItem>[].obs;
|
||||
final AdvancedCalendarController calendarControllerToday =
|
||||
AdvancedCalendarController.today();
|
||||
final AdvancedCalendarController calendarControllerCustom =
|
||||
@ -31,7 +35,7 @@ class DoorLockLogState {
|
||||
.millisecondsSinceEpoch
|
||||
.obs;
|
||||
final RxInt endDate = DateTime(
|
||||
DateTime.now().year, DateTime.now().month, DateTime.now().day + 1)
|
||||
DateTime.now().year, DateTime.now().month, DateTime.now().day + 1)
|
||||
.subtract(const Duration(milliseconds: 1))
|
||||
.millisecondsSinceEpoch
|
||||
.obs;
|
||||
|
||||
220
lib/main/lockDetail/doorLockLog/week_calendar_view.dart
Normal file
220
lib/main/lockDetail/doorLockLog/week_calendar_view.dart
Normal file
@ -0,0 +1,220 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/app_settings/app_colors.dart';
|
||||
import 'package:star_lock/main/lockDetail/doorLockLog/date_time_extensions.dart';
|
||||
|
||||
class WeekCalendarView extends StatefulWidget {
|
||||
// 用于判断某一天是否有数据(激活状态)
|
||||
final bool Function(DateTime date)? hasData;
|
||||
final void Function(DateTime date)? onDateSelected; // 新增:选中日期回调
|
||||
final void Function(DateTime start, DateTime end)? onWeekChanged;
|
||||
|
||||
const WeekCalendarView({
|
||||
Key? key,
|
||||
this.hasData,
|
||||
this.onDateSelected,
|
||||
this.onWeekChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_WeekCalendarViewState createState() => _WeekCalendarViewState();
|
||||
}
|
||||
|
||||
class _WeekCalendarViewState extends State<WeekCalendarView> {
|
||||
final PageController _pageController = PageController(initialPage: 500);
|
||||
int _currentPage = 500;
|
||||
|
||||
// 当前选中的日期(以 DateTime 格式存储)
|
||||
late DateTime _selectedDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDate = DateTime.now().withoutTime; // 默认选中今天
|
||||
}
|
||||
|
||||
// 获取指定 page 对应的一周日期
|
||||
List<DateTime> _getWeekDatesForPage(int page) {
|
||||
final now = DateTime.now();
|
||||
final baseSunday =
|
||||
DateTime(now.year, now.month, now.day - (now.weekday % 7));
|
||||
final daysOffset = (page - 500) * 7;
|
||||
final targetSunday = baseSunday.add(Duration(days: daysOffset));
|
||||
return List.generate(
|
||||
7,
|
||||
(i) => DateTime(
|
||||
targetSunday.year, targetSunday.month, targetSunday.day + i));
|
||||
}
|
||||
|
||||
// 判断是否为今天
|
||||
bool _isToday(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
return date.year == now.year &&
|
||||
date.month == now.month &&
|
||||
date.day == now.day;
|
||||
}
|
||||
|
||||
// 判断是否为选中日期
|
||||
bool _isSelected(DateTime date) {
|
||||
return date.year == _selectedDate.year &&
|
||||
date.month == _selectedDate.month &&
|
||||
date.day == _selectedDate.day;
|
||||
}
|
||||
|
||||
// 判断是否有数据(激活状态)
|
||||
bool _hasData(DateTime date) {
|
||||
return widget.hasData?.call(date.withoutTime) ?? false;
|
||||
}
|
||||
|
||||
void _onDateSelected(DateTime date) {
|
||||
setState(() {
|
||||
_selectedDate = date.withoutTime;
|
||||
});
|
||||
// 触发回调,通知父组件
|
||||
widget.onDateSelected?.call(date);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 当前周范围提示
|
||||
_buildWeekRangeLabel(_currentPage),
|
||||
|
||||
SizedBox(height: 10.h),
|
||||
|
||||
SizedBox(
|
||||
height: 100.h,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: 1000,
|
||||
itemBuilder: (context, page) {
|
||||
final weekDates = _getWeekDatesForPage(page);
|
||||
return Row(
|
||||
children: weekDates.asMap().entries.map((entry) {
|
||||
final int index = entry.key;
|
||||
final DateTime date = entry.value;
|
||||
final bool isSelected = _isSelected(date);
|
||||
final bool hasData = _hasData(date);
|
||||
final bool isToday = _isToday(date);
|
||||
|
||||
// 确定文字颜色
|
||||
Color textColor;
|
||||
if (isSelected) {
|
||||
textColor = Colors.white; // 选中时文字为白色
|
||||
} else if (hasData) {
|
||||
textColor = Colors.black; // 有数据:黑色
|
||||
} else if (isToday) {
|
||||
textColor = Colors.black; // 今天:黑色
|
||||
} else {
|
||||
textColor = Colors.grey; // 默认:灰色
|
||||
}
|
||||
|
||||
// 确定背景颜色
|
||||
Color? bgColor;
|
||||
if (isSelected) {
|
||||
bgColor = AppColors.mainColor; // 选中用主题色
|
||||
}
|
||||
// 其他状态无背景
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _onDateSelected(date),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(4.w),
|
||||
width: 75.w,
|
||||
height: 75.w,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(50.r),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
[
|
||||
'简写周日',
|
||||
'简写周一',
|
||||
'简写周二',
|
||||
'简写周三',
|
||||
'简写周四',
|
||||
'简写周五',
|
||||
'简写周六'
|
||||
][index]
|
||||
.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
date.day.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 26.sp,
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (isToday && !isSelected) // 今天但未选中
|
||||
SizedBox(height: 2.h),
|
||||
if (isToday && !isSelected)
|
||||
Container(
|
||||
width: 6.w,
|
||||
height: 6.w,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.mainColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
// ✅ 获取当前页对应的周的起止日期
|
||||
final dates = _getWeekDatesForPage(page);
|
||||
final startOfWeek = dates.first;
|
||||
final endOfWeek = dates.last;
|
||||
|
||||
// ✅ 触发回调,可用于请求接口
|
||||
widget.onWeekChanged?.call(startOfWeek, endOfWeek);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeekRangeLabel(int page) {
|
||||
final dates = _getWeekDatesForPage(page);
|
||||
final start = dates[0];
|
||||
final end = dates[6];
|
||||
|
||||
String label;
|
||||
|
||||
if (start.year == end.year) {
|
||||
// 同一年:显示为 "2025年8月18日 - 8月24日"
|
||||
label =
|
||||
'${start.year}${'年'.tr}${start.month}${'月'.tr}${start.day}${'日'.tr} - ${end.month}${'月'.tr}${end.day}${'日'.tr}';
|
||||
} else {
|
||||
// 跨年:显示为 "2024年12月31日 - 2025年1月6日"
|
||||
label =
|
||||
'${start.year}${'年'.tr}${start.month}${'月'.tr}${start.day}${'日'.tr} - ${end.year}${'年'.tr}${end.month}${'月'.tr}${end.day}${'日'.tr}';
|
||||
}
|
||||
|
||||
return Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.w600),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -253,10 +253,10 @@ class AddFingerprintLogic extends BaseGetXController {
|
||||
final List<int> getTokenList = changeStringListToIntList(token!);
|
||||
String startTime = DateTool().dateToHNString(state.effectiveDateTime.value);
|
||||
String endTime = DateTool().dateToHNString(state.failureDateTime.value);
|
||||
if (F.isSKY) {
|
||||
startTime = '255:00';
|
||||
endTime = '255:00';
|
||||
}
|
||||
// if (F.isSKY) {
|
||||
// startTime = '255:00';
|
||||
// endTime = '255:00';
|
||||
// }
|
||||
|
||||
final String command = SenderAddFingerprintWithTimeCycleCoercionCommand(
|
||||
keyID: '1',
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:star_lock/blue/blue_manage.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_getDeviceModel.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_otaUpgrade.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_processOtaUpgrade.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_setVoicePackageFinalResult.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigure.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart';
|
||||
import 'package:star_lock/blue/io_reply.dart';
|
||||
@ -52,6 +53,8 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
|
||||
_handlerVoicePackageConfigureProcess(reply);
|
||||
} else if (reply is VoicePackageConfigureConfirmationReply) {
|
||||
handleVoiceConfigureThrottled(reply);
|
||||
} else if (reply is SetVoicePackageFinalResultReply) {
|
||||
handleSetResult(reply);
|
||||
}
|
||||
});
|
||||
await initList();
|
||||
@ -93,7 +96,7 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
|
||||
final passthroughItem = PassthroughItem(
|
||||
lang: element.lang,
|
||||
timbres: element.timbres,
|
||||
langText: '简体中文'.tr + '(中国台湾)'.tr,
|
||||
langText: '简体中文'.tr + '(中国台湾)'.tr + '(Simplified Chinese TW)',
|
||||
name: element.name,
|
||||
);
|
||||
state.languages.add(passthroughItem);
|
||||
@ -432,6 +435,33 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
|
||||
_handlerVoicePackageConfigureConfirmation(
|
||||
VoicePackageConfigureConfirmationReply reply,
|
||||
) async {
|
||||
final int status = reply.data[2];
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
await BlueManage().blueSendData(BlueManage().connectDeviceName,
|
||||
(BluetoothConnectionState deviceConnectionState) async {
|
||||
if (deviceConnectionState == BluetoothConnectionState.connected) {
|
||||
await BlueManage().writeCharacteristicWithResponse(
|
||||
SetVoicePackageFinalResult(
|
||||
lockID: BlueManage().connectDeviceName,
|
||||
languageCode: state.tempLangStr.value,
|
||||
).packageData(),
|
||||
);
|
||||
} else if (deviceConnectionState ==
|
||||
BluetoothConnectionState.disconnected) {
|
||||
dismissEasyLoading();
|
||||
cancelBlueConnetctToastTimer();
|
||||
showBlueConnetctToast();
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
showToast('设置'.tr + '失败'.tr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void handleSetResult(SetVoicePackageFinalResultReply reply) async {
|
||||
final int status = reply.data[2];
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
|
||||
@ -107,6 +107,7 @@ class _SpeechLanguageSettingsPageState
|
||||
isHaveLine: true,
|
||||
isHaveDirection: false,
|
||||
isHaveRightWidget: true,
|
||||
leftTitleMaxWidth: 0.9.sw, // 设置左侧标题最大宽度
|
||||
rightWidget:
|
||||
state.selectPassthroughListIndex.value == index
|
||||
? Image(
|
||||
@ -131,66 +132,6 @@ class _SpeechLanguageSettingsPageState
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Obx(
|
||||
() => SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListView.builder(
|
||||
itemCount: state.soundTypeList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
// 判断是否是最后一个元素(索引等于 itemCount - 1)
|
||||
final isLastItem = index == state.soundTypeList.length - 1;
|
||||
|
||||
// 获取当前平台数据(假设 platFormSet 是 RxList<Platform>)
|
||||
final platform = state.soundTypeList.value[index];
|
||||
return CommonItem(
|
||||
leftTitel: state.soundTypeList.value[index],
|
||||
rightTitle: '',
|
||||
isHaveLine: !isLastItem,
|
||||
// 最后一个元素不显示分割线(取反)
|
||||
isHaveDirection: false,
|
||||
isHaveRightWidget: true,
|
||||
rightWidget: Radio<String>(
|
||||
// Radio 的值:使用平台的唯一标识(如 id)
|
||||
value: platform,
|
||||
// 当前选中的值:与 selectPlatFormIndex 关联的 id
|
||||
groupValue: state.soundTypeList
|
||||
.value[state.selectSoundTypeIndex.value],
|
||||
// 选中颜色(可选,默认主题色)
|
||||
activeColor: AppColors.mainColor,
|
||||
// 点击 Radio 时回调(更新选中索引)
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
// 找到当前选中平台的索引(根据 id 匹配)
|
||||
final newIndex = state.soundTypeList.value
|
||||
.indexWhere((p) => p == value);
|
||||
if (newIndex != -1) {
|
||||
state.selectSoundTypeIndex.value = newIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
action: () {
|
||||
setState(() {
|
||||
state.selectSoundTypeIndex.value = index;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics() //add this line,
|
||||
),
|
||||
Column(
|
||||
children: _buildList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildList() {
|
||||
final appLocalLanguages = state.languages;
|
||||
|
||||
@ -16,6 +16,7 @@ class ThirdPartyPlatformState {
|
||||
final RxList<String> platFormSet = List.of({
|
||||
'锁通通'.tr,
|
||||
'涂鸦智能'.tr,
|
||||
'Matter'.tr ,
|
||||
}).obs;
|
||||
|
||||
RxInt selectPlatFormIndex = 0.obs;
|
||||
|
||||
@ -145,9 +145,9 @@ class EditVideoLogLogic extends BaseGetXController {
|
||||
}
|
||||
|
||||
// 根据URL生成唯一的文件名(MD5哈希值)
|
||||
String getFileNameFromUrl(String url, String extension) {
|
||||
String getFileNameFromUrl(String url, String extension, int recordType) {
|
||||
final hash = md5.convert(utf8.encode(url)).toString(); // 使用 md5 生成哈希值
|
||||
return '$hash.$extension';
|
||||
return '$recordType' + '_' + '$hash.$extension';
|
||||
}
|
||||
|
||||
Future<void> recordDownloadTime(String filePath) async {
|
||||
@ -169,7 +169,7 @@ class EditVideoLogLogic extends BaseGetXController {
|
||||
}
|
||||
|
||||
// 下载文件方法(支持视频和图片)
|
||||
Future<String?> downloadFile(String? url) async {
|
||||
Future<String?> downloadFile(String? url, int recordType) async {
|
||||
if (url == null || url.isEmpty) {
|
||||
print('URL不能为空');
|
||||
return null;
|
||||
@ -183,7 +183,8 @@ class EditVideoLogLogic extends BaseGetXController {
|
||||
|
||||
// 根据URL生成唯一文件名(自动识别扩展名)
|
||||
String extension = _getFileTypeFromUrl(url); // 自动检测文件类型
|
||||
String fileName = getFileNameFromUrl(url, extension); // 根据URL生成唯一文件名
|
||||
String fileName =
|
||||
getFileNameFromUrl(url, extension, recordType); // 根据URL生成唯一文件名
|
||||
String savePath = '${appDocDir.path}/downloads/$fileName'; // 自定义保存路径
|
||||
|
||||
// 确保目录存在
|
||||
|
||||
@ -76,23 +76,31 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Obx(() => ListView.builder(
|
||||
child: Obx(
|
||||
() => ListView.builder(
|
||||
itemCount: state.videoLogList.length,
|
||||
itemBuilder: (BuildContext c, int index) {
|
||||
final CloudStorageData item = state.videoLogList[index];
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: 20.w, top: 15.w, bottom: 15.w),
|
||||
child: Row(children: <Widget>[
|
||||
Text(item.date ?? '',
|
||||
style: TextStyle(fontSize: 20.sp)),
|
||||
])),
|
||||
mainListView(index, item)
|
||||
],
|
||||
return ExpansionTile(
|
||||
shape: Border(),
|
||||
collapsedShape: Border(),
|
||||
expansionAnimationStyle: AnimationStyle(
|
||||
curve: Curves.easeInOut,
|
||||
duration: Duration(milliseconds: 400),
|
||||
),
|
||||
initiallyExpanded: true,
|
||||
title: Text(
|
||||
item.date ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
children: mainListView(index, item),
|
||||
);
|
||||
})),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomBottomBtnWidget()
|
||||
],
|
||||
@ -100,29 +108,35 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotData() {
|
||||
return Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'images/icon_noData.png',
|
||||
width: 160.w,
|
||||
height: 180.h,
|
||||
),
|
||||
Text(
|
||||
'暂无数据'.tr,
|
||||
style: TextStyle(
|
||||
color: AppColors.darkGrayTextColor, fontSize: 22.sp),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double itemW = (1.sw - 15.w * 4) / 3;
|
||||
double itemH = (1.sw - 15.w * 4) / 3 + 40.h;
|
||||
|
||||
Widget mainListView(int index, CloudStorageData itemData) {
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.only(left: 15.w, right: 15.w),
|
||||
itemCount: itemData.recordList!.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//横轴元素个数
|
||||
crossAxisCount: 3,
|
||||
//纵轴间距
|
||||
mainAxisSpacing: 10.w,
|
||||
// 横轴间距
|
||||
crossAxisSpacing: 15.w,
|
||||
//子组件宽高长度比例
|
||||
childAspectRatio: itemW / itemH),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final RecordListData recordData = itemData.recordList![index];
|
||||
return videoItem(recordData);
|
||||
},
|
||||
);
|
||||
// 云存列表
|
||||
List<Widget> mainListView(int index, CloudStorageData itemData) {
|
||||
return itemData.recordList!.map((e) => videoItem(e)).toList();
|
||||
}
|
||||
|
||||
// Widget videoItem(RecordListData recordData, int index) {
|
||||
@ -237,9 +251,9 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
|
||||
if (state.selectVideoLogList.value.isNotEmpty) {
|
||||
state.selectVideoLogList.value.forEach((element) {
|
||||
if (element.videoUrl != null && element.videoUrl != '') {
|
||||
logic.downloadFile(element.videoUrl ?? '');
|
||||
logic.downloadFile(element.videoUrl ?? '', element.recordType!);
|
||||
} else if (element.imagesUrl != null && element.imagesUrl != '') {
|
||||
logic.downloadFile(element.imagesUrl ?? '');
|
||||
logic.downloadFile(element.imagesUrl ?? '', element.recordType!);
|
||||
}
|
||||
});
|
||||
// double _progress = 0.0;
|
||||
@ -339,77 +353,154 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
|
||||
Widget videoItem(RecordListData recordData) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (recordData.videoUrl != null && recordData.videoUrl!.isNotEmpty) {
|
||||
Get.toNamed(Routers.videoLogDetailPage, arguments: <String, Object>{
|
||||
'recordData': recordData,
|
||||
'videoDataList': state.videoLogList.value
|
||||
});
|
||||
} else if (recordData.imagesUrl != null &&
|
||||
recordData.imagesUrl!.isNotEmpty) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => FullScreenImagePage(
|
||||
imageUrl: recordData.imagesUrl!,
|
||||
),
|
||||
),
|
||||
);
|
||||
recordData.isSelect = !recordData.isSelect!;
|
||||
if (recordData.isSelect! == true) {
|
||||
state.selectVideoLogList.add(recordData);
|
||||
} else {
|
||||
state.selectVideoLogList.remove(recordData);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: itemW,
|
||||
height: itemH,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: 20.h,
|
||||
left: 18.w,
|
||||
right: 18.w,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
width: 1.sw,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Image(
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
image: state.selectVideoLogList.value.contains(recordData)
|
||||
? const AssetImage('images/icon_round_select.png')
|
||||
: const AssetImage('images/icon_round_unSelect.png'),
|
||||
),
|
||||
SizedBox(
|
||||
width: 14.w,
|
||||
),
|
||||
Container(
|
||||
width: itemW,
|
||||
height: itemW,
|
||||
margin: const EdgeInsets.all(0),
|
||||
color: Colors.white,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
child: _buildImageOrVideoItem(recordData),
|
||||
padding: EdgeInsets.all(10.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(58.w),
|
||||
color: AppColors.mainColor,
|
||||
),
|
||||
child: Icon(
|
||||
_buildIconByType(recordData),
|
||||
size: 48.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 14.w,
|
||||
),
|
||||
Container(
|
||||
height: itemW,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_buildTitleByType(recordData),
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.h,
|
||||
),
|
||||
Text(
|
||||
DateTool()
|
||||
.dateToHNString(recordData.operateDate.toString()),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5.h),
|
||||
Text(
|
||||
DateTool()
|
||||
.dateToYMDHNString(recordData.operateDate.toString()),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18.sp),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0.w,
|
||||
right: 0.w,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
recordData.isSelect = !recordData.isSelect!;
|
||||
if (recordData.isSelect! == true) {
|
||||
state.selectVideoLogList.add(recordData);
|
||||
} else {
|
||||
state.selectVideoLogList.remove(recordData);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Image(
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
image: state.selectVideoLogList.value.contains(recordData)
|
||||
? const AssetImage('images/icon_round_select.png')
|
||||
: const AssetImage('images/icon_round_unSelect.png'),
|
||||
Container(
|
||||
width: 118.w,
|
||||
height: 118.w,
|
||||
margin: const EdgeInsets.all(0),
|
||||
color: Colors.white,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
child: _buildImageOrVideoItem(recordData),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _buildTitleByType(RecordListData item) {
|
||||
final recordType = item.recordType;
|
||||
switch (recordType) {
|
||||
case 130:
|
||||
return '防拆报警'.tr;
|
||||
case 160:
|
||||
return '人脸'.tr + '开锁'.tr;
|
||||
case 220:
|
||||
return '逗留警告'.tr;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
IconData _buildIconByType(RecordListData item) {
|
||||
final recordType = item.recordType;
|
||||
switch (recordType) {
|
||||
case 130:
|
||||
return Icons.fmd_bad_outlined;
|
||||
case 160:
|
||||
return Icons.tag_faces_outlined;
|
||||
case 220:
|
||||
return Icons.wifi_tethering_error_rounded_outlined;
|
||||
default:
|
||||
return Icons.priority_high_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
Color _buildTextColorByType(RecordListData item) {
|
||||
final recordType = item.recordType;
|
||||
switch (recordType) {
|
||||
case 120:
|
||||
case 150:
|
||||
case 130:
|
||||
case 190:
|
||||
case 200:
|
||||
case 210:
|
||||
case 220:
|
||||
return Colors.red;
|
||||
default:
|
||||
return Colors.black;
|
||||
}
|
||||
}
|
||||
|
||||
_buildImageOrVideoItem(RecordListData recordData) {
|
||||
if (recordData.videoUrl != null && recordData.videoUrl!.isNotEmpty) {
|
||||
return _buildVideoItem(recordData);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:star_lock/appRouters.dart';
|
||||
@ -63,8 +63,17 @@ class VideoLogLogic extends BaseGetXController {
|
||||
final content = await File(logFilePath).readAsString();
|
||||
final logData = Map<String, int>.from(json.decode(content));
|
||||
|
||||
// 遍历所有记录
|
||||
logData.forEach((filePath, timestamp) {
|
||||
String fileName = filePath
|
||||
.split('/')
|
||||
.last; // 得到: 220_f5e371111918ff70cb3532bec20e38c4.mp4
|
||||
String withoutExt = fileName.replaceAll('.mp4', ''); // 或使用 substring 截取
|
||||
|
||||
String numberStr = withoutExt.split('_').first; // 得到: 220
|
||||
|
||||
int number = int.parse(numberStr);
|
||||
|
||||
print(number); // 输出: 220
|
||||
final downloadDateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
final dateKey =
|
||||
'${downloadDateTime.year}-${downloadDateTime.month.toString().padLeft(2, '0')}-${downloadDateTime.day.toString().padLeft(2, '0')}';
|
||||
@ -77,11 +86,15 @@ class VideoLogLogic extends BaseGetXController {
|
||||
// 将文件记录添加到对应日期的列表中
|
||||
if (filePath.endsWith('.jpg')) {
|
||||
groupedDownloads[dateKey]?.add(
|
||||
RecordListData(operateDate: timestamp, imagesUrl: filePath),
|
||||
RecordListData(
|
||||
operateDate: timestamp,
|
||||
imagesUrl: filePath,
|
||||
recordType: number),
|
||||
);
|
||||
} else if (filePath.endsWith('.mp4')) {
|
||||
groupedDownloads[dateKey]?.add(
|
||||
RecordListData(operateDate: timestamp, videoUrl: filePath),
|
||||
RecordListData(
|
||||
operateDate: timestamp, videoUrl: filePath, recordType: number),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/appRouters.dart';
|
||||
import 'package:star_lock/app_settings/app_settings.dart';
|
||||
import 'package:star_lock/flavors.dart';
|
||||
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
|
||||
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_state.dart';
|
||||
@ -26,9 +27,7 @@ class VideoLogPage extends StatefulWidget {
|
||||
|
||||
class _VideoLogPageState extends State<VideoLogPage> {
|
||||
final VideoLogLogic logic = Get.put(VideoLogLogic());
|
||||
final VideoLogState state = Get
|
||||
.find<VideoLogLogic>()
|
||||
.state;
|
||||
final VideoLogState state = Get.find<VideoLogLogic>().state;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -56,66 +55,71 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
// title加编辑按钮
|
||||
editVideoTip(),
|
||||
Obx(
|
||||
() =>
|
||||
Visibility(
|
||||
visible: !state.isNavLocal.value,
|
||||
child: state.videoLogList.length > 0
|
||||
? Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: state.videoLogList.length,
|
||||
itemBuilder: (BuildContext c, int index) {
|
||||
final CloudStorageData item =
|
||||
state.videoLogList[index];
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: 20.w, top: 15.w, bottom: 15.w),
|
||||
child: Row(children: <Widget>[
|
||||
Text(item.date ?? '',
|
||||
style: TextStyle(fontSize: 20.sp)),
|
||||
])),
|
||||
mainListView(index, item)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: _buildNotData(),
|
||||
),
|
||||
() => Visibility(
|
||||
visible: !state.isNavLocal.value,
|
||||
child: state.videoLogList.length > 0
|
||||
? Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: state.videoLogList.length,
|
||||
itemBuilder: (BuildContext c, int index) {
|
||||
final CloudStorageData item =
|
||||
state.videoLogList[index];
|
||||
return ExpansionTile(
|
||||
shape: Border(),
|
||||
collapsedShape: Border(),
|
||||
expansionAnimationStyle: AnimationStyle(
|
||||
curve: Curves.easeInOut,
|
||||
duration: Duration(milliseconds: 400),
|
||||
),
|
||||
initiallyExpanded: true,
|
||||
title: Text(
|
||||
item.date ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
children: mainListView(index, item),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: _buildNotData(),
|
||||
),
|
||||
),
|
||||
// 本地顶部
|
||||
Obx(
|
||||
() =>
|
||||
Visibility(
|
||||
visible: state.isNavLocal.value,
|
||||
child: state.lockVideoList.length > 0
|
||||
? Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: state.lockVideoList.length,
|
||||
itemBuilder: (BuildContext c, int index) {
|
||||
final CloudStorageData item =
|
||||
state.lockVideoList[index];
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: 20.w, top: 15.w, bottom: 15.w),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(item.date ?? '',
|
||||
style: TextStyle(fontSize: 20.sp)),
|
||||
],
|
||||
() => Visibility(
|
||||
visible: state.isNavLocal.value,
|
||||
child: state.lockVideoList.length > 0
|
||||
? Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: state.lockVideoList.length,
|
||||
itemBuilder: (BuildContext c, int index) {
|
||||
final CloudStorageData item =
|
||||
state.lockVideoList[index];
|
||||
return ExpansionTile(
|
||||
shape: Border(),
|
||||
collapsedShape: Border(),
|
||||
expansionAnimationStyle: AnimationStyle(
|
||||
curve: Curves.easeInOut,
|
||||
duration: Duration(milliseconds: 400),
|
||||
),
|
||||
initiallyExpanded: true,
|
||||
title: Text(
|
||||
item.date ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
lockMainListView(index, item)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: _buildNotData(),
|
||||
),
|
||||
children: mainListView(index, item),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: _buildNotData(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -154,24 +158,28 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
state.isNavLocal.value = false;
|
||||
state.lockVideoList.clear();
|
||||
// logic.clearDownloads();
|
||||
});
|
||||
},
|
||||
child: Obx(() =>
|
||||
Text('云存'.tr,
|
||||
style: state.isNavLocal.value == true
|
||||
? TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.w600)
|
||||
: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28.sp,
|
||||
fontWeight: FontWeight.w600)))),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
state.isNavLocal.value = false;
|
||||
state.lockVideoList.clear();
|
||||
// logic.clearDownloads();
|
||||
});
|
||||
},
|
||||
child: Obx(
|
||||
() => Text(
|
||||
'云存'.tr,
|
||||
style: state.isNavLocal.value == true
|
||||
? TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.w600)
|
||||
: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28.sp,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@ -180,19 +188,18 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
});
|
||||
},
|
||||
child: Obx(
|
||||
() =>
|
||||
Text(
|
||||
'已下载'.tr,
|
||||
style: state.isNavLocal.value == true
|
||||
? TextStyle(
|
||||
() => Text(
|
||||
'已下载'.tr,
|
||||
style: state.isNavLocal.value == true
|
||||
? TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28.sp,
|
||||
fontWeight: FontWeight.w600)
|
||||
: TextStyle(
|
||||
: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -212,93 +219,88 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
// height: 150.h,
|
||||
margin: EdgeInsets.all(15.w),
|
||||
padding:
|
||||
EdgeInsets.only(left: 20.w, top: 20.w, bottom: 20.w, right: 10.w),
|
||||
EdgeInsets.only(left: 20.w, top: 20.w, bottom: 20.w, right: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF6F7F8),
|
||||
borderRadius: BorderRadius.circular(20.h)),
|
||||
color: const Color(0xFFF6F7F8),
|
||||
borderRadius: BorderRadius.circular(
|
||||
20.h,
|
||||
),
|
||||
),
|
||||
child: Obx(
|
||||
() =>
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('3天滚动储存'.tr,
|
||||
style: TextStyle(fontSize: 24.sp)),
|
||||
SizedBox(height: 10.h),
|
||||
Text("${F
|
||||
.navTitle}${"已为本设备免费提供3大滚动视频储存服务"
|
||||
.tr}",
|
||||
style:
|
||||
TextStyle(fontSize: 22.sp, color: Colors
|
||||
.grey)),
|
||||
],
|
||||
)),
|
||||
SizedBox(width: 15.w),
|
||||
Text('去升级'.tr, style: TextStyle(fontSize: 22.sp)),
|
||||
Image(
|
||||
width: 40.w,
|
||||
height: 24.w,
|
||||
image: const AssetImage(
|
||||
'images/icon_right_black.png'))
|
||||
Text('3天滚动储存'.tr, style: TextStyle(fontSize: 24.sp)),
|
||||
SizedBox(height: 10.h),
|
||||
Text("${F.navTitle}${"已为本设备免费提供3大滚动视频储存服务".tr}",
|
||||
style:
|
||||
TextStyle(fontSize: 22.sp, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 16.h,
|
||||
),
|
||||
Text(
|
||||
'云存服务状态:${_handlerValidityPeriodStatsText()}',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.h,
|
||||
),
|
||||
Visibility(
|
||||
visible: state.validityPeriodInfo.value != null &&
|
||||
state.validityPeriodInfo.value?.status == 1,
|
||||
child: Text(
|
||||
'过期时间:${state.validityPeriodInfo.value
|
||||
?.validityPeriodEnd}',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.h,
|
||||
),
|
||||
Visibility(
|
||||
visible: state.validityPeriodInfo.value != null &&
|
||||
state.validityPeriodInfo.value?.status == 1,
|
||||
child: Text(
|
||||
'滚动存储天数:${state.validityPeriodInfo.value?.rollingStorageDays} 天',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.h,
|
||||
),
|
||||
Visibility(
|
||||
visible: state.validityPeriodInfo.value != null &&
|
||||
state.validityPeriodInfo.value?.status == 1,
|
||||
child: Text(
|
||||
'剩余天数:${state.validityPeriodInfo.value
|
||||
?.remainingDays} 天',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
SizedBox(width: 15.w),
|
||||
Text('去升级'.tr, style: TextStyle(fontSize: 22.sp)),
|
||||
Image(
|
||||
width: 40.w,
|
||||
height: 24.w,
|
||||
image: const AssetImage('images/icon_right_black.png'))
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 16.h,
|
||||
),
|
||||
Text(
|
||||
'云存服务状态:${_handlerValidityPeriodStatsText()}',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.h,
|
||||
),
|
||||
Visibility(
|
||||
visible: state.validityPeriodInfo.value != null &&
|
||||
state.validityPeriodInfo.value?.status == 1,
|
||||
child: Text(
|
||||
'过期时间:${state.validityPeriodInfo.value?.validityPeriodEnd}',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.h,
|
||||
),
|
||||
Visibility(
|
||||
visible: state.validityPeriodInfo.value != null &&
|
||||
state.validityPeriodInfo.value?.status == 1,
|
||||
child: Text(
|
||||
'滚动存储天数:${state.validityPeriodInfo.value?.rollingStorageDays} 天',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.h,
|
||||
),
|
||||
Visibility(
|
||||
visible: state.validityPeriodInfo.value != null &&
|
||||
state.validityPeriodInfo.value?.status == 1,
|
||||
child: Text(
|
||||
'剩余天数:${state.validityPeriodInfo.value?.remainingDays} 天',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -329,7 +331,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
// height: 130.h,
|
||||
margin: EdgeInsets.all(15.w),
|
||||
padding:
|
||||
EdgeInsets.only(left: 20.w, top: 30.w, bottom: 30.w, right: 10.w),
|
||||
EdgeInsets.only(left: 20.w, top: 30.w, bottom: 30.w, right: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF6F7F8),
|
||||
borderRadius: BorderRadius.circular(20.h)),
|
||||
@ -337,15 +339,15 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// SizedBox(height: 20.h),
|
||||
Text('下载列表'.tr, style: TextStyle(fontSize: 24.sp)),
|
||||
SizedBox(height: 15.h),
|
||||
Text('暂无下载内容'.tr,
|
||||
style: TextStyle(fontSize: 22.sp, color: Colors.grey)),
|
||||
],
|
||||
)),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// SizedBox(height: 20.h),
|
||||
Text('下载列表'.tr, style: TextStyle(fontSize: 24.sp)),
|
||||
SizedBox(height: 15.h),
|
||||
Text('暂无下载内容'.tr,
|
||||
style: TextStyle(fontSize: 22.sp, color: Colors.grey)),
|
||||
],
|
||||
)),
|
||||
SizedBox(width: 15.w),
|
||||
// Text("去升级", style: TextStyle(fontSize: 24.sp)),
|
||||
Image(
|
||||
@ -412,48 +414,12 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
double itemH = (1.sw - 15.w * 4) / 3 + 40.h;
|
||||
|
||||
// 云存列表
|
||||
Widget mainListView(int index, CloudStorageData itemData) {
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.only(left: 15.w, right: 15.w),
|
||||
itemCount: itemData.recordList!.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//横轴元素个数
|
||||
crossAxisCount: 3,
|
||||
//纵轴间距
|
||||
mainAxisSpacing: 15.w,
|
||||
// 横轴间距
|
||||
crossAxisSpacing: 15.w,
|
||||
//子组件宽高长度比例
|
||||
childAspectRatio: itemW / itemH),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final RecordListData recordData = itemData.recordList![index];
|
||||
return videoItem(recordData);
|
||||
},
|
||||
);
|
||||
List<Widget> mainListView(int index, CloudStorageData itemData) {
|
||||
return itemData.recordList!.map((e) => videoItem(e)).toList();
|
||||
}
|
||||
|
||||
Widget lockMainListView(int index, CloudStorageData itemData) {
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.only(left: 15.w, right: 15.w),
|
||||
itemCount: itemData.recordList!.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//横轴元素个数
|
||||
crossAxisCount: 3,
|
||||
//纵轴间距
|
||||
mainAxisSpacing: 15.w,
|
||||
// 横轴间距
|
||||
crossAxisSpacing: 15.w,
|
||||
//子组件宽高长度比例
|
||||
childAspectRatio: itemW / itemH),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final RecordListData recordData = itemData.recordList![index];
|
||||
return videoItem(recordData);
|
||||
},
|
||||
);
|
||||
List<Widget> lockMainListView(int index, CloudStorageData itemData) {
|
||||
return itemData.recordList!.map((e) => videoItem(e)).toList();
|
||||
}
|
||||
|
||||
Widget videoItem(RecordListData recordData) {
|
||||
@ -469,22 +435,86 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
FullScreenImagePage(
|
||||
imageUrl: recordData.imagesUrl!,
|
||||
),
|
||||
builder: (context) => FullScreenImagePage(
|
||||
imageUrl: recordData.imagesUrl!,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
width: itemW,
|
||||
height: itemH,
|
||||
child: Column(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: 20.h,
|
||||
left: 18.w,
|
||||
right: 18.w,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
width: 1.sw,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(10.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(58.w),
|
||||
color: AppColors.mainColor,
|
||||
),
|
||||
child: Icon(
|
||||
_buildIconByType(recordData),
|
||||
size: 48.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 14.w,
|
||||
),
|
||||
Container(
|
||||
height: itemW,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_buildTitleByType(recordData),
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.h,
|
||||
),
|
||||
Text(
|
||||
DateTool()
|
||||
.dateToHNString(recordData.operateDate.toString()),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: itemW,
|
||||
height: itemW,
|
||||
width: 118.w,
|
||||
height: 118.w,
|
||||
margin: const EdgeInsets.all(0),
|
||||
color: Colors.white,
|
||||
child: ClipRRect(
|
||||
@ -492,12 +522,6 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
child: _buildImageOrVideoItem(recordData),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5.h),
|
||||
Text(
|
||||
DateTool().dateToYMDHNString(recordData.operateDate.toString()),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18.sp),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -512,6 +536,50 @@ class _VideoLogPageState extends State<VideoLogPage> {
|
||||
}
|
||||
}
|
||||
|
||||
String _buildTitleByType(RecordListData item) {
|
||||
final recordType = item.recordType;
|
||||
switch (recordType) {
|
||||
case 130:
|
||||
return '防拆报警'.tr;
|
||||
case 160:
|
||||
return '人脸'.tr + '开锁'.tr;
|
||||
case 220:
|
||||
return '逗留警告'.tr;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
IconData _buildIconByType(RecordListData item) {
|
||||
final recordType = item.recordType;
|
||||
switch (recordType) {
|
||||
case 130:
|
||||
return Icons.fmd_bad_outlined;
|
||||
case 160:
|
||||
return Icons.tag_faces_outlined;
|
||||
case 220:
|
||||
return Icons.wifi_tethering_error_rounded_outlined;
|
||||
default:
|
||||
return Icons.priority_high_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
Color _buildTextColorByType(RecordListData item) {
|
||||
final recordType = item.recordType;
|
||||
switch (recordType) {
|
||||
case 120:
|
||||
case 150:
|
||||
case 130:
|
||||
case 190:
|
||||
case 200:
|
||||
case 210:
|
||||
case 220:
|
||||
return Colors.red;
|
||||
default:
|
||||
return Colors.black;
|
||||
}
|
||||
}
|
||||
|
||||
_buildVideoItem(RecordListData recordData) {
|
||||
return VideoThumbnailImage(videoUrl: recordData.videoUrl!);
|
||||
}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/appRouters.dart';
|
||||
import 'package:star_lock/app_settings/app_settings.dart';
|
||||
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
|
||||
import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/controlsOverlay_page.dart';
|
||||
import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/videoLogDetail_state.dart';
|
||||
@ -30,11 +34,11 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
AppLog.log(
|
||||
'state.recordData.value.videoUrl!' + state.recordData.value.videoUrl!);
|
||||
|
||||
state.videoController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(state.recordData.value.videoUrl!),
|
||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
);
|
||||
state.videoController =
|
||||
createVideoController(state.recordData.value.videoUrl!);
|
||||
|
||||
state.videoController.addListener(() {
|
||||
setState(() {});
|
||||
@ -47,10 +51,8 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
|
||||
if (state.videoController != null) {
|
||||
await state.videoController.dispose(); // 释放旧资源
|
||||
}
|
||||
state.videoController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(videoUrl),
|
||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
);
|
||||
state.videoController =
|
||||
createVideoController(state.recordData.value.videoUrl!);
|
||||
|
||||
// 初始化完成后通知框架重新构建界面
|
||||
await state.videoController.initialize();
|
||||
@ -60,6 +62,22 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
VideoPlayerController createVideoController(String url) {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return VideoPlayerController.networkUrl(
|
||||
Uri.parse(url),
|
||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
);
|
||||
} else {
|
||||
final file = File(
|
||||
url.startsWith('file://') ? url.replaceFirst('file://', '') : url);
|
||||
return VideoPlayerController.file(
|
||||
file,
|
||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -104,6 +122,7 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// _buildTitleRow(),
|
||||
_buildOther(),
|
||||
],
|
||||
)
|
||||
@ -135,14 +154,79 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
width: itemW,
|
||||
height: itemH,
|
||||
child: Column(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: 20.h,
|
||||
left: 18.w,
|
||||
right: 18.w,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
width: 1.sw,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(10.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(58.w),
|
||||
color: AppColors.mainColor,
|
||||
),
|
||||
child: Icon(
|
||||
_buildIconByType(recordData),
|
||||
size: 48.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 14.w,
|
||||
),
|
||||
Container(
|
||||
height: itemW,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_buildTitleByType(recordData),
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.h,
|
||||
),
|
||||
Text(
|
||||
DateTool()
|
||||
.dateToHNString(recordData.operateDate.toString()),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: itemW,
|
||||
height: itemW,
|
||||
width: 118.w,
|
||||
height: 118.w,
|
||||
margin: const EdgeInsets.all(0),
|
||||
color: Colors.white,
|
||||
child: ClipRRect(
|
||||
@ -213,11 +297,13 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
|
||||
margin: EdgeInsets.only(left: 20.w, top: 15.w, bottom: 15.w),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(item.date ?? '', style: TextStyle(fontSize: 20.sp)),
|
||||
Text(item.date ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
mainListView(index, item),
|
||||
...mainListView(index, item),
|
||||
],
|
||||
);
|
||||
},
|
||||
@ -225,22 +311,60 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget mainListView(int index, CloudStorageData itemData) {
|
||||
return GridView.builder(
|
||||
itemCount: itemData.recordList!.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//横轴元素个数
|
||||
crossAxisCount: 3,
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _buildItem(itemData.recordList![index]);
|
||||
},
|
||||
);
|
||||
// 云存列表
|
||||
List<Widget> mainListView(int index, CloudStorageData itemData) {
|
||||
return itemData.recordList!.map((e) => videoItem(e)).toList();
|
||||
}
|
||||
|
||||
String _buildTitleByType(RecordListData item) {
|
||||
final recordType = item.recordType;
|
||||
switch (recordType) {
|
||||
case 130:
|
||||
return '防拆报警'.tr;
|
||||
case 160:
|
||||
return '人脸'.tr + '开锁'.tr;
|
||||
case 220:
|
||||
return '逗留警告'.tr;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
IconData _buildIconByType(RecordListData item) {
|
||||
final recordType = item.recordType;
|
||||
switch (recordType) {
|
||||
case 130:
|
||||
return Icons.fmd_bad_outlined;
|
||||
case 160:
|
||||
return Icons.tag_faces_outlined;
|
||||
case 220:
|
||||
return Icons.wifi_tethering_error_rounded_outlined;
|
||||
default:
|
||||
return Icons.priority_high_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
_buildItem(itemData) {
|
||||
return videoItem(itemData);
|
||||
}
|
||||
|
||||
_buildTitleRow() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
),
|
||||
padding: EdgeInsets.only(left: 15.w, top: 24.w, bottom: 24.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
_buildTitleByType(state.recordData.value) ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,53 +1,48 @@
|
||||
import 'dart:io'; // 导入 dart:io 以使用 File 类
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:path_provider/path_provider.dart'; // 导入 path_provider
|
||||
import 'package:video_thumbnail/video_thumbnail.dart'; // 导入 video_thumbnail
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
|
||||
class VideoThumbnailImage extends StatefulWidget {
|
||||
final String videoUrl;
|
||||
|
||||
VideoThumbnailImage({required this.videoUrl});
|
||||
const VideoThumbnailImage({Key? key, required this.videoUrl})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_VideoThumbnailState createState() => _VideoThumbnailState();
|
||||
}
|
||||
|
||||
class _VideoThumbnailState extends State<VideoThumbnailImage> {
|
||||
final Map<String, String> _thumbnailCache = {}; // 缩略图缓存
|
||||
late Future<String?> _thumbnailFuture; // 用于存储缩略图生成的 Future
|
||||
// ✅ 使用 static 缓存:所有实例共享,避免重复请求
|
||||
static final Map<String, Future<String?>> _pendingThumbnails = {};
|
||||
|
||||
late Future<String?> _thumbnailFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_thumbnailFuture = _generateThumbnail(); // 在 initState 中初始化 Future
|
||||
// ✅ 如果已存在该 URL 的 Future,复用;否则创建并缓存
|
||||
_thumbnailFuture = _pendingThumbnails.putIfAbsent(widget.videoUrl, () {
|
||||
return _generateThumbnail(widget.videoUrl);
|
||||
});
|
||||
}
|
||||
|
||||
// 生成缩略图
|
||||
Future<String?> _generateThumbnail() async {
|
||||
// 生成缩略图(只执行一次 per URL)
|
||||
Future<String?> _generateThumbnail(String url) async {
|
||||
try {
|
||||
// 检查缓存中是否已有缩略图
|
||||
if (_thumbnailCache.containsKey(widget.videoUrl)) {
|
||||
return _thumbnailCache[widget.videoUrl];
|
||||
}
|
||||
|
||||
// 获取临时目录路径
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final thumbnailPath = await VideoThumbnail.thumbnailFile(
|
||||
video: widget.videoUrl,
|
||||
// 视频 URL
|
||||
final thumbnail = await VideoThumbnail.thumbnailFile(
|
||||
video: url,
|
||||
thumbnailPath: tempDir.path,
|
||||
// 缩略图保存路径
|
||||
imageFormat: ImageFormat.JPEG,
|
||||
// 缩略图格式
|
||||
maxHeight: 200,
|
||||
// 缩略图最大高度
|
||||
quality: 100, // 缩略图质量 (0-100)
|
||||
quality: 100,
|
||||
);
|
||||
|
||||
// 更新缓存
|
||||
_thumbnailCache[widget.videoUrl] = thumbnailPath!;
|
||||
return thumbnailPath;
|
||||
return thumbnail;
|
||||
} catch (e) {
|
||||
print('Failed to generate thumbnail: $e');
|
||||
return null;
|
||||
@ -57,27 +52,25 @@ class _VideoThumbnailState extends State<VideoThumbnailImage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<String?>(
|
||||
future: _thumbnailFuture, // 生成缩略图的 Future
|
||||
future: _thumbnailFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
// 加载中显示转圈
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasError || !snapshot.hasData) {
|
||||
// 加载失败或没有数据时显示提示
|
||||
return Image.asset(
|
||||
'images/icon_unHaveData.png', // 错误图片路径
|
||||
fit: BoxFit.cover,
|
||||
return Center(
|
||||
child: Image.asset(
|
||||
'images/icon_unHaveData.png',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 加载成功,显示缩略图
|
||||
final thumbnailPath = snapshot.data!;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
RotatedBox(
|
||||
quarterTurns: -1,
|
||||
child: Image.file(
|
||||
File(thumbnailPath), // 显示生成的缩略图
|
||||
File(snapshot.data!),
|
||||
width: 200,
|
||||
height: 200,
|
||||
fit: BoxFit.cover,
|
||||
@ -85,7 +78,7 @@ class _VideoThumbnailState extends State<VideoThumbnailImage> {
|
||||
),
|
||||
Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
size: 80,
|
||||
size: 88.sp,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
],
|
||||
|
||||
@ -297,6 +297,11 @@ class LockListInfoItemEntity {
|
||||
LockListInfoItemEntity copy() {
|
||||
return LockListInfoItemEntity.fromJson(toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LockListInfoItemEntity{keyId: $keyId, lockId: $lockId, lockName: $lockName, lockAlias: $lockAlias, electricQuantity: $electricQuantity, fwVersion: $fwVersion, hwVersion: $hwVersion, keyType: $keyType, passageMode: $passageMode, userType: $userType, startDate: $startDate, endDate: $endDate, weekDays: $weekDays, remoteEnable: $remoteEnable, faceAuthentication: $faceAuthentication, lastFaceValidateTime: $lastFaceValidateTime, nextFaceValidateTime: $nextFaceValidateTime, keyRight: $keyRight, keyStatus: $keyStatus, isLockOwner: $isLockOwner, sendDate: $sendDate, lockUserNo: $lockUserNo, senderUserId: $senderUserId, electricQuantityDate: $electricQuantityDate, electricQuantityStandby: $electricQuantityStandby, isOnlyManageSelf: $isOnlyManageSelf, restoreCount: $restoreCount, model: $model, vendor: $vendor, bluetooth: $bluetooth, lockFeature: $lockFeature, lockSetting: $lockSetting, hasGateway: $hasGateway, appUnlockOnline: $appUnlockOnline, mac: $mac, initUserNo: $initUserNo, updateDate: $updateDate, network: $network}';
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkInfo {
|
||||
@ -323,6 +328,11 @@ class NetworkInfo {
|
||||
data['isOnline'] = isOnline;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NetworkInfo{peerId: $peerId, wifiName: $wifiName, isOnline: $isOnline}';
|
||||
}
|
||||
}
|
||||
|
||||
class Bluetooth {
|
||||
@ -356,6 +366,11 @@ class Bluetooth {
|
||||
data['signKey'] = signKey;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Bluetooth{bluetoothDeviceId: $bluetoothDeviceId, bluetoothDeviceName: $bluetoothDeviceName, publicKey: $publicKey, privateKey: $privateKey, signKey: $signKey}';
|
||||
}
|
||||
}
|
||||
|
||||
class LockFeature {
|
||||
@ -442,6 +457,11 @@ class LockFeature {
|
||||
data['isMJpeg'] = isMJpeg;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LockFeature{password: $password, passwordIssue: $passwordIssue, icCard: $icCard, fingerprint: $fingerprint, fingerVein: $fingerVein, palmVein: $palmVein, isSupportIris: $isSupportIris, d3Face: $d3Face, bluetoothRemoteControl: $bluetoothRemoteControl, videoIntercom: $videoIntercom, isSupportCatEye: $isSupportCatEye, isSupportBackupBattery: $isSupportBackupBattery, isNoSupportedBlueBroadcast: $isNoSupportedBlueBroadcast, wifiLockType: $wifiLockType, wifi: $wifi, isH264: $isH264, isH265: $isH265, isMJpeg: $isMJpeg}';
|
||||
}
|
||||
}
|
||||
|
||||
class LockSetting {
|
||||
@ -486,6 +506,11 @@ class LockSetting {
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LockSetting{attendance: $attendance, appUnlockOnline: $appUnlockOnline, remoteUnlock: $remoteUnlock, catEyeConfig: $catEyeConfig}';
|
||||
}
|
||||
}
|
||||
|
||||
// 定义 CatEyeConfig 类
|
||||
|
||||
@ -11,6 +11,7 @@ import 'package:star_lock/appRouters.dart';
|
||||
import 'package:star_lock/app_settings/app_colors.dart';
|
||||
import 'package:star_lock/blue/blue_manage.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_getDeviceModel.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_readVoicePackageFinalResult.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigure.dart';
|
||||
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart';
|
||||
import 'package:star_lock/blue/io_reply.dart';
|
||||
@ -50,9 +51,12 @@ class LockVoiceSettingLogic extends BaseGetXController {
|
||||
_handlerVoicePackageConfigureProcess(reply);
|
||||
} else if (reply is VoicePackageConfigureConfirmationReply) {
|
||||
handleVoiceConfigureThrottled(reply);
|
||||
} else if (reply is ReadLockCurrentVoicePacketReply) {
|
||||
handleLockCurrentVoicePacketResult(reply);
|
||||
}
|
||||
});
|
||||
initList();
|
||||
readLockLanguage();
|
||||
}
|
||||
|
||||
void handleVoiceConfigureThrottled(
|
||||
@ -197,7 +201,7 @@ class LockVoiceSettingLogic extends BaseGetXController {
|
||||
// 开始配置语音包
|
||||
void _handlerStartVoicePackageConfigure(
|
||||
VoicePackageConfigureReply reply) async {
|
||||
final int status = reply.data[6];
|
||||
final int status = reply.data[3];
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
//成功
|
||||
@ -250,11 +254,10 @@ class LockVoiceSettingLogic extends BaseGetXController {
|
||||
if (lang == 'zh_TW') {
|
||||
// 如果是台湾的话应该显示未简体中文
|
||||
List<String> parts = lang.split('_');
|
||||
final indexOf = locales.indexOf(Locale(parts[0], parts[1]));
|
||||
final passthroughItem = PassthroughItem(
|
||||
lang: element.lang,
|
||||
timbres: element.timbres,
|
||||
langText: '简体中文'.tr + '(中国台湾)'.tr,
|
||||
langText: '简体中文'.tr + '(中国台湾)'.tr + '(Simplified Chinese TW)',
|
||||
name: element.name,
|
||||
);
|
||||
state.languages.add(passthroughItem);
|
||||
@ -268,6 +271,7 @@ class LockVoiceSettingLogic extends BaseGetXController {
|
||||
ExtensionLanguageType.fromLocale(locales[indexOf]).lanTitle,
|
||||
name: element.name,
|
||||
);
|
||||
|
||||
state.languages.add(passthroughItem);
|
||||
}
|
||||
});
|
||||
@ -403,4 +407,71 @@ class LockVoiceSettingLogic extends BaseGetXController {
|
||||
state.data = null;
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void readLockLanguage() async {
|
||||
await BlueManage().blueSendData(BlueManage().connectDeviceName,
|
||||
(BluetoothConnectionState deviceConnectionState) async {
|
||||
if (deviceConnectionState == BluetoothConnectionState.connected) {
|
||||
await BlueManage().writeCharacteristicWithResponse(
|
||||
ReadLockCurrentVoicePacket(
|
||||
lockID: BlueManage().connectDeviceName,
|
||||
).packageData(),
|
||||
);
|
||||
} else if (deviceConnectionState ==
|
||||
BluetoothConnectionState.disconnected) {
|
||||
dismissEasyLoading();
|
||||
cancelBlueConnetctToastTimer();
|
||||
showBlueConnetctToast();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void handleLockCurrentVoicePacketResult(
|
||||
ReadLockCurrentVoicePacketReply reply) {
|
||||
final int status = reply.data[6];
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
//成功
|
||||
cancelBlueConnetctToastTimer();
|
||||
|
||||
// 1. 计算 LanguageCode 在字节数组中的起始和结束索引
|
||||
// CmdID (2 bytes) + Status (1 byte) = 3 bytes -> LanguageCode 从索引 3 开始
|
||||
const int languageCodeStartIndex = 3;
|
||||
const int languageCodeLength = 20;
|
||||
const int languageCodeEndIndex =
|
||||
languageCodeStartIndex + languageCodeLength; // 23
|
||||
|
||||
// 2. 检查数据长度是否足够
|
||||
if (reply.data.length < languageCodeEndIndex) {
|
||||
throw Exception(
|
||||
'Reply data is too short to contain LanguageCode. Expected at least $languageCodeEndIndex bytes, got ${reply.data.length}');
|
||||
}
|
||||
// 3. 从字节数组中截取 LanguageCode 对应的字节段
|
||||
List<int> languageCodeBytes =
|
||||
reply.data.sublist(languageCodeStartIndex, languageCodeEndIndex);
|
||||
|
||||
// 4. 将字节列表转换为字符串
|
||||
// 通常这种编码是 UTF-8 或 ASCII
|
||||
String languageCode = String.fromCharCodes(languageCodeBytes);
|
||||
|
||||
// 5. (可选) 清理字符串:移除可能的填充字符(如空字符 '\0' 或空格)
|
||||
// 因为字段长度固定为20,不足的部分可能用 '\0' 填充
|
||||
languageCode = languageCode.trim(); // 移除首尾空格
|
||||
languageCode =
|
||||
languageCode.replaceAll('\u0000', ''); // 移除空字符 (null bytes)
|
||||
|
||||
// 6. 使用提取到的 languageCode
|
||||
print('LanguageCode: $languageCode'); // 例如: zh_CN, en_US
|
||||
break;
|
||||
case 0x06:
|
||||
//无权限
|
||||
final List<int> token = reply.data.sublist(2, 6);
|
||||
if (state.data != null) {
|
||||
sendFileToDevice(state.data!, token);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,6 +95,12 @@ class _LockVoiceSettingState extends State<LockVoiceSetting> {
|
||||
final soundType = state.soundTypeList.value[index];
|
||||
return CommonItem(
|
||||
leftTitel: soundType,
|
||||
leftTitleStyle: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: state.selectSoundTypeIndex.value == index
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
),
|
||||
rightTitle: '',
|
||||
isHaveLine: !isLastItem,
|
||||
isHaveDirection: false,
|
||||
@ -135,10 +141,18 @@ class _LockVoiceSettingState extends State<LockVoiceSetting> {
|
||||
final item = state.languages[index];
|
||||
return CommonItem(
|
||||
leftTitel: item.langText,
|
||||
leftTitleStyle: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight:
|
||||
state.selectPassthroughListIndex.value == index
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
),
|
||||
rightTitle: '',
|
||||
isHaveLine: true,
|
||||
isHaveDirection: false,
|
||||
isHaveRightWidget: true,
|
||||
leftTitleMaxWidth: 0.9.sw,
|
||||
rightWidget:
|
||||
state.selectPassthroughListIndex.value == index
|
||||
? Image(
|
||||
|
||||
@ -8,7 +8,7 @@ import 'messageDetail_state.dart';
|
||||
class MessageDetailLogic extends BaseGetXController {
|
||||
final MessageDetailState state = MessageDetailState();
|
||||
|
||||
//请求密码钥匙列表
|
||||
// 将消息设置为已读
|
||||
Future<void> readMessageDataRequest() async {
|
||||
final MessageListEntity entity = await ApiRepository.to.readMessageLoadData(messageId:state.itemData.value.id!);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
|
||||
@ -24,14 +24,22 @@ class MessageListEntity {
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageListEntity{errorCode: $errorCode, description: $description, errorMsg: $errorMsg, data: $data}';
|
||||
}
|
||||
}
|
||||
|
||||
class Data {
|
||||
List<MessageItemEntity>? list;
|
||||
int? pageNo;
|
||||
int? pageSize;
|
||||
int? total;
|
||||
int? readCount;
|
||||
int? unreadCount;
|
||||
|
||||
Data({this.list, this.pageNo, this.pageSize});
|
||||
Data({this.list, this.pageNo, this.pageSize, this.total,this.readCount, this.unreadCount});
|
||||
|
||||
Data.fromJson(Map<String, dynamic> json) {
|
||||
if (json['list'] != null) {
|
||||
@ -42,6 +50,9 @@ class Data {
|
||||
}
|
||||
pageNo = json['pageNo'];
|
||||
pageSize = json['pageSize'];
|
||||
total = json['total'];
|
||||
readCount = json['readCount'];
|
||||
unreadCount = json['unreadCount'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@ -51,8 +62,16 @@ class Data {
|
||||
}
|
||||
data['pageNo'] = pageNo;
|
||||
data['pageSize'] = pageSize;
|
||||
data['total'] = total;
|
||||
data['readCount'] = readCount;
|
||||
data['unreadCount'] = unreadCount;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Data{list: $list, pageNo: $pageNo, pageSize: $pageSize, total: $total, readCount: $readCount, unreadCount: $unreadCount}';
|
||||
}
|
||||
}
|
||||
|
||||
class MessageItemEntity {
|
||||
@ -78,4 +97,9 @@ class MessageItemEntity {
|
||||
data['readAt'] = readAt;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageItemEntity{id: $id, data: $data, createdAt: $createdAt, readAt: $readAt}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_app_badger/flutter_app_badger.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:star_lock/app_settings/app_settings.dart';
|
||||
import 'package:star_lock/tools/baseGetXController.dart';
|
||||
import '../../../network/api_repository.dart';
|
||||
import '../../../tools/eventBusEventManage.dart';
|
||||
@ -18,6 +19,10 @@ class MessageListLogic extends BaseGetXController {
|
||||
final MessageListEntity entity = await ApiRepository.to
|
||||
.messageListLoadData(pageNo: pageNo.toString(), pageSize: pageSize);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
AppLog.log('消息列表数据请求成功:${entity.data!.total}');
|
||||
// 设置角标(直接设置一个数值)
|
||||
await FlutterAppBadger.updateBadgeCount(entity.data!.unreadCount!);
|
||||
|
||||
if (pageNo == 1) {
|
||||
state.itemDataList.value = entity.data!.list!;
|
||||
pageNo++;
|
||||
|
||||
@ -104,6 +104,7 @@ class _MineMultiLanguagePageState extends State<MineMultiLanguagePage> {
|
||||
isHaveLine: true,
|
||||
isHaveDirection: false,
|
||||
isHaveRightWidget: true,
|
||||
leftTitleMaxWidth: 0.9.sw, // 设置左侧标题最大宽度
|
||||
rightWidget: state.currentLanguageType.value == lanType
|
||||
? Image(
|
||||
image: const AssetImage('images/icon_item_checked.png'),
|
||||
|
||||
@ -2169,7 +2169,8 @@ class ApiProvider extends BaseProvider {
|
||||
readMessageURL.toUrl,
|
||||
jsonEncode({
|
||||
'id': messageId,
|
||||
}));
|
||||
}),
|
||||
isUnShowLoading: true);
|
||||
|
||||
// 删除消息
|
||||
Future<Response> deletMessageLoadData(String messageId) => post(
|
||||
|
||||
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/app_settings/app_settings.dart';
|
||||
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||
import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
|
||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||
@ -15,6 +16,7 @@ import 'package:star_lock/talk/starChart/proto/generic.pb.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_accept.pb.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||
import 'package:star_lock/tools/commonDataManage.dart';
|
||||
import 'package:star_lock/tools/eventBusEventManage.dart';
|
||||
import 'package:star_lock/tools/storage.dart';
|
||||
|
||||
import '../../star_chart_manage.dart';
|
||||
@ -43,6 +45,25 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle
|
||||
// 同意接听之后,停止对讲请求超时监听定时器
|
||||
talkeRequestOverTimeTimerManager.renew();
|
||||
talkeRequestOverTimeTimerManager.cancel();
|
||||
|
||||
// 同意接听之后代表已读消息
|
||||
// 需要把对应的消息设置为已读
|
||||
AppLog.log('msg:${scpMessage}');
|
||||
AppLog.log('msg:${startChartManage.lockListPeerId}');
|
||||
|
||||
// 锁发过来的id
|
||||
final fromPeerId = scpMessage.FromPeerId;
|
||||
if (fromPeerId != null && fromPeerId != '') {
|
||||
startChartManage.lockListPeerId.forEach((element) {
|
||||
if (element != null &&
|
||||
element.network != null &&
|
||||
element.network!.peerId == fromPeerId) {
|
||||
// 找到了对应的锁,设置为已读
|
||||
eventBus.fire(ReadTalkMessageRefreshUI(element.lockName!));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 启动发送rbcuInfo数据
|
||||
// startChartManage.startSendingRbcuInfoMessages(
|
||||
// ToPeerId: startChartManage.lockPeerId);
|
||||
|
||||
@ -1,11 +1,45 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/mine/message/messageList/messageList_entity.dart';
|
||||
import 'package:star_lock/network/api_repository.dart';
|
||||
import 'package:star_lock/network/start_chart_api.dart';
|
||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||
import 'package:star_lock/tools/baseGetXController.dart';
|
||||
import 'package:star_lock/tools/eventBusEventManage.dart';
|
||||
import 'package:star_lock/tools/storage.dart';
|
||||
|
||||
class AppLifecycleObserver extends WidgetsBindingObserver {
|
||||
// 刷新消息列表
|
||||
StreamSubscription? _readMessageRefreshUIEvent;
|
||||
|
||||
void _readMessageRefreshUIAction() {
|
||||
// 蓝牙协议通知传输跟蓝牙之外的数据传输类不一样 eventBus
|
||||
_readMessageRefreshUIEvent =
|
||||
eventBus.on<ReadTalkMessageRefreshUI>().listen((event) async {
|
||||
// 查询第一条消息
|
||||
final MessageListEntity entity = await ApiRepository.to
|
||||
.messageListLoadData(pageNo: '1', pageSize: '1');
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
final lockName = event.lockName;
|
||||
if (lockName != null && lockName.isNotEmpty) {
|
||||
final readAt = entity.data?.list?.first.readAt == 0;
|
||||
final data = entity.data?.list?.first.data;
|
||||
if (readAt && data != null && data.contains(lockName)) {
|
||||
// 如果是未读且饱含了锁名,就将这个消息设置为已读
|
||||
final entity2 = await ApiRepository.to
|
||||
.readMessageLoadData(messageId: entity.data!.list!.first.id!);
|
||||
if (entity2.errorCode!.codeIsSuccessful) {
|
||||
eventBus.fire(ReadMessageRefreshUI());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
@ -37,6 +71,7 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
|
||||
Get.back();
|
||||
}
|
||||
StartChartManage().destruction();
|
||||
_readMessageRefreshUIEvent?.cancel();
|
||||
}
|
||||
|
||||
void onAppResumed() async {
|
||||
@ -52,6 +87,9 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
|
||||
StartChartApi.to.startChartHost =
|
||||
loginData!.starchart!.scdUrl ?? StartChartApi.to.startChartHost;
|
||||
}
|
||||
|
||||
// 监听对讲消息处理角标已读
|
||||
_readMessageRefreshUIAction();
|
||||
print('App has resumed to the foreground.');
|
||||
}
|
||||
|
||||
|
||||
@ -561,8 +561,11 @@ class ImageTransmissionLogic extends BaseGetXController {
|
||||
state.voiceProcessor = VoiceProcessor.instance;
|
||||
}
|
||||
|
||||
Timer? _startProcessingAudioTimer;
|
||||
|
||||
//开始录音
|
||||
Future<void> startProcessingAudio() async {
|
||||
|
||||
try {
|
||||
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
|
||||
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
|
||||
@ -580,7 +583,6 @@ class ImageTransmissionLogic extends BaseGetXController {
|
||||
} on PlatformException catch (ex) {
|
||||
// state.errorMessage.value = 'Failed to start recorder: $ex';
|
||||
}
|
||||
state.isOpenVoice.value = false;
|
||||
}
|
||||
|
||||
/// 停止录音
|
||||
@ -602,48 +604,53 @@ class ImageTransmissionLogic extends BaseGetXController {
|
||||
} finally {
|
||||
final bool? isRecording = await state.voiceProcessor?.isRecording();
|
||||
state.isRecordingAudio.value = isRecording!;
|
||||
state.isOpenVoice.value = true;
|
||||
}
|
||||
_startProcessingAudioTimer?.cancel();
|
||||
_startProcessingAudioTimer = null;
|
||||
_bufferedAudioFrames.clear();
|
||||
}
|
||||
|
||||
static const int chunkSize = 320; // 每次发送320字节(10ms G.711)
|
||||
static const int intervalMs = 40; // 每40ms发送一次(4个chunk)
|
||||
void _sendAudioChunk(Timer timer) async {
|
||||
if (_bufferedAudioFrames.length < chunkSize) {
|
||||
// 数据不足,等待下一周期
|
||||
return;
|
||||
}
|
||||
|
||||
// 截取前 chunkSize 个字节
|
||||
final chunk = _bufferedAudioFrames.sublist(0, chunkSize);
|
||||
// 更新缓冲区:移除已发送部分
|
||||
_bufferedAudioFrames.removeRange(0, chunkSize);
|
||||
|
||||
// 获取时间戳(相对时间)
|
||||
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000;
|
||||
|
||||
print('Send chunk ${timer.tick}: ${chunk.take(10).toList()}...');
|
||||
|
||||
await StartChartManage().sendTalkDataMessage(
|
||||
talkData: TalkData(
|
||||
content: chunk,
|
||||
contentType: TalkData_ContentTypeE.G711,
|
||||
durationMs: ms,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 音频帧处理
|
||||
Future<void> _onFrame(List<int> frame) async {
|
||||
// 添加最大缓冲限制
|
||||
if (_bufferedAudioFrames.length > state.frameLength * 3) {
|
||||
_bufferedAudioFrames.clear(); // 清空过多积累的数据
|
||||
return;
|
||||
}
|
||||
final applyGain = _applyGain(frame, 1.6);
|
||||
|
||||
// 首先应用固定增益提升基础音量
|
||||
List<int> amplifiedFrame = _applyGain(frame, 1.6);
|
||||
// 编码为G711数据
|
||||
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law
|
||||
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0表示A-law
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
// 使用相对时间戳
|
||||
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳
|
||||
int getFrameLength = state.frameLength;
|
||||
if (Platform.isIOS) {
|
||||
getFrameLength = state.frameLength * 2;
|
||||
}
|
||||
|
||||
// 添加发送间隔控制
|
||||
if (_bufferedAudioFrames.length >= state.frameLength) {
|
||||
try {
|
||||
await StartChartManage().sendTalkDataMessage(
|
||||
talkData: TalkData(
|
||||
content: _bufferedAudioFrames,
|
||||
contentType: TalkData_ContentTypeE.G711,
|
||||
durationMs: ms,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_bufferedAudioFrames.clear(); // 确保清理缓冲区
|
||||
}
|
||||
} else {
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
|
||||
// 启动定时发送器(仅启动一次)
|
||||
if (_startProcessingAudioTimer == null && _bufferedAudioFrames.length > chunkSize) {
|
||||
_startProcessingAudioTimer = Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误监听
|
||||
void _onError(VoiceProcessorException error) {
|
||||
AppLog.log(error.message!);
|
||||
|
||||
@ -9,7 +9,6 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
|
||||
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
|
||||
import 'package:gallery_saver/gallery_saver.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@ -18,28 +17,20 @@ import 'package:star_lock/app_settings/app_settings.dart';
|
||||
import 'package:star_lock/login/login/entity/LoginEntity.dart';
|
||||
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart';
|
||||
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart';
|
||||
import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart';
|
||||
import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart';
|
||||
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||
import 'package:star_lock/network/api_repository.dart';
|
||||
import 'package:star_lock/talk/call/callTalk.dart';
|
||||
import 'package:star_lock/talk/call/g711.dart';
|
||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
||||
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart';
|
||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||
import 'package:star_lock/tools/G711Tool.dart';
|
||||
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
||||
import 'package:star_lock/tools/callkit_handler.dart';
|
||||
import 'package:star_lock/tools/commonDataManage.dart';
|
||||
import 'package:star_lock/tools/storage.dart';
|
||||
import 'package:video_decode_plugin/nalu_utils.dart';
|
||||
import 'package:video_decode_plugin/video_decode_plugin.dart';
|
||||
|
||||
import '../../../../tools/baseGetXController.dart';
|
||||
@ -51,7 +42,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
|
||||
int bufferSize = 25; // 初始化为默认大小
|
||||
|
||||
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||
int audioBufferSize = 20; // 音频默认缓冲2帧
|
||||
|
||||
// 回绕阈值,动态调整,frameSeq较小时阈值也小
|
||||
int _getFrameSeqRolloverThreshold(int lastSeq) {
|
||||
@ -107,12 +98,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
state.isLoading.value = true;
|
||||
// 创建解码器配置
|
||||
final config = VideoDecoderConfig(
|
||||
width: 864,
|
||||
width: StartChartManage().videoWidth,
|
||||
// 实际视频宽度
|
||||
height: 480,
|
||||
height: StartChartManage().videoHeight,
|
||||
codecType: 'h264',
|
||||
);
|
||||
// 初始化解码器并获取textureId
|
||||
AppLog.log('StartChartManage().videoWidth:${StartChartManage()
|
||||
.videoWidth}');
|
||||
AppLog.log('StartChartManage().videoHeight:${StartChartManage()
|
||||
.videoHeight}');
|
||||
final textureId = await VideoDecodePlugin.initDecoder(config);
|
||||
if (textureId != null) {
|
||||
Future.microtask(() => state.textureId.value = textureId);
|
||||
@ -144,11 +139,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
FlutterPcmSound.setLogLevel(LogLevel.none);
|
||||
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
|
||||
// 设置 feed 阈值
|
||||
if (Platform.isAndroid) {
|
||||
FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理
|
||||
} else {
|
||||
FlutterPcmSound.setFeedThreshold(2000); // 非 Android 平台的处理
|
||||
}
|
||||
// if (Platform.isAndroid) {
|
||||
// FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理
|
||||
// } else {
|
||||
// FlutterPcmSound.setFeedThreshold(4096); // 非 Android 平台的处理
|
||||
// }
|
||||
}
|
||||
|
||||
/// 挂断
|
||||
@ -499,15 +494,18 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
/// 播放音频数据
|
||||
void _playAudioData(TalkData talkData) async {
|
||||
if (state.isOpenVoice.value && state.isLoading.isFalse) {
|
||||
final list =
|
||||
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
|
||||
// // 将 PCM 数据转换为 PcmArrayInt16
|
||||
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
|
||||
List<int> encodedData = G711Tool.decode(talkData.content, 0); // 0表示A-law
|
||||
// 将 PCM 数据转换为 PcmArrayInt16
|
||||
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(encodedData);
|
||||
FlutterPcmSound.feed(fromList);
|
||||
if (!state.isPlaying.value) {
|
||||
AppLog.log('play');
|
||||
FlutterPcmSound.play();
|
||||
state.isPlaying.value = true;
|
||||
}
|
||||
} else if (state.isOpenVoice.isFalse) {
|
||||
FlutterPcmSound.pause();
|
||||
state.isPlaying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -573,8 +571,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// 启动监听音视频数据流
|
||||
_startListenTalkData();
|
||||
// 启动监听对讲状态
|
||||
_startListenTalkStatus();
|
||||
// 在没有监听成功之前赋值一遍状态
|
||||
@ -596,6 +592,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
// 初始化H264帧缓冲区
|
||||
state.h264FrameBuffer.clear();
|
||||
state.isProcessingFrame = false;
|
||||
|
||||
// 启动监听音视频数据流
|
||||
_startListenTalkData();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -639,7 +638,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
|
||||
// 清空已解码I帧集合
|
||||
_decodedIFrames.clear();
|
||||
|
||||
_startProcessingAudioTimer?.cancel();
|
||||
_startProcessingAudioTimer = null;
|
||||
_bufferedAudioFrames.clear();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@ -652,33 +653,12 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
|
||||
/// 更新发送预期数据
|
||||
void updateTalkExpect() {
|
||||
// 清晰度与VideoTypeE的映射
|
||||
final Map<String, VideoTypeE> qualityToVideoType = {
|
||||
'标清': VideoTypeE.H264,
|
||||
'高清': VideoTypeE.H264_720P,
|
||||
// 可扩展更多清晰度
|
||||
};
|
||||
TalkExpectReq talkExpectReq = TalkExpectReq();
|
||||
state.isOpenVoice.value = !state.isOpenVoice.value;
|
||||
// 根据当前清晰度动态设置videoType
|
||||
VideoTypeE currentVideoType =
|
||||
qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264;
|
||||
if (!state.isOpenVoice.value) {
|
||||
talkExpectReq = TalkExpectReq(
|
||||
videoType: [currentVideoType],
|
||||
audioType: [],
|
||||
);
|
||||
showToast('已静音'.tr);
|
||||
if (state.isOpenVoice.isTrue) {
|
||||
FlutterPcmSound.play();
|
||||
} else {
|
||||
talkExpectReq = TalkExpectReq(
|
||||
videoType: [currentVideoType],
|
||||
audioType: [AudioTypeE.G711],
|
||||
);
|
||||
FlutterPcmSound.pause();
|
||||
}
|
||||
|
||||
/// 修改发送预期数据
|
||||
StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||
talkExpect: talkExpectReq);
|
||||
}
|
||||
|
||||
/// 截图并保存到相册
|
||||
@ -762,8 +742,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
state.voiceProcessor = VoiceProcessor.instance;
|
||||
}
|
||||
|
||||
Timer? _startProcessingAudioTimer;
|
||||
|
||||
//开始录音
|
||||
Future<void> startProcessingAudio() async {
|
||||
|
||||
try {
|
||||
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
|
||||
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
|
||||
@ -781,7 +764,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
} on PlatformException catch (ex) {
|
||||
// state.errorMessage.value = 'Failed to start recorder: $ex';
|
||||
}
|
||||
state.isOpenVoice.value = false;
|
||||
}
|
||||
|
||||
/// 停止录音
|
||||
@ -803,51 +785,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
} finally {
|
||||
final bool? isRecording = await state.voiceProcessor?.isRecording();
|
||||
state.isRecordingAudio.value = isRecording!;
|
||||
state.isOpenVoice.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 音频帧处理
|
||||
Future<void> _onFrame(List<int> frame) async {
|
||||
// 添加最大缓冲限制
|
||||
if (_bufferedAudioFrames.length > state.frameLength * 3) {
|
||||
_bufferedAudioFrames.clear(); // 清空过多积累的数据
|
||||
return;
|
||||
}
|
||||
|
||||
// 首先应用固定增益提升基础音量
|
||||
List<int> amplifiedFrame = _applyGain(frame, 1.6);
|
||||
// 编码为G711数据
|
||||
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
// 使用相对时间戳
|
||||
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳
|
||||
int getFrameLength = state.frameLength;
|
||||
if (Platform.isIOS) {
|
||||
getFrameLength = state.frameLength * 2;
|
||||
}
|
||||
|
||||
// 添加发送间隔控制
|
||||
if (_bufferedAudioFrames.length >= state.frameLength) {
|
||||
try {
|
||||
await StartChartManage().sendTalkDataMessage(
|
||||
talkData: TalkData(
|
||||
content: _bufferedAudioFrames,
|
||||
contentType: TalkData_ContentTypeE.G711,
|
||||
durationMs: ms,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_bufferedAudioFrames.clear(); // 确保清理缓冲区
|
||||
}
|
||||
} else {
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误监听
|
||||
void _onError(VoiceProcessorException error) {
|
||||
AppLog.log(error.message!);
|
||||
_startProcessingAudioTimer?.cancel();
|
||||
_startProcessingAudioTimer = null;
|
||||
_bufferedAudioFrames.clear();
|
||||
}
|
||||
|
||||
// 添加音频增益处理方法
|
||||
@ -873,453 +814,53 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
|
||||
return result;
|
||||
}
|
||||
static const int chunkSize = 320; // 每次发送320字节(10ms G.711)
|
||||
static const int intervalMs = 40; // 每40ms发送一次(4个chunk)
|
||||
void _sendAudioChunk(Timer timer) async {
|
||||
if (_bufferedAudioFrames.length < chunkSize) {
|
||||
// 数据不足,等待下一周期
|
||||
return;
|
||||
}
|
||||
|
||||
/// 追加写入一帧到h264文件(需传入帧数据和帧类型frameType)
|
||||
Future<void> _appendH264FrameToFile(
|
||||
List<int> frameData, TalkDataH264Frame_FrameTypeE frameType) async {
|
||||
try {
|
||||
if (state.h264File == null) {
|
||||
await _initH264File();
|
||||
}
|
||||
// NALU分割函数,返回每个NALU的完整字节数组
|
||||
List<List<int>> splitNalus(List<int> data) {
|
||||
List<List<int>> nalus = [];
|
||||
int i = 0;
|
||||
while (i < data.length - 3) {
|
||||
int start = -1;
|
||||
int next = -1;
|
||||
if (data[i] == 0x00 && data[i + 1] == 0x00) {
|
||||
if (data[i + 2] == 0x01) {
|
||||
start = i;
|
||||
i += 3;
|
||||
} else if (i + 3 < data.length &&
|
||||
data[i + 2] == 0x00 &&
|
||||
data[i + 3] == 0x01) {
|
||||
start = i;
|
||||
i += 4;
|
||||
} else {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
next = i;
|
||||
while (next < data.length - 3) {
|
||||
if (data[next] == 0x00 &&
|
||||
data[next + 1] == 0x00 &&
|
||||
((data[next + 2] == 0x01) ||
|
||||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
|
||||
break;
|
||||
}
|
||||
next++;
|
||||
}
|
||||
nalus.add(data.sublist(start, next));
|
||||
i = next;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
int nalusTotalLen =
|
||||
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
|
||||
if (nalus.isEmpty && data.isNotEmpty) {
|
||||
nalus.add(data);
|
||||
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
|
||||
nalus.add(data.sublist(nalusTotalLen));
|
||||
}
|
||||
return nalus;
|
||||
}
|
||||
// 截取前 chunkSize 个字节
|
||||
final chunk = _bufferedAudioFrames.sublist(0, chunkSize);
|
||||
// 更新缓冲区:移除已发送部分
|
||||
_bufferedAudioFrames.removeRange(0, chunkSize);
|
||||
|
||||
// 优化:I帧前只缓存SPS/PPS/IDR,首次写入严格顺序
|
||||
if (!_hasWrittenFirstIFrame) {
|
||||
final nalus = splitNalus(frameData);
|
||||
List<List<int>> spsList = [];
|
||||
List<List<int>> ppsList = [];
|
||||
List<List<int>> idrList = [];
|
||||
for (final nalu in nalus) {
|
||||
int offset = 0;
|
||||
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
|
||||
if (nalu[2] == 0x01)
|
||||
offset = 3;
|
||||
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
|
||||
}
|
||||
if (nalu.length > offset) {
|
||||
int naluType = nalu[offset] & 0x1F;
|
||||
if (naluType == 7) {
|
||||
spsList.add(nalu);
|
||||
// AppLog.log('SPS内容: ' +
|
||||
// nalu
|
||||
// .map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
// .join(' '));
|
||||
} else if (naluType == 8) {
|
||||
ppsList.add(nalu);
|
||||
// AppLog.log('PPS内容: ' +
|
||||
// nalu
|
||||
// .map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
// .join(' '));
|
||||
} else if (naluType == 5) {
|
||||
idrList.add(nalu);
|
||||
}
|
||||
// 其他类型不缓存也不写入头部
|
||||
}
|
||||
}
|
||||
// 只在首次I帧写入前缓存,严格顺序写入
|
||||
if (spsList.isNotEmpty && ppsList.isNotEmpty && idrList.isNotEmpty) {
|
||||
for (final sps in spsList) {
|
||||
await _writeSingleFrameToFile(_ensureStartCode(sps));
|
||||
// AppLog.log('写入顺序: SPS');
|
||||
}
|
||||
for (final pps in ppsList) {
|
||||
await _writeSingleFrameToFile(_ensureStartCode(pps));
|
||||
// AppLog.log('写入顺序: PPS');
|
||||
}
|
||||
for (final idr in idrList) {
|
||||
await _writeSingleFrameToFile(_ensureStartCode(idr));
|
||||
// AppLog.log('写入顺序: IDR');
|
||||
}
|
||||
_hasWrittenFirstIFrame = true;
|
||||
} else {
|
||||
// 未收齐SPS/PPS/IDR则继续缓存,等待下次I帧
|
||||
if (spsList.isNotEmpty) _preIFrameCache.addAll(spsList);
|
||||
if (ppsList.isNotEmpty) _preIFrameCache.addAll(ppsList);
|
||||
if (idrList.isNotEmpty) _preIFrameCache.addAll(idrList);
|
||||
}
|
||||
} else {
|
||||
// 首帧后只写入IDR和P帧
|
||||
final nalus = splitNalus(frameData);
|
||||
for (final nalu in nalus) {
|
||||
int offset = 0;
|
||||
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
|
||||
if (nalu[2] == 0x01)
|
||||
offset = 3;
|
||||
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
|
||||
}
|
||||
if (nalu.length > offset) {
|
||||
int naluType = nalu[offset] & 0x1F;
|
||||
if (naluType == 5) {
|
||||
await _writeSingleFrameToFile(_ensureStartCode(nalu));
|
||||
// AppLog.log('写入顺序: IDR');
|
||||
} else if (naluType == 1) {
|
||||
await _writeSingleFrameToFile(_ensureStartCode(nalu));
|
||||
// AppLog.log('写入顺序: P帧');
|
||||
} else if (naluType == 7) {
|
||||
// AppLog.log('遇到新SPS,已忽略');
|
||||
} else if (naluType == 8) {
|
||||
// AppLog.log('遇到新PPS,已忽略');
|
||||
}
|
||||
// 其他类型不写入
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
AppLog.log('写入H264帧到文件失败: $e');
|
||||
// 获取时间戳(相对时间)
|
||||
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000;
|
||||
|
||||
print('Send chunk ${timer.tick}: ${chunk.take(10).toList()}...');
|
||||
|
||||
await StartChartManage().sendTalkDataMessage(
|
||||
talkData: TalkData(
|
||||
content: chunk,
|
||||
contentType: TalkData_ContentTypeE.G711,
|
||||
durationMs: ms,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 音频帧处理
|
||||
Future<void> _onFrame(List<int> frame) async {
|
||||
final applyGain = _applyGain(frame, 1.6);
|
||||
|
||||
// 编码为G711数据
|
||||
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0表示A-law
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
|
||||
|
||||
// 启动定时发送器(仅启动一次)
|
||||
if (_startProcessingAudioTimer == null && _bufferedAudioFrames.length > chunkSize) {
|
||||
_startProcessingAudioTimer = Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
|
||||
}
|
||||
}
|
||||
|
||||
// 统一NALU起始码为0x00000001
|
||||
List<int> _ensureStartCode(List<int> nalu) {
|
||||
if (nalu.length >= 4 &&
|
||||
nalu[0] == 0x00 &&
|
||||
nalu[1] == 0x00 &&
|
||||
nalu[2] == 0x00 &&
|
||||
nalu[3] == 0x01) {
|
||||
return nalu;
|
||||
} else if (nalu.length >= 3 &&
|
||||
nalu[0] == 0x00 &&
|
||||
nalu[1] == 0x00 &&
|
||||
nalu[2] == 0x01) {
|
||||
return [0x00, 0x00, 0x00, 0x01] + nalu.sublist(3);
|
||||
} else {
|
||||
return [0x00, 0x00, 0x00, 0x01] + nalu;
|
||||
}
|
||||
// 错误监听
|
||||
void _onError(VoiceProcessorException error) {
|
||||
AppLog.log(error.message!);
|
||||
}
|
||||
|
||||
/// 实际写入单帧到文件(带NALU头判断)
|
||||
Future<void> _writeSingleFrameToFile(List<int> frameData) async {
|
||||
bool hasNaluHeader = false;
|
||||
if (frameData.length >= 4 &&
|
||||
frameData[0] == 0x00 &&
|
||||
frameData[1] == 0x00 &&
|
||||
((frameData[2] == 0x01) ||
|
||||
(frameData[2] == 0x00 && frameData[3] == 0x01))) {
|
||||
hasNaluHeader = true;
|
||||
}
|
||||
if (hasNaluHeader) {
|
||||
await state.h264File!.writeAsBytes(frameData, mode: FileMode.append);
|
||||
} else {
|
||||
final List<int> naluHeader = [0x00, 0x00, 0x01];
|
||||
await state.h264File!
|
||||
.writeAsBytes(naluHeader + frameData, mode: FileMode.append);
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化h264文件
|
||||
Future<void> _initH264File() async {
|
||||
try {
|
||||
if (state.h264File != null) return;
|
||||
// 获取Download目录
|
||||
Directory? downloadsDir;
|
||||
if (Platform.isAndroid) {
|
||||
// Android 10+ 推荐用getExternalStorageDirectory()
|
||||
downloadsDir = await getExternalStorageDirectory();
|
||||
// 兼容部分ROM,优先用Download
|
||||
final downloadPath = '/storage/emulated/0/Download';
|
||||
if (Directory(downloadPath).existsSync()) {
|
||||
downloadsDir = Directory(downloadPath);
|
||||
}
|
||||
} else {
|
||||
downloadsDir = await getApplicationDocumentsDirectory();
|
||||
}
|
||||
final filePath =
|
||||
'${downloadsDir!.path}/video_${DateTime.now().millisecondsSinceEpoch}.h264';
|
||||
state.h264FilePath = filePath;
|
||||
state.h264File = File(filePath);
|
||||
if (!await state.h264File!.exists()) {
|
||||
await state.h264File!.create(recursive: true);
|
||||
}
|
||||
AppLog.log('H264文件初始化: $filePath');
|
||||
} catch (e) {
|
||||
AppLog.log('H264文件初始化失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 关闭h264文件
|
||||
Future<void> _closeH264File() async {
|
||||
try {
|
||||
if (state.h264File != null) {
|
||||
AppLog.log('H264文件已关闭: ${state.h264FilePath ?? ''}');
|
||||
}
|
||||
state.h264File = null;
|
||||
state.h264FilePath = null;
|
||||
_preIFrameCache.clear();
|
||||
_hasWrittenFirstIFrame = false;
|
||||
} catch (e) {
|
||||
AppLog.log('关闭H264文件时出错: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 从I帧数据中分割NALU并将SPS/PPS优先放入缓冲区(用于缓冲区发送)
|
||||
void _extractAndBufferSpsPpsForBuffer(
|
||||
List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
|
||||
List<List<int>> splitNalus(List<int> data) {
|
||||
List<List<int>> nalus = [];
|
||||
int i = 0;
|
||||
while (i < data.length - 3) {
|
||||
int start = -1;
|
||||
int next = -1;
|
||||
if (data[i] == 0x00 && data[i + 1] == 0x00) {
|
||||
if (data[i + 2] == 0x01) {
|
||||
start = i;
|
||||
i += 3;
|
||||
} else if (i + 3 < data.length &&
|
||||
data[i + 2] == 0x00 &&
|
||||
data[i + 3] == 0x01) {
|
||||
start = i;
|
||||
i += 4;
|
||||
} else {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
next = i;
|
||||
while (next < data.length - 3) {
|
||||
if (data[next] == 0x00 &&
|
||||
data[next + 1] == 0x00 &&
|
||||
((data[next + 2] == 0x01) ||
|
||||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
|
||||
break;
|
||||
}
|
||||
next++;
|
||||
}
|
||||
nalus.add(data.sublist(start, next));
|
||||
i = next;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
int nalusTotalLen =
|
||||
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
|
||||
if (nalus.isEmpty && data.isNotEmpty) {
|
||||
nalus.add(data);
|
||||
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
|
||||
nalus.add(data.sublist(nalusTotalLen));
|
||||
}
|
||||
return nalus;
|
||||
}
|
||||
|
||||
final nalus = splitNalus(frameData);
|
||||
for (final nalu in nalus) {
|
||||
int offset = 0;
|
||||
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
|
||||
if (nalu[2] == 0x01)
|
||||
offset = 3;
|
||||
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
|
||||
}
|
||||
if (nalu.length > offset) {
|
||||
int naluType = nalu[offset] & 0x1F;
|
||||
if (naluType == 7) {
|
||||
// SPS
|
||||
hasSps = true;
|
||||
// 只在首次或内容变化时更新缓存
|
||||
if (spsCache == null || !_listEquals(spsCache!, nalu)) {
|
||||
spsCache = List<int>.from(nalu);
|
||||
}
|
||||
} else if (naluType == 8) {
|
||||
// PPS
|
||||
hasPps = true;
|
||||
if (ppsCache == null || !_listEquals(ppsCache!, nalu)) {
|
||||
ppsCache = List<int>.from(nalu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:List比较工具
|
||||
bool _listEquals(List<int> a, List<int> b) {
|
||||
if (a.length != b.length) return false;
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 新增:I帧处理方法
|
||||
// void _handleIFrameWithSpsPpsAndIdr(
|
||||
// List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
|
||||
// // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧)
|
||||
// state.h264FrameBuffer.clear();
|
||||
// _extractAndBufferSpsPpsForBuffer(
|
||||
// frameData, durationMs, frameSeq, frameSeqI);
|
||||
// // 只要缓存有SPS/PPS就先写入,再写I帧本体(只写IDR)
|
||||
// if (spsCache == null || ppsCache == null) {
|
||||
// // 没有SPS/PPS缓存,丢弃本次I帧
|
||||
// return;
|
||||
// }
|
||||
// // 先写入SPS/PPS
|
||||
// _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
|
||||
// frameSeq, frameSeqI);
|
||||
// _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
|
||||
// frameSeq, frameSeqI);
|
||||
// // 分割I帧包,只写入IDR(type 5)
|
||||
// List<List<int>> nalus = [];
|
||||
// int i = 0;
|
||||
// List<int> data = frameData;
|
||||
// while (i < data.length - 3) {
|
||||
// int start = -1;
|
||||
// int next = -1;
|
||||
// if (data[i] == 0x00 && data[i + 1] == 0x00) {
|
||||
// if (data[i + 2] == 0x01) {
|
||||
// start = i;
|
||||
// i += 3;
|
||||
// } else if (i + 3 < data.length &&
|
||||
// data[i + 2] == 0x00 &&
|
||||
// data[i + 3] == 0x01) {
|
||||
// start = i;
|
||||
// i += 4;
|
||||
// } else {
|
||||
// i++;
|
||||
// continue;
|
||||
// }
|
||||
// next = i;
|
||||
// while (next < data.length - 3) {
|
||||
// if (data[next] == 0x00 &&
|
||||
// data[next + 1] == 0x00 &&
|
||||
// ((data[next + 2] == 0x01) ||
|
||||
// (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
|
||||
// break;
|
||||
// }
|
||||
// next++;
|
||||
// }
|
||||
// nalus.add(data.sublist(start, next));
|
||||
// i = next;
|
||||
// } else {
|
||||
// i++;
|
||||
// }
|
||||
// }
|
||||
// int nalusTotalLen =
|
||||
// nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
|
||||
// if (nalus.isEmpty && data.isNotEmpty) {
|
||||
// nalus.add(data);
|
||||
// } else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
|
||||
// nalus.add(data.sublist(nalusTotalLen));
|
||||
// }
|
||||
// for (final nalu in nalus) {
|
||||
// int offset = 0;
|
||||
// if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
|
||||
// if (nalu[2] == 0x01)
|
||||
// offset = 3;
|
||||
// else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
|
||||
// }
|
||||
// if (nalu.length > offset) {
|
||||
// int naluType = nalu[offset] & 0x1F;
|
||||
// if (naluType == 5) {
|
||||
// _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs,
|
||||
// frameSeq, frameSeqI);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 新增:P帧处理方法
|
||||
// void _handlePFrame(
|
||||
// List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
|
||||
// // 只写入P帧(type 1)
|
||||
// List<List<int>> nalus = [];
|
||||
// int i = 0;
|
||||
// List<int> data = frameData;
|
||||
// while (i < data.length - 3) {
|
||||
// int start = -1;
|
||||
// int next = -1;
|
||||
// if (data[i] == 0x00 && data[i + 1] == 0x00) {
|
||||
// if (data[i + 2] == 0x01) {
|
||||
// start = i;
|
||||
// i += 3;
|
||||
// } else if (i + 3 < data.length &&
|
||||
// data[i + 2] == 0x00 &&
|
||||
// data[i + 3] == 0x01) {
|
||||
// start = i;
|
||||
// i += 4;
|
||||
// } else {
|
||||
// i++;
|
||||
// continue;
|
||||
// }
|
||||
// next = i;
|
||||
// while (next < data.length - 3) {
|
||||
// if (data[next] == 0x00 &&
|
||||
// data[next + 1] == 0x00 &&
|
||||
// ((data[next + 2] == 0x01) ||
|
||||
// (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
|
||||
// break;
|
||||
// }
|
||||
// next++;
|
||||
// }
|
||||
// nalus.add(data.sublist(start, next));
|
||||
// i = next;
|
||||
// } else {
|
||||
// i++;
|
||||
// }
|
||||
// }
|
||||
// int nalusTotalLen =
|
||||
// nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
|
||||
// if (nalus.isEmpty && data.isNotEmpty) {
|
||||
// nalus.add(data);
|
||||
// } else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
|
||||
// nalus.add(data.sublist(nalusTotalLen));
|
||||
// }
|
||||
// for (final nalu in nalus) {
|
||||
// int offset = 0;
|
||||
// if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
|
||||
// if (nalu[2] == 0x01)
|
||||
// offset = 3;
|
||||
// else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
|
||||
// }
|
||||
// if (nalu.length > offset) {
|
||||
// int naluType = nalu[offset] & 0x1F;
|
||||
// if (naluType == 1) {
|
||||
// _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs,
|
||||
// frameSeq, frameSeqI);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 切换清晰度的方法,后续补充具体实现
|
||||
void onQualityChanged(String quality) async {
|
||||
state.currentQuality.value = quality;
|
||||
@ -1432,6 +973,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
// 判断数据类型,进行分发处理
|
||||
switch (contentType) {
|
||||
case TalkData_ContentTypeE.G711:
|
||||
if (!state.isOpenVoice.value) {
|
||||
return;
|
||||
}
|
||||
if (state.audioBuffer.length >= audioBufferSize) {
|
||||
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
||||
}
|
||||
|
||||
@ -558,8 +558,11 @@ class TalkViewLogic extends BaseGetXController {
|
||||
state.voiceProcessor = VoiceProcessor.instance;
|
||||
}
|
||||
|
||||
Timer? _startProcessingAudioTimer;
|
||||
|
||||
//开始录音
|
||||
Future<void> startProcessingAudio() async {
|
||||
|
||||
try {
|
||||
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
|
||||
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
|
||||
@ -577,7 +580,6 @@ class TalkViewLogic extends BaseGetXController {
|
||||
} on PlatformException catch (ex) {
|
||||
// state.errorMessage.value = 'Failed to start recorder: $ex';
|
||||
}
|
||||
state.isOpenVoice.value = false;
|
||||
}
|
||||
|
||||
/// 停止录音
|
||||
@ -599,45 +601,51 @@ class TalkViewLogic extends BaseGetXController {
|
||||
} finally {
|
||||
final bool? isRecording = await state.voiceProcessor?.isRecording();
|
||||
state.isRecordingAudio.value = isRecording!;
|
||||
state.isOpenVoice.value = true;
|
||||
}
|
||||
_startProcessingAudioTimer?.cancel();
|
||||
_startProcessingAudioTimer = null;
|
||||
_bufferedAudioFrames.clear();
|
||||
}
|
||||
|
||||
static const int chunkSize = 320; // 每次发送320字节(10ms G.711)
|
||||
static const int intervalMs = 40; // 每40ms发送一次(4个chunk)
|
||||
void _sendAudioChunk(Timer timer) async {
|
||||
if (_bufferedAudioFrames.length < chunkSize) {
|
||||
// 数据不足,等待下一周期
|
||||
return;
|
||||
}
|
||||
|
||||
// 截取前 chunkSize 个字节
|
||||
final chunk = _bufferedAudioFrames.sublist(0, chunkSize);
|
||||
// 更新缓冲区:移除已发送部分
|
||||
_bufferedAudioFrames.removeRange(0, chunkSize);
|
||||
|
||||
// 获取时间戳(相对时间)
|
||||
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000;
|
||||
|
||||
print('Send chunk ${timer.tick}: ${chunk.take(10).toList()}...');
|
||||
|
||||
await StartChartManage().sendTalkDataMessage(
|
||||
talkData: TalkData(
|
||||
content: chunk,
|
||||
contentType: TalkData_ContentTypeE.G711,
|
||||
durationMs: ms,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 音频帧处理
|
||||
Future<void> _onFrame(List<int> frame) async {
|
||||
// 添加最大缓冲限制
|
||||
if (_bufferedAudioFrames.length > state.frameLength * 3) {
|
||||
_bufferedAudioFrames.clear(); // 清空过多积累的数据
|
||||
return;
|
||||
}
|
||||
final applyGain = _applyGain(frame, 1.6);
|
||||
|
||||
// 首先应用固定增益提升基础音量
|
||||
List<int> amplifiedFrame = _applyGain(frame, 1.6);
|
||||
// 编码为G711数据
|
||||
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law
|
||||
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0表示A-law
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
// 使用相对时间戳
|
||||
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳
|
||||
int getFrameLength = state.frameLength;
|
||||
if (Platform.isIOS) {
|
||||
getFrameLength = state.frameLength * 2;
|
||||
}
|
||||
|
||||
// 添加发送间隔控制
|
||||
if (_bufferedAudioFrames.length >= state.frameLength) {
|
||||
try {
|
||||
await StartChartManage().sendTalkDataMessage(
|
||||
talkData: TalkData(
|
||||
content: _bufferedAudioFrames,
|
||||
contentType: TalkData_ContentTypeE.G711,
|
||||
durationMs: ms,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_bufferedAudioFrames.clear(); // 确保清理缓冲区
|
||||
}
|
||||
} else {
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
|
||||
// 启动定时发送器(仅启动一次)
|
||||
if (_startProcessingAudioTimer == null && _bufferedAudioFrames.length > chunkSize) {
|
||||
_startProcessingAudioTimer = Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,8 @@ class CommonItem extends StatelessWidget {
|
||||
this.rightWidget,
|
||||
this.isTipsImg,
|
||||
this.action,
|
||||
this.leftTitleMaxWidth, // 新增属性
|
||||
this.leftTitleMaxWidth, // 新增属性
|
||||
this.leftTitleStyle, // 新增属性
|
||||
this.tipsImgAction})
|
||||
: super(key: key);
|
||||
String? leftTitel;
|
||||
@ -35,6 +36,7 @@ class CommonItem extends StatelessWidget {
|
||||
bool? setHeight;
|
||||
bool? isTipsImg;
|
||||
bool? isPadding;
|
||||
TextStyle? leftTitleStyle; // 新增属性
|
||||
final double? leftTitleMaxWidth; // 新增属性声明
|
||||
|
||||
@override
|
||||
@ -65,7 +67,7 @@ class CommonItem extends StatelessWidget {
|
||||
),
|
||||
child: Text(
|
||||
leftTitel!,
|
||||
style: TextStyle(fontSize: 22.sp),
|
||||
style: leftTitleStyle ?? TextStyle(fontSize: 22.sp),
|
||||
overflow: TextOverflow.ellipsis, // 超出部分显示省略号
|
||||
maxLines: 3, // 最多显示2行
|
||||
),
|
||||
|
||||
@ -131,6 +131,13 @@ class ReadMessageRefreshUI {
|
||||
ReadMessageRefreshUI();
|
||||
}
|
||||
|
||||
/// 刷新接收到对讲消息后将消息设置为已读
|
||||
class ReadTalkMessageRefreshUI {
|
||||
ReadTalkMessageRefreshUI(this.lockName);
|
||||
|
||||
String lockName;
|
||||
}
|
||||
|
||||
/// 刷新电子钥匙列表
|
||||
class ElectronicKeyListRefreshUI {
|
||||
ElectronicKeyListRefreshUI();
|
||||
|
||||
@ -206,130 +206,130 @@ extension ExtensionLanguageType on LanguageType {
|
||||
var str = '';
|
||||
switch (this) {
|
||||
case LanguageType.english:
|
||||
str = '英文'.tr;
|
||||
str = '英文'.tr + '(English)';
|
||||
break;
|
||||
case LanguageType.chinese:
|
||||
str = '简体中文'.tr;
|
||||
str = '简体中文'.tr + '(Simplified Chinese)';
|
||||
break;
|
||||
case LanguageType.traditionalChineseTW:
|
||||
str = '繁体中文(中国台湾)'.tr;
|
||||
str = '繁体中文(中国台湾)'.tr + '(Traditional Chinese TW)';
|
||||
break;
|
||||
case LanguageType.traditionalChineseHK:
|
||||
str = '繁体中文(中国香港)'.tr;
|
||||
str = '繁体中文(中国香港)'.tr + '(Traditional Chinese HK)';
|
||||
break;
|
||||
case LanguageType.french:
|
||||
str = '法语'.tr;
|
||||
str = '法语'.tr + '(French)';
|
||||
break;
|
||||
case LanguageType.russian:
|
||||
str = '俄语'.tr;
|
||||
str = '俄语'.tr + '(Russian)';
|
||||
break;
|
||||
case LanguageType.german:
|
||||
str = '德语'.tr;
|
||||
str = '德语'.tr + '(German)';
|
||||
break;
|
||||
case LanguageType.japanese:
|
||||
str = '日语'.tr;
|
||||
str = '日语'.tr + '(Japanese)';
|
||||
break;
|
||||
case LanguageType.korean:
|
||||
str = '韩语'.tr;
|
||||
str = '韩语'.tr + '(Korean)';
|
||||
break;
|
||||
case LanguageType.italian:
|
||||
str = '意大利语'.tr;
|
||||
str = '意大利语'.tr + '(Italian)';
|
||||
break;
|
||||
case LanguageType.portuguese:
|
||||
str = '葡萄牙语'.tr;
|
||||
str = '葡萄牙语'.tr + '(Portuguese)';
|
||||
break;
|
||||
case LanguageType.spanish:
|
||||
str = '西班牙语'.tr;
|
||||
str = '西班牙语'.tr + '(Spanish)';
|
||||
break;
|
||||
case LanguageType.arabic:
|
||||
str = '阿拉伯语'.tr;
|
||||
str = '阿拉伯语'.tr + '(Arabic)';
|
||||
break;
|
||||
case LanguageType.vietnamese:
|
||||
str = '越南语'.tr;
|
||||
str = '越南语'.tr + '(Vietnamese)';
|
||||
break;
|
||||
case LanguageType.malay:
|
||||
str = '马来语'.tr;
|
||||
str = '马来语'.tr + '(Malay)';
|
||||
break;
|
||||
case LanguageType.dutch:
|
||||
str = '荷兰语'.tr;
|
||||
str = '荷兰语'.tr + '(Dutch)';
|
||||
break;
|
||||
case LanguageType.romanian:
|
||||
str = '罗马尼亚语'.tr;
|
||||
str = '罗马尼亚语'.tr + '(Romanian)';
|
||||
break;
|
||||
case LanguageType.lithuanian:
|
||||
str = '立陶宛语'.tr;
|
||||
str = '立陶宛语'.tr + '(Lithuanian)';
|
||||
break;
|
||||
case LanguageType.swedish:
|
||||
str = '瑞典语'.tr;
|
||||
str = '瑞典语'.tr + '(Swedish)';
|
||||
break;
|
||||
case LanguageType.estonian:
|
||||
str = '爱沙尼亚语'.tr;
|
||||
str = '爱沙尼亚语'.tr + '(Estonian)';
|
||||
break;
|
||||
case LanguageType.polish:
|
||||
str = '波兰语'.tr;
|
||||
str = '波兰语'.tr + '(Polish)';
|
||||
break;
|
||||
case LanguageType.slovak:
|
||||
str = '斯洛伐克语'.tr;
|
||||
str = '斯洛伐克语'.tr + '(Slovak)';
|
||||
break;
|
||||
case LanguageType.czech:
|
||||
str = '捷克语'.tr;
|
||||
str = '捷克语'.tr + '(Czech)';
|
||||
break;
|
||||
case LanguageType.greek:
|
||||
str = '希腊语'.tr;
|
||||
str = '希腊语'.tr + '(Greek)';
|
||||
break;
|
||||
case LanguageType.hebrew:
|
||||
str = '希伯来语'.tr;
|
||||
str = '希伯来语'.tr + '(Hebrew)';
|
||||
break;
|
||||
case LanguageType.serbian:
|
||||
str = '塞尔维亚语'.tr;
|
||||
str = '塞尔维亚语'.tr + '(Serbian)';
|
||||
break;
|
||||
case LanguageType.turkish:
|
||||
str = '土耳其语'.tr;
|
||||
str = '土耳其语'.tr + '(Turkish)';
|
||||
break;
|
||||
case LanguageType.hungarian:
|
||||
str = '匈牙利语'.tr;
|
||||
str = '匈牙利语'.tr + '(Hungarian)';
|
||||
break;
|
||||
case LanguageType.bulgarian:
|
||||
str = '保加利亚语'.tr;
|
||||
str = '保加利亚语'.tr + '(Bulgarian)';
|
||||
break;
|
||||
case LanguageType.kazakh:
|
||||
str = '哈萨克斯坦语'.tr;
|
||||
str = '哈萨克斯坦语'.tr + '(Kazakh)';
|
||||
break;
|
||||
case LanguageType.bengali:
|
||||
str = '孟加拉语'.tr;
|
||||
str = '孟加拉语'.tr + '(Bengali)';
|
||||
break;
|
||||
case LanguageType.croatian:
|
||||
str = '克罗地亚语'.tr;
|
||||
str = '克罗地亚语'.tr + '(Croatian)';
|
||||
break;
|
||||
case LanguageType.thai:
|
||||
str = '泰语'.tr;
|
||||
str = '泰语'.tr + '(Thai)';
|
||||
break;
|
||||
case LanguageType.indonesian:
|
||||
str = '印度尼西亚语'.tr;
|
||||
str = '印度尼西亚语'.tr + '(Indonesian)';
|
||||
break;
|
||||
case LanguageType.finnish:
|
||||
str = '芬兰语'.tr;
|
||||
str = '芬兰语'.tr + '(Finnish)';
|
||||
break;
|
||||
case LanguageType.danish:
|
||||
str = '丹麦语'.tr;
|
||||
str = '丹麦语'.tr + '(Danish)';
|
||||
break;
|
||||
case LanguageType.ukrainian:
|
||||
str = '乌克兰语'.tr;
|
||||
str = '乌克兰语'.tr + '(Ukrainian)';
|
||||
break;
|
||||
case LanguageType.hindi:
|
||||
str = '印地语'.tr;
|
||||
str = '印地语'.tr + '(Hindi)';
|
||||
break;
|
||||
case LanguageType.urdu:
|
||||
str = '乌尔都语'.tr;
|
||||
str = '乌尔都语'.tr + '(Urdu)';
|
||||
break;
|
||||
case LanguageType.armenian:
|
||||
str = '亚美尼亚语'.tr;
|
||||
str = '亚美尼亚语'.tr + '(Armenian)';
|
||||
break;
|
||||
case LanguageType.georgian:
|
||||
str = '格鲁吉亚语'.tr;
|
||||
str = '格鲁吉亚语'.tr + '(Georgian)';
|
||||
break;
|
||||
case LanguageType.brazilianPortuguese:
|
||||
str = '巴西葡萄牙语'.tr;
|
||||
str = '巴西葡萄牙语'.tr + '(Brazilian Portuguese)';
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user