Merge branch 'release_sky' into 'master_sky'

Release sky

See merge request StarlockTeam/app-starlock!272
This commit is contained in:
李仪 2025-09-03 09:46:21 +00:00
commit 46c9bc7a29
84 changed files with 45667 additions and 44745 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1167,5 +1167,11 @@
"锁语音包设置": "Lock voice package settings", "锁语音包设置": "Lock voice package settings",
"(中国台湾)": "(中国台湾)", "(中国台湾)": "(中国台湾)",
"男声": "male voice", "男声": "male voice",
"女声": "female voice" "女声": "female voice",
"您的图像和视频数据仅保留": "Your image and video data is only retained",
"后图像和视频数据将会失效,开通": "After that, the image and video data will be invalid and activated",
"云存会员": "Cloud Storage Membership",
"服务,图像视频信息随心存!": "Service, image and video information are at your heart!",
"图像": "image",
"视频": "Video"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1162,5 +1162,11 @@
"锁语音包设置": "鎖語音包設定", "锁语音包设置": "鎖語音包設定",
"(中国台湾)": "(中国台湾)", "(中国台湾)": "(中国台湾)",
"男声": "男聲", "男声": "男聲",
"女声": "女聲" "女声": "女聲",
"您的图像和视频数据仅保留": "您的圖像和視頻數據僅保留",
"后图像和视频数据将会失效,开通": "后圖像和視頻數據將會失效,開通",
"云存会员": "雲存會員",
"服务,图像视频信息随心存!": "服務,圖像視頻資訊隨心存!",
"图像": "圖像",
"视频": "視頻"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1172,5 +1172,11 @@
"语音包设置": "语音包设置", "语音包设置": "语音包设置",
"(中国台湾)": "(中国台湾)", "(中国台湾)": "(中国台湾)",
"男声": "男声", "男声": "男声",
"女声": "女声" "女声": "女声",
"您的图像和视频数据仅保留": "您的图像和视频数据仅保留",
"后图像和视频数据将会失效,开通": "后图像和视频数据将会失效,开通",
"云存会员": "云存会员",
"服务,图像视频信息随心存!": "服务,图像视频信息随心存!",
"图像": "图像",
"视频": "视频"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1166,5 +1166,11 @@
"语音包设置": "Configurações do pacote de voz", "语音包设置": "Configurações do pacote de voz",
"(中国台湾)": "(中国台湾)", "(中国台湾)": "(中国台湾)",
"男声": "Macho", "男声": "Macho",
"女声": "Garota" "女声": "Garota",
"您的图像和视频数据仅保留": "Seus dados de imagem e vídeo são retidos apenas",
"后图像和视频数据将会失效,开通": "Depois disso, os dados de imagem e vídeo serão inválidos e ativados",
"云存会员": "Associação de armazenamento em nuvem",
"服务,图像视频信息随心存!": "Informações de serviço, imagem e vídeo estão no seu coração!",
"图像": "imagem",
"视频": "Vídeo"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Закључајте подешавања говорног пакета", "锁语音包设置": "Закључајте подешавања говорног пакета",
"(中国台湾)": "(中国台湾)", "(中国台湾)": "(中国台湾)",
"男声": "мушки глас", "男声": "мушки глас",
"女声": "женски глас" "女声": "женски глас",
"您的图像和视频数据仅保留": "Ваши подаци о слици и видео записима се задржавају само",
"后图像和视频数据将会失效,开通": "Након тога, сликовни и видео подаци ће бити неважећи и активирани",
"云存会员": "Чланство у облаку за складиштење",
"服务,图像视频信息随心存!": "Сервис , слике и видео информације су у вашем срцу!",
"图像": "Слика",
"视频": "Пријава"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "鎖語音包設定", "锁语音包设置": "鎖語音包設定",
"(中国台湾)": "(中国台湾)", "(中国台湾)": "(中国台湾)",
"男声": "男聲", "男声": "男聲",
"女声": "女聲" "女声": "女聲",
"您的图像和视频数据仅保留": "您的圖像和視頻數據僅保留",
"后图像和视频数据将会失效,开通": "后圖像和視頻數據將會失效,開通",
"云存会员": "雲存會員",
"服务,图像视频信息随心存!": "服務,圖像視頻資訊隨心存!",
"图像": "圖像",
"视频": "視頻"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -63,7 +63,6 @@
"授权管理员拥有操作这把锁的重要权限,请确保只发给我你信任的人": "授权管理员拥有操作这把锁的重要权限,请确保只发给我你信任的人", "授权管理员拥有操作这把锁的重要权限,请确保只发给我你信任的人": "授权管理员拥有操作这把锁的重要权限,请确保只发给我你信任的人",
"功能开启后,你将可以通过网关远程开锁。此功能的开启和关闭只能在锁附近通过手机蓝牙进行。": "功能开启后,你将可以通过网关远程开锁。此功能的开启和关闭只能在锁附近通过手机蓝牙进行。", "功能开启后,你将可以通过网关远程开锁。此功能的开启和关闭只能在锁附近通过手机蓝牙进行。": "功能开启后,你将可以通过网关远程开锁。此功能的开启和关闭只能在锁附近通过手机蓝牙进行。",
"此功能的开启和关闭只能在锁附近通过手机蓝牙进行": "此功能的开启和关闭只能在锁附近通过手机蓝牙进行", "此功能的开启和关闭只能在锁附近通过手机蓝牙进行": "此功能的开启和关闭只能在锁附近通过手机蓝牙进行",
"功能开启后,你将可以通过网关远程开锁。": "功能开启后,你将可以通过网关远程开锁。", "功能开启后,你将可以通过网关远程开锁。": "功能开启后,你将可以通过网关远程开锁。",
"排列方式": "排列方式", "排列方式": "排列方式",
"早到榜": "早到榜", "早到榜": "早到榜",
@ -1102,7 +1101,7 @@
"支持的国家": "支持的国家", "支持的国家": "支持的国家",
"支持的国家值": "美国、加拿大、英国、澳大利亚、印度、德国、法国、意大利、西班牙、日本", "支持的国家值": "美国、加拿大、英国、澳大利亚、印度、德国、法国、意大利、西班牙、日本",
"操作流程": "操作流程", "操作流程": "操作流程",
"操作流程值":"1 用智能锁APP添加锁和网关\n\n2 在APP里开启锁的远程开锁功能这个功能默认是关闭的。如果没有这个选项则锁不支持Alexa \n\n3 在Alexa中添加Skill并用智能锁APP的账号和密码进行授权。授权成功后就可以发现账号下的设备\n\n4 在Alexa app里找到锁开启语音开锁的功能并设置语言密码\n\n5 可以通过Alexa操作锁了", "操作流程值": "1 用智能锁APP添加锁和网关\n\n2 在APP里开启锁的远程开锁功能这个功能默认是关闭的。如果没有这个选项则锁不支持Alexa \n\n3 在Alexa中添加Skill并用智能锁APP的账号和密码进行授权。授权成功后就可以发现账号下的设备\n\n4 在Alexa app里找到锁开启语音开锁的功能并设置语言密码\n\n5 可以通过Alexa操作锁了",
"Google Home": "Google Home", "Google Home": "Google Home",
"Action name": "Action name", "Action name": "Action name",
"ScienerSmart": "ScienerSmart", "ScienerSmart": "ScienerSmart",
@ -1174,5 +1173,11 @@
"语音包设置": "语音包设置", "语音包设置": "语音包设置",
"(中国台湾)": "(中国台湾)", "(中国台湾)": "(中国台湾)",
"男声": "男声", "男声": "男声",
"女声": "女声" "女声": "女声",
"您的图像和视频数据仅保留": "您的图像和视频数据仅保留",
"后图像和视频数据将会失效,开通": "后图像和视频数据将会失效,开通",
"云存会员": "云存会员",
"服务,图像视频信息随心存!": "服务,图像视频信息随心存!",
"图像": "图像",
"视频": "视频"
} }

View 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[2];
errorWithStstus(status);
}
}

View 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[2];
errorWithStstus(status);
}
}

View File

@ -44,7 +44,9 @@ enum CommandType {
startVoicePackageConfigure, // 0x30A1 startVoicePackageConfigure, // 0x30A1
voicePackageConfigureProcess, // 0x30A2 voicePackageConfigureProcess, // 0x30A2
voicePackageConfigureConfirmation, // 0x30A3 voicePackageConfigureConfirmation, // 0x30A3
getDeviceModel, // 0x30A4 readLockCurrentVoicePacket, // 0x30A4
setLockCurrentVoicePacket, // 0x30A5
getDeviceModel, // 0x30A4
gatewayConfiguringWifi, // 0x30F4 gatewayConfiguringWifi, // 0x30F4
gatewayConfiguringWifiResult, // 0x30F5 gatewayConfiguringWifiResult, // 0x30F5
@ -210,7 +212,12 @@ extension ExtensionCommandType on CommandType {
break; break;
case 0x30A4: case 0x30A4:
{ {
type = CommandType.getDeviceModel; type = CommandType.readLockCurrentVoicePacket;
}
break;
case 0x30A5:
{
type = CommandType.setLockCurrentVoicePacket;
} }
break; break;
case 0x30F4: case 0x30F4:
@ -340,9 +347,12 @@ extension ExtensionCommandType on CommandType {
case CommandType.voicePackageConfigureConfirmation: case CommandType.voicePackageConfigureConfirmation:
type = 0x30A3; type = 0x30A3;
break; break;
case CommandType.getDeviceModel: case CommandType.readLockCurrentVoicePacket:
type = 0x30A4; type = 0x30A4;
break; break;
case CommandType.setLockCurrentVoicePacket:
type = 0x30A5;
break;
default: default:
type = 0x300A; type = 0x300A;
break; break;
@ -362,7 +372,8 @@ extension ExtensionCommandType on CommandType {
case CommandType.gatewayGetWifiList: case CommandType.gatewayGetWifiList:
case CommandType.gatewayConfiguringWifi: case CommandType.gatewayConfiguringWifi:
case CommandType.gatewayGetStatus: case CommandType.gatewayGetStatus:
case CommandType.getDeviceModel: case CommandType.readLockCurrentVoicePacket:
case CommandType.setLockCurrentVoicePacket:
// //
type = 0x20; type = 0x20;
break; break;
@ -476,7 +487,10 @@ extension ExtensionCommandType on CommandType {
t = '语音包配置确认'; t = '语音包配置确认';
break; break;
case 0x30A4: case 0x30A4:
t = '获取设备型号'; t = '读取锁当前语音包';
break;
case 0x30A5:
t = '设置锁当前语音包';
break; break;
default: default:
t = '读星锁状态信息'; t = '读星锁状态信息';

View File

@ -18,9 +18,11 @@ import 'package:star_lock/blue/io_protocol/io_processOtaUpgrade.dart';
import 'package:star_lock/blue/io_protocol/io_readAdminPassword.dart'; import 'package:star_lock/blue/io_protocol/io_readAdminPassword.dart';
import 'package:star_lock/blue/io_protocol/io_readSupportFunctionsNoParameters.dart'; import 'package:star_lock/blue/io_protocol/io_readSupportFunctionsNoParameters.dart';
import 'package:star_lock/blue/io_protocol/io_readSupportFunctionsWithParameters.dart'; import 'package:star_lock/blue/io_protocol/io_readSupportFunctionsWithParameters.dart';
import 'package:star_lock/blue/io_protocol/io_readVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_referEventRecordTime.dart'; import 'package:star_lock/blue/io_protocol/io_referEventRecordTime.dart';
import 'package:star_lock/blue/io_protocol/io_setSupportFunctionsNoParameters.dart'; import 'package:star_lock/blue/io_protocol/io_setSupportFunctionsNoParameters.dart';
import 'package:star_lock/blue/io_protocol/io_setSupportFunctionsWithParameters.dart'; import 'package:star_lock/blue/io_protocol/io_setSupportFunctionsWithParameters.dart';
import 'package:star_lock/blue/io_protocol/io_setVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_timing.dart'; import 'package:star_lock/blue/io_protocol/io_timing.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigure.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_protocol/io_voicePackageConfigureProcess.dart';
@ -317,6 +319,18 @@ class CommandReciverManager {
commandType, data); commandType, data);
} }
break; break;
case CommandType.readLockCurrentVoicePacket:
{
reply =
ReadLockCurrentVoicePacketReply.parseData(commandType, data);
}
break;
case CommandType.setLockCurrentVoicePacket:
{
reply =
SetVoicePackageFinalResultReply.parseData(commandType, data);
}
break;
case CommandType.generalExtendedCommond: case CommandType.generalExtendedCommond:
{ {
// //

View File

@ -80,231 +80,236 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
), ),
], ],
), ),
body: ListView( body: GestureDetector(
padding: EdgeInsets.only(top: 120.h, left: 40.w, right: 40.w), onTap: (){
children: <Widget>[ FocusScope.of(context).unfocus();
Container( },
padding: EdgeInsets.all(10.w), child: ListView(
child: Center( padding: EdgeInsets.only(top: 120.h, left: 40.w, right: 40.w),
child: Image.asset('images/icon_main_sky_1024.png', children: <Widget>[
width: 110.w, height: 110.w))), Container(
SizedBox(height: 50.w), padding: EdgeInsets.all(10.w),
Obx(() => CommonItem( child: Center(
leftTitel: '你所在的国家/地区'.tr, child: Image.asset('images/icon_main_sky_1024.png',
rightTitle: '', width: 110.w, height: 110.w))),
isHaveLine: true, SizedBox(height: 50.w),
isPadding: false, Obx(() => CommonItem(
isHaveRightWidget: true, leftTitel: '你所在的国家/地区'.tr,
isHaveDirection: true, rightTitle: '',
rightWidget: Text( isHaveLine: true,
'${state.countryName} +${state.countryCode.value}', isPadding: false,
textAlign: TextAlign.end, isHaveRightWidget: true,
style: TextStyle( isHaveDirection: true,
fontSize: 22.sp, color: AppColors.darkGrayTextColor), rightWidget: Text(
), '${state.countryName} +${state.countryCode.value}',
action: () async { textAlign: TextAlign.end,
final result = style: TextStyle(
await Get.toNamed(Routers.selectCountryRegionPage); fontSize: 22.sp, color: AppColors.darkGrayTextColor),
if (result != null) { ),
result as Map<String, dynamic>; action: () async {
state.countryCode.value = result['code']; final result =
state.countryKey.value = result['countryName']; await Get.toNamed(Routers.selectCountryRegionPage);
logic.checkIpAction(); if (result != null) {
} result as Map<String, dynamic>;
}, state.countryCode.value = result['code'];
)), state.countryKey.value = result['countryName'];
LoginInput( logic.checkIpAction();
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();
}
} }
: null)), },
// SizedBox(height: 20.w), )),
// Obx(() => Visibility( LoginInput(
// visible: state.isCheckVerifyEnable.value, focusNode: logic.state.emailOrPhoneFocusNode,
// child: SubmitBtn( controller: state.emailOrPhoneController,
// btnName: '一键登录', onchangeAction: (v) {
// fontSize: 28.sp, logic.checkNext(state.emailOrPhoneController);
// borderRadius: 20.w, },
// padding: EdgeInsets.only(top: 25.w, bottom: 25.w), leftWidget: Padding(
// // isDisabled: state.canNext.value, padding: EdgeInsets.only(
// onClick: () { top: 30.w, bottom: 20.w, right: 5.w, left: 5.w),
// if (state.agree.value == false) { child: Image.asset(
// logic.showToast('请先同意用户协议及隐私政策'.tr); 'images/icon_login_account.png',
// return; width: 36.w,
// } else { height: 36.w,
// 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: () { hintText: '请输入手机号或者邮箱'.tr,
Navigator.pushNamed( // keyboardType: TextInputType.number,
context, Routers.starLockForgetPasswordPage); 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,
Expanded( // isSuffixIcon: 2,
child: SizedBox( leftWidget: Padding(
width: 10.sp, padding: EdgeInsets.only(
)), top: 30.w, bottom: 20.w, right: 5.w, left: 5.w),
Obx(() => Visibility( child: Image.asset(
visible: state.isCheckVerifyEnable.value && 'images/icon_login_password.png',
state.currentLanguage == 'zh_CN', width: 36.w,
child: GestureDetector( height: 36.w,
child: SizedBox( ),
// width: 150.w, ),
height: 50.h, hintText: '请输入密码'.tr,
// color: Colors.red, inputFormatters: <TextInputFormatter>[
child: Center( LengthLimitingTextInputFormatter(20),
child: Text('一键登录'.tr, ]),
style: TextStyle( // SizedBox(height: 15.h),
fontSize: 22.sp, Row(
color: AppColors.mainColor)), 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);
},
),
))
],
),
],
),
)); ));
} }

View File

@ -62,10 +62,8 @@ FutureOr<void> main() async {
} }
}); });
// //ToDo: // ios则初始化获取到voip token
// runApp(MultiProvider(providers: [ // token callkit
// ChangeNotifierProvider(create: (_) => DebugInfoModel()),
// ], child: MyApp(isLogin: isLogin)));
if (Platform.isIOS) { if (Platform.isIOS) {
CallKitHandler.setupListener(); CallKitHandler.setupListener();
String? token = await CallKitHandler.getVoipToken(); String? token = await CallKitHandler.getVoipToken();
@ -111,20 +109,4 @@ Future<void> privacySDKInitialization() async {
await jpushProvider.initJPushService(); await jpushProvider.initJPushService();
NotificationService().init(); // 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());
// }
// }
} }

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

View File

@ -125,4 +125,9 @@ class DoorLockLogDataItem {
data['recordDetailStr'] = recordDetailStr; data['recordDetailStr'] = recordDetailStr;
return data; return data;
} }
@override
String toString() {
return 'DoorLockLogDataItem{recordId: $recordId, lockId: $lockId, lockAlias: $lockAlias, recordType: $recordType, recordTypeName: $recordTypeName, username: $username, operateDate: $operateDate, imagesUrl: $imagesUrl, videoUrl: $videoUrl, headUrl: $headUrl, userid: $userid, keyboardPwd: $keyboardPwd, recordStr: $recordStr, recordDetailStr: $recordDetailStr}';
}
} }

View File

@ -3,11 +3,15 @@ import 'dart:async';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:star_lock/apm/apm_helper.dart'; import 'package:star_lock/apm/apm_helper.dart';
import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/common/XSConstantMacro/XSConstantMacro.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_entity.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_state.dart'; import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_state.dart';
import 'package:star_lock/main/lockDetail/lockOperatingRecord/lockOperatingRecordGetLastRecordTime_entity.dart'; import 'package:star_lock/main/lockDetail/lockOperatingRecord/lockOperatingRecordGetLastRecordTime_entity.dart';
import 'package:star_lock/mine/valueAddedServices/advancedFeaturesWeb/advancedFeaturesWeb_entity.dart';
import 'package:star_lock/tools/commonDataManage.dart'; import 'package:star_lock/tools/commonDataManage.dart';
import 'package:star_lock/tools/dateTool.dart'; import 'package:star_lock/tools/dateTool.dart';
import 'package:star_lock/tools/eventBusEventManage.dart'; import 'package:star_lock/tools/eventBusEventManage.dart';
@ -235,13 +239,15 @@ class DoorLockLogLogic extends BaseGetXController {
lockId: state.keyInfos.value.lockId!, lockId: state.keyInfos.value.lockId!,
lockEventType: state.dropdownValue.value, lockEventType: state.dropdownValue.value,
pageNo: pageNo, pageNo: pageNo,
pageSize: int.parse(pageSize), pageSize: 1000,
startDate: state.startDate.value, startDate: state.startDate.value,
endDate: state.endDate.value); endDate: state.endDate.value);
if (entity.errorCode!.codeIsSuccessful) { if (entity.errorCode!.codeIsSuccessful) {
// //
state.lockLogItemList.addAll(entity.data!.itemList!); state.lockLogItemList.addAll(entity.data!.itemList!);
state.lockLogItemList.refresh(); state.lockLogItemList.refresh();
state.weekEventList.addAll(entity.data!.itemList!);
state.weekEventList.refresh();
// //
pageNo++; pageNo++;
} }
@ -358,6 +364,7 @@ class DoorLockLogLogic extends BaseGetXController {
@override @override
Future<void> onInit() async { Future<void> onInit() async {
_setWeekRange();
super.onInit(); super.onInit();
// //
@ -370,6 +377,48 @@ 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();
}
getWebPlayUrl() async {
final AdvancedFeaturesWebEntity entity =
await ApiRepository.to.getServicePackageBuyUrl();
if (entity.errorCode!.codeIsSuccessful) {
state.cloudStorageWebViewUrl.value = entity.data!.cloudStorage!;
final uploadReportBuyRequest = await ApiRepository.to
.uploadReportBuyRequest(lockId: state.keyInfos.value.lockId!);
if (uploadReportBuyRequest.errorCode!.codeIsSuccessful) {
Get.toNamed(Routers.advancedFeaturesWebPage, arguments: <String, int>{
'webBuyType': XSConstantMacro.webBuyTypeCloudStorage,
});
}
}
}
@override @override
Future<void> onClose() async { Future<void> onClose() async {
super.onClose(); super.onClose();

View File

@ -1,14 +1,18 @@
import 'package:flustars/flustars.dart'; import 'package:flustars/flustars.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:star_lock/appRouters.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_entity.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_logic.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/doorLockLog_state.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/exportRecordDialog/exportRecordDialog_page.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/videoLog/videoLog_entity.dart';
import 'package:star_lock/main/lockDetail/videoLog/widget/full_screenImage_page.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'; import 'package:star_lock/main/lockDetail/videoLog/widget/video_thumbnail_image.dart';
@ -34,8 +38,39 @@ class DoorLockLogPage extends StatefulWidget {
} }
class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware { class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
final ScrollController _scrollController = ScrollController();
final DoorLockLogLogic logic = Get.put(DoorLockLogLogic()); final DoorLockLogLogic logic = Get.put(DoorLockLogLogic());
final DoorLockLogState state = Get.find<DoorLockLogLogic>().state; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -97,16 +132,31 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
topAdvancedCalendarWidget(), topAdvancedCalendarWidget(),
Divider(
height: 1,
color: AppColors.greyLineColor,
indent: 30.w,
endIndent: 30.w,
),
eventDropDownWidget(), eventDropDownWidget(),
Expanded(child: timeLineView()) 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 +202,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() { Widget topAdvancedCalendarWidget() {
final ThemeData theme = Theme.of(context); return Container(
return Theme( margin: EdgeInsets.only(top: 20.h, left: 30.w, bottom: 10.h, right: 20.w),
data: theme.copyWith( child: Column(
textTheme: theme.textTheme.copyWith( crossAxisAlignment: CrossAxisAlignment.start,
titleMedium: theme.textTheme.titleMedium!.copyWith( children: [
fontSize: 16, _buildWeekCalendar(),
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,
),
)),
),
], ],
), ),
); );
@ -269,38 +245,38 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16.w), borderRadius: BorderRadius.circular(16.w),
), ),
child: Obx(() => EasyRefreshTool( child: Obx(
onRefresh: () async { () => state.lockLogItemList.isNotEmpty
logic.mockNetworkDataRequest(isRefresh: true); ? Timeline.tileBuilder(
}, controller: _scrollController,
onLoad: () async { builder: _timelineBuilderWidget(),
logic.mockNetworkDataRequest(isRefresh: false); theme: TimelineThemeData(
}, nodePosition: 0.04, //
child: state.lockLogItemList.isNotEmpty connectorTheme: const ConnectorThemeData(
? Timeline.tileBuilder( thickness: 1.0,
builder: _timelineBuilderWidget(), color: AppColors.greyLineColor,
theme: TimelineThemeData( indent: 0.5,
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,
),
), ),
) indicatorTheme: const IndicatorThemeData(
: NoData())), size: 8.0,
color: AppColors.greyLineColor,
position: 0.4,
),
),
)
: NoData(),
),
); );
} }
String formatTimestampToHHmm(int timestampMs) { String formatTimestampToDateTimeYYYYMMDD(int timestampMs) {
// 1. DateTime DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
int timestampSec = timestampMs ~/ 1000; DateFormat formatter =
DateFormat('MM${''.tr}dd${''.tr}'); // 2025-08-18 14:30
return formatter.format(dateTime);
}
String formatTimestampToHHmm(int timestampMs) {
// 2. DateTime // 2. DateTime
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs); DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
@ -309,6 +285,17 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
return formatter.format(dateTime); return formatter.format(dateTime);
} }
bool _checkIsVideoOrImagesType(DoorLockLogDataItem item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
case 220:
return true;
default:
return false;
}
}
String _buildIDByType(DoorLockLogDataItem item) { String _buildIDByType(DoorLockLogDataItem item) {
final recordType = item.recordType; final recordType = item.recordType;
switch (recordType) { switch (recordType) {
@ -325,7 +312,8 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
return '${formatTimestampToHHmm(item.operateDate!)} ' + return '${formatTimestampToHHmm(item.operateDate!)} ' +
'密码'.tr + '密码'.tr +
'开锁'.tr + '开锁'.tr +
'${'昵称'.tr}${item.username}'+'${'密码'.tr}${item.keyboardPwd}'; '${'昵称'.tr}${item.username}' +
'${'密码'.tr}${item.keyboardPwd}';
case 30: case 30:
return '${formatTimestampToHHmm(item.operateDate!)} ' + return '${formatTimestampToHHmm(item.operateDate!)} ' +
''.tr + ''.tr +
@ -431,6 +419,20 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
itemCount: state.lockLogItemList.length, itemCount: state.lockLogItemList.length,
contentsBuilder: (BuildContext context, int index) { contentsBuilder: (BuildContext context, int index) {
final DoorLockLogDataItem timelineData = state.lockLogItemList[index]; final DoorLockLogDataItem timelineData = state.lockLogItemList[index];
// 👇 videoUrl build
int? firstVideoIndex = state.lockLogItemList
.indexWhere((item) => _checkIsVideoOrImagesType(item));
bool isInvalid = _checkIsVideoOrImagesType(timelineData) &&
((timelineData.imagesUrl == null &&
timelineData.videoUrl == null) ||
(timelineData.videoUrl == '' && timelineData.imagesUrl == ''));
String typeText = '';
if (timelineData.recordType == 130) {
typeText = '图像'.tr;
} else if (timelineData.recordType == 220) {
typeText = '视频'.tr;
}
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Get.toNamed( Get.toNamed(
@ -444,20 +446,45 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text(
'${formatTimestampToDateTimeYYYYMMDD(timelineData.operateDate!)}',
style: TextStyle(
fontSize: 20.sp,
)),
// 使 SingleChildScrollView // 使 SingleChildScrollView
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, // scrollDirection: Axis.horizontal, //
child: Text( child: RichText(
_buildIDByType(timelineData),
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: TextStyle( text: TextSpan(
color: _buildTextColorByType(timelineData), style: TextStyle(
fontSize: 24.sp, color: _buildTextColorByType(timelineData),
fontWeight: FontWeight.w600, fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
children: [
TextSpan(
text: _buildIDByType(timelineData) +
(isInvalid
? '${typeText}' +
'已失效'.tr +
''
: ''),
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Visibility(
visible: isInvalid,
child: Icon(
Icons.error,
size: 24.sp,
color: Colors.red,
),
),
),
],
), ),
//
maxLines: 1, maxLines: 1,
//
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@ -474,8 +501,71 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
), ),
), ),
SizedBox( SizedBox(
height: 20.h, height: 12.h,
), ),
Visibility(
visible: _checkIsVideoOrImagesType(timelineData) &&
index == firstVideoIndex,
child: GestureDetector(
onTap: () async {
await logic.getWebPlayUrl();
},
child: Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8.r)),
),
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
//
TextSpan(
text:
'${'您的图像和视频数据仅保留'.tr} ${state.rollingStorageDays.value} ${''.tr} ,${state.rollingStorageDays.value} ${''.tr} ${'后图像和视频数据将会失效,开通'.tr}',
style: TextStyle(
color: Colors.grey,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
height: 1.8,
),
),
// 🔥
TextSpan(
text: '云存会员'.tr,
style: TextStyle(
color: AppColors.mainColor,
fontSize: 22.sp,
fontWeight: FontWeight.w800,
//
decoration: TextDecoration.underline,
decorationThickness: 1.5,
height: 1.8,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
// 👉
print('点击了“云存会员”');
await logic.getWebPlayUrl();
// Navigator.push(context, MaterialPageRoute(builder: ...));
},
),
//
TextSpan(
text: '服务,图像视频信息随心存!'.tr,
style: TextStyle(
color: Colors.grey,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
height: 1.8,
),
),
],
),
),
),
),
)
], ],
), ),
), ),
@ -622,4 +712,56 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
} }
state.ifCurrentScreen.value = false; 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);
},
);
});
}
} }

View File

@ -1,5 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_rx/get_rx.dart';
import 'package:star_lock/common/XSConstantMacro/XSConstantMacro.dart'; import 'package:star_lock/common/XSConstantMacro/XSConstantMacro.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart'; import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
import 'package:star_lock/tools/advancedCalendar/src/controller.dart'; import 'package:star_lock/tools/advancedCalendar/src/controller.dart';
@ -13,10 +13,15 @@ class DoorLockLogState {
DoorLockLogState() { DoorLockLogState() {
keyInfos.value = Get.arguments['keyInfo']; keyInfos.value = Get.arguments['keyInfo'];
} }
final Rx<DoorLockLogEntity> lockLogEntity = DoorLockLogEntity().obs; final Rx<DoorLockLogEntity> lockLogEntity = DoorLockLogEntity().obs;
final Rx<LockListInfoItemEntity> keyInfos = LockListInfoItemEntity().obs; final Rx<LockListInfoItemEntity> keyInfos = LockListInfoItemEntity().obs;
final RxList<DoorLockLogDataItem> lockLogItemList = final RxList<DoorLockLogDataItem> lockLogItemList =
<DoorLockLogDataItem>[].obs; <DoorLockLogDataItem>[].obs;
final RxList<DoorLockLogDataItem> weekEventList =
<DoorLockLogDataItem>[].obs;
final RxList<DoorLockLogDataItem> dayEventList =
<DoorLockLogDataItem>[].obs;
final AdvancedCalendarController calendarControllerToday = final AdvancedCalendarController calendarControllerToday =
AdvancedCalendarController.today(); AdvancedCalendarController.today();
final AdvancedCalendarController calendarControllerCustom = final AdvancedCalendarController calendarControllerCustom =
@ -31,7 +36,7 @@ class DoorLockLogState {
.millisecondsSinceEpoch .millisecondsSinceEpoch
.obs; .obs;
final RxInt endDate = DateTime( 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)) .subtract(const Duration(milliseconds: 1))
.millisecondsSinceEpoch .millisecondsSinceEpoch
.obs; .obs;
@ -69,4 +74,6 @@ class DoorLockLogState {
int logCountPage = 10; // int logCountPage = 10; //
Rx<DateTime> currentSelectDate = DateTime.now().obs; Rx<DateTime> currentSelectDate = DateTime.now().obs;
bool isLockReceiveResponse = false; // bool isLockReceiveResponse = false; //
RxString cloudStorageWebViewUrl = ''.obs;
RxInt rollingStorageDays = 3.obs; //
} }

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

View File

@ -253,10 +253,10 @@ class AddFingerprintLogic extends BaseGetXController {
final List<int> getTokenList = changeStringListToIntList(token!); final List<int> getTokenList = changeStringListToIntList(token!);
String startTime = DateTool().dateToHNString(state.effectiveDateTime.value); String startTime = DateTool().dateToHNString(state.effectiveDateTime.value);
String endTime = DateTool().dateToHNString(state.failureDateTime.value); String endTime = DateTool().dateToHNString(state.failureDateTime.value);
if (F.isSKY) { // if (F.isSKY) {
startTime = '255:00'; // startTime = '255:00';
endTime = '255:00'; // endTime = '255:00';
} // }
final String command = SenderAddFingerprintWithTimeCycleCoercionCommand( final String command = SenderAddFingerprintWithTimeCycleCoercionCommand(
keyID: '1', keyID: '1',

View File

@ -89,27 +89,27 @@ class _CatEyeCustomModePageState extends State<CatEyeCustomModePage> {
SizedBox( SizedBox(
height: 30.h, height: 30.h,
), ),
Container( // Container(
margin: EdgeInsets.only(left: 20.w), // margin: EdgeInsets.only(left: 20.w),
child: CommonItem( // child: CommonItem(
leftTitel: '实时画面'.tr, // leftTitel: '实时画面'.tr,
rightTitle: state.realTimeMode.value, // rightTitle: state.realTimeMode.value,
isHaveLine: false, // isHaveLine: false,
isHaveDirection: true, // isHaveDirection: true,
isHaveRightWidget: false, // isHaveRightWidget: false,
action: () { // action: () {
Navigator.pushNamed(context, Routers.liveVideoPage, // Navigator.pushNamed(context, Routers.liveVideoPage,
arguments: { // arguments: {
'lockSetInfoData': state.lockSetInfoData.value, // 'lockSetInfoData': state.lockSetInfoData.value,
'catEyeConfigData': state.lockSetInfoData.value // 'catEyeConfigData': state.lockSetInfoData.value
.lockSettingInfo!.catEyeConfig!.isNotEmpty // .lockSettingInfo!.catEyeConfig!.isNotEmpty
? state.lockSetInfoData.value.lockSettingInfo! // ? state.lockSetInfoData.value.lockSettingInfo!
.catEyeConfig![0] // .catEyeConfig![0]
: null // : null
}); // });
}, // },
), // ),
) // )
], ],
), ),
), ),

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/blue/blue_manage.dart'; import 'package:star_lock/blue/blue_manage.dart';
import 'package:star_lock/blue/io_protocol/io_setSupportFunctionsWithParameters.dart'; import 'package:star_lock/blue/io_protocol/io_setSupportFunctionsWithParameters.dart';
import 'package:star_lock/blue/io_reply.dart'; import 'package:star_lock/blue/io_reply.dart';
@ -82,30 +83,31 @@ class CatEyeSetLogic extends BaseGetXController {
// //
cancelBlueConnetctToastTimer(); cancelBlueConnetctToastTimer();
dismissEasyLoading(); dismissEasyLoading();
AppLog.log('state.settingOptions.value:${state.settingOptions.value}');
switch (state.settingOptions.value) { switch (state.settingOptions.value) {
case 1: // case 1: //
{ {
updateAutoLightScreenConfig(); await updateAutoLightScreenConfig();
} }
break; break;
case 2: // case 2: //
{ {
updateStayWarnConfig(); await updateStayWarnConfig();
} }
break; break;
case 3: // case 3: //
{ {
updateAbnormalWarnConfig(); await updateAbnormalWarnConfig();
} }
break; break;
case 4: // case 4: //
{ {
updateLightScreenTimeConfig(); await updateLightScreenTimeConfig();
} }
break; break;
case 5: // case 5: //
{ {
updateCatEyeModeConfig(); await updateCatEyeModeConfig();
} }
break; break;
default: default:
@ -288,6 +290,7 @@ class CatEyeSetLogic extends BaseGetXController {
.catEyeConfig![0] .catEyeConfig![0]
.catEyeModeConfig .catEyeModeConfig
?.realTimeMode = state.catEyeConfig.value.realTimeMode; ?.realTimeMode = state.catEyeConfig.value.realTimeMode;
eventBus eventBus
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
} }
@ -456,6 +459,10 @@ class CatEyeSetLogic extends BaseGetXController {
} }
void sendBlueMessage() { void sendBlueMessage() {
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
final message = _buildCatEyeSetBlueMessage(); final message = _buildCatEyeSetBlueMessage();
BlueManage().blueSendData(BlueManage().connectDeviceName, BlueManage().blueSendData(BlueManage().connectDeviceName,
(BluetoothConnectionState connectionState) async { (BluetoothConnectionState connectionState) async {

View File

@ -80,12 +80,12 @@ class _CatEyeSetPageState extends State<CatEyeSetPage> {
isHaveRightWidget: true, isHaveRightWidget: true,
rightWidget: _otherToDoSwitch(2), rightWidget: _otherToDoSwitch(2),
)), )),
Obx(() => CommonItem( // Obx(() => CommonItem(
leftTitel: '异常警告'.tr, // leftTitel: '异常警告'.tr,
rightTitle: '', // rightTitle: '',
isHaveLine: true, // isHaveLine: true,
isHaveRightWidget: true, // isHaveRightWidget: true,
rightWidget: _otherToDoSwitch(3))), // rightWidget: _otherToDoSwitch(3))),
//ToDo //ToDo
CommonItem( CommonItem(
leftTitel: '呼叫目标'.tr, leftTitel: '呼叫目标'.tr,

View File

@ -12,6 +12,8 @@ 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_getDeviceModel.dart';
import 'package:star_lock/blue/io_protocol/io_otaUpgrade.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_processOtaUpgrade.dart';
import 'package:star_lock/blue/io_protocol/io_readVoicePackageFinalResult.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_voicePackageConfigure.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart'; import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart';
import 'package:star_lock/blue/io_reply.dart'; import 'package:star_lock/blue/io_reply.dart';
@ -52,9 +54,14 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
_handlerVoicePackageConfigureProcess(reply); _handlerVoicePackageConfigureProcess(reply);
} else if (reply is VoicePackageConfigureConfirmationReply) { } else if (reply is VoicePackageConfigureConfirmationReply) {
handleVoiceConfigureThrottled(reply); handleVoiceConfigureThrottled(reply);
} else if (reply is SetVoicePackageFinalResultReply) {
handleSetResult(reply);
} else if (reply is ReadLockCurrentVoicePacketReply) {
handleLockCurrentVoicePacketResult(reply);
} }
}); });
await initList(); await initList();
readLockLanguage();
} }
/// ///
@ -93,7 +100,7 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
final passthroughItem = PassthroughItem( final passthroughItem = PassthroughItem(
lang: element.lang, lang: element.lang,
timbres: element.timbres, timbres: element.timbres,
langText: '简体中文'.tr + '(中国台湾)'.tr, langText: '简体中文'.tr + '(中国台湾)'.tr + 'Simplified Chinese TW',
name: element.name, name: element.name,
); );
state.languages.add(passthroughItem); state.languages.add(passthroughItem);
@ -432,28 +439,50 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
_handlerVoicePackageConfigureConfirmation( _handlerVoicePackageConfigureConfirmation(
VoicePackageConfigureConfirmationReply reply, VoicePackageConfigureConfirmationReply reply,
) async { ) async {
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
final LoginEntity entity = await ApiRepository.to.settingCurrentVoiceTimbre(
data: {
'lang': state.tempLangStr.value,
'timbre': state.tempTimbreStr.value,
},
lockId: state.lockSetInfoData.value.lockId!,
);
if (entity.errorCode!.codeIsSuccessful) {
showSuccess('设置成功'.tr, something: () async {
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre?.lang =
state.tempLangStr.value;
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre
?.timbre = state.tempTimbreStr.value;
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();
}
});
await Future.delayed(Duration(seconds: 1));
});
}
}
void handleSetResult(SetVoicePackageFinalResultReply reply) async {
final int status = reply.data[2]; final int status = reply.data[2];
switch (status) { switch (status) {
case 0x00: case 0x00:
cancelBlueConnetctToastTimer(); cancelBlueConnetctToastTimer();
final LoginEntity entity =
await ApiRepository.to.settingCurrentVoiceTimbre(
data: {
'lang': state.tempLangStr.value,
'timbre': state.tempTimbreStr.value,
},
lockId: state.lockSetInfoData.value.lockId!,
);
if (entity.errorCode!.codeIsSuccessful) {
showSuccess('设置成功'.tr, something: () {
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre
?.lang = state.tempLangStr.value;
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre
?.timbre = state.tempTimbreStr.value;
eventBus.fire(
PassCurrentLockInformationEvent(state.lockSetInfoData.value));
});
}
dismissEasyLoading(); dismissEasyLoading();
break; break;
default: default:
@ -461,4 +490,74 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
break; break;
} }
} }
void handleLockCurrentVoicePacketResult(
ReadLockCurrentVoicePacketReply reply) {
final int status = reply.data[2];
switch (status) {
case 0x00:
//
cancelBlueConnetctToastTimer();
const int languageCodeStartIndex = 3;
const int languageCodeLength = 20;
const int languageCodeEndIndex =
languageCodeStartIndex + languageCodeLength; // 23
if (reply.data.length < languageCodeEndIndex) {
throw Exception(
'Reply data is too short to contain LanguageCode. Expected at least $languageCodeEndIndex bytes, got ${reply.data.length}');
}
List<int> languageCodeBytes =
reply.data.sublist(languageCodeStartIndex, languageCodeEndIndex);
String languageCode = String.fromCharCodes(languageCodeBytes);
languageCode = languageCode.trim(); //
languageCode =
languageCode.replaceAll('\u0000', ''); // (null bytes)
if (languageCode != null && languageCode != '') {
final indexWhere = state.languages
.indexWhere((element) => element.lang == languageCode);
if (indexWhere != -1) {
print('锁板上的语言是:$languageCode,下标是:$indexWhere');
state.selectPassthroughListIndex.value = indexWhere;
}
}
dismissEasyLoading();
break;
case 0x06:
//
final List<int> token = reply.data.sublist(2, 6);
if (state.data != null) {
sendFileToDevice(state.data!, token);
}
break;
default:
break;
}
}
void readLockLanguage() async {
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
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();
}
});
}
} }

View File

@ -63,6 +63,12 @@ class _SpeechLanguageSettingsPageState
final soundType = state.soundTypeList.value[index]; final soundType = state.soundTypeList.value[index];
return CommonItem( return CommonItem(
leftTitel: soundType, leftTitel: soundType,
leftTitleStyle: TextStyle(
fontSize: 20.sp,
fontWeight: state.selectSoundTypeIndex.value == index
? FontWeight.bold
: null,
),
rightTitle: '', rightTitle: '',
isHaveLine: !isLastItem, isHaveLine: !isLastItem,
isHaveDirection: false, isHaveDirection: false,
@ -94,34 +100,44 @@ class _SpeechLanguageSettingsPageState
height: 8.h, height: 8.h,
), ),
// //
Container( Obx(
color: Colors.transparent, () => Container(
child: Column( color: Colors.transparent,
children: List.generate( child: Column(
state.languages.length, children: List.generate(
(index) { state.languages.length,
final item = state.languages[index]; (index) {
return CommonItem( final item = state.languages[index];
leftTitel: item.langText, return CommonItem(
rightTitle: '', leftTitel: item.langText,
isHaveLine: true, leftTitleStyle: TextStyle(
isHaveDirection: false, fontSize: 20.sp,
isHaveRightWidget: true, fontWeight: state.selectPassthroughListIndex.value == index
rightWidget: ? FontWeight.bold
state.selectPassthroughListIndex.value == index : null,
? Image( ),
image: const AssetImage( rightTitle: '',
'images/icon_item_checked.png'), isHaveLine: true,
width: 30.w, isHaveDirection: false,
height: 30.w, isHaveRightWidget: true,
fit: BoxFit.contain, leftTitleMaxWidth: 0.9.sw,
) //
: Container(), rightWidget:
action: () { state.selectPassthroughListIndex.value == index
state.selectPassthroughListIndex.value = index; ? Image(
}, image: const AssetImage(
); 'images/icon_item_checked.png'),
}, width: 30.w,
height: 30.w,
fit: BoxFit.contain,
)
: Container(),
action: () {
state.selectPassthroughListIndex.value = index;
},
);
},
),
), ),
), ),
), ),
@ -131,77 +147,7 @@ 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;
return List.generate(
appLocalLanguages.length,
(index) => _buildItem(
appLocalLanguages[index],
index,
),
);
}
@override @override
void dispose() { void dispose() {
@ -211,24 +157,4 @@ class _SpeechLanguageSettingsPageState
} }
} }
_buildItem(PassthroughItem item, index) {
return CommonItem(
leftTitel: item.langText,
rightTitle: '',
isHaveLine: true,
isHaveDirection: false,
isHaveRightWidget: true,
rightWidget: state.selectPassthroughListIndex.value == index
? Image(
image: const AssetImage('images/icon_item_checked.png'),
width: 30.w,
height: 30.w,
fit: BoxFit.contain,
)
: Container(),
action: () {
state.selectPassthroughListIndex.value = index;
},
);
}
} }

View File

@ -16,6 +16,7 @@ class ThirdPartyPlatformState {
final RxList<String> platFormSet = List.of({ final RxList<String> platFormSet = List.of({
'锁通通'.tr, '锁通通'.tr,
'涂鸦智能'.tr, '涂鸦智能'.tr,
'Matter'.tr ,
}).obs; }).obs;
RxInt selectPlatFormIndex = 0.obs; RxInt selectPlatFormIndex = 0.obs;

View File

@ -145,9 +145,9 @@ class EditVideoLogLogic extends BaseGetXController {
} }
// URL生成唯一的文件名MD5哈希值 // 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 final hash = md5.convert(utf8.encode(url)).toString(); // 使 md5
return '$hash.$extension'; return '$recordType' + '_' + '$hash.$extension';
} }
Future<void> recordDownloadTime(String filePath) async { 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) { if (url == null || url.isEmpty) {
print('URL不能为空'); print('URL不能为空');
return null; return null;
@ -183,7 +183,8 @@ class EditVideoLogLogic extends BaseGetXController {
// URL生成唯一文件名 // URL生成唯一文件名
String extension = _getFileTypeFromUrl(url); // String extension = _getFileTypeFromUrl(url); //
String fileName = getFileNameFromUrl(url, extension); // URL生成唯一文件名 String fileName =
getFileNameFromUrl(url, extension, recordType); // URL生成唯一文件名
String savePath = '${appDocDir.path}/downloads/$fileName'; // String savePath = '${appDocDir.path}/downloads/$fileName'; //
// //

View File

@ -76,23 +76,31 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Obx(() => ListView.builder( child: Obx(
() => ListView.builder(
itemCount: state.videoLogList.length, itemCount: state.videoLogList.length,
itemBuilder: (BuildContext c, int index) { itemBuilder: (BuildContext c, int index) {
final CloudStorageData item = state.videoLogList[index]; final CloudStorageData item = state.videoLogList[index];
return Column( return ExpansionTile(
children: <Widget>[ shape: Border(),
Container( collapsedShape: Border(),
margin: EdgeInsets.only( expansionAnimationStyle: AnimationStyle(
left: 20.w, top: 15.w, bottom: 15.w), curve: Curves.easeInOut,
child: Row(children: <Widget>[ duration: Duration(milliseconds: 400),
Text(item.date ?? '', ),
style: TextStyle(fontSize: 20.sp)), initiallyExpanded: true,
])), title: Text(
mainListView(index, item) item.date ?? '',
], style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
children: mainListView(index, item),
); );
})), },
),
),
), ),
bottomBottomBtnWidget() 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 itemW = (1.sw - 15.w * 4) / 3;
double itemH = (1.sw - 15.w * 4) / 3 + 40.h; double itemH = (1.sw - 15.w * 4) / 3 + 40.h;
Widget mainListView(int index, CloudStorageData itemData) { //
return GridView.builder( List<Widget> mainListView(int index, CloudStorageData itemData) {
padding: EdgeInsets.only(left: 15.w, right: 15.w), return itemData.recordList!.map((e) => videoItem(e)).toList();
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);
},
);
} }
// Widget videoItem(RecordListData recordData, int index) { // Widget videoItem(RecordListData recordData, int index) {
@ -237,9 +251,9 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
if (state.selectVideoLogList.value.isNotEmpty) { if (state.selectVideoLogList.value.isNotEmpty) {
state.selectVideoLogList.value.forEach((element) { state.selectVideoLogList.value.forEach((element) {
if (element.videoUrl != null && element.videoUrl != '') { if (element.videoUrl != null && element.videoUrl != '') {
logic.downloadFile(element.videoUrl ?? ''); logic.downloadFile(element.videoUrl ?? '', element.recordType!);
} else if (element.imagesUrl != null && element.imagesUrl != '') { } else if (element.imagesUrl != null && element.imagesUrl != '') {
logic.downloadFile(element.imagesUrl ?? ''); logic.downloadFile(element.imagesUrl ?? '', element.recordType!);
} }
}); });
// double _progress = 0.0; // double _progress = 0.0;
@ -339,77 +353,154 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
Widget videoItem(RecordListData recordData) { Widget videoItem(RecordListData recordData) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
if (recordData.videoUrl != null && recordData.videoUrl!.isNotEmpty) { recordData.isSelect = !recordData.isSelect!;
Get.toNamed(Routers.videoLogDetailPage, arguments: <String, Object>{ if (recordData.isSelect! == true) {
'recordData': recordData, state.selectVideoLogList.add(recordData);
'videoDataList': state.videoLogList.value } else {
}); state.selectVideoLogList.remove(recordData);
} else if (recordData.imagesUrl != null &&
recordData.imagesUrl!.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FullScreenImagePage(
imageUrl: recordData.imagesUrl!,
),
),
);
} }
setState(() {});
}, },
child: Stack( child: Container(
children: [ padding: EdgeInsets.symmetric(horizontal: 20.w),
SizedBox( margin: EdgeInsets.only(
width: itemW, bottom: 20.h,
height: itemH, left: 18.w,
child: Column( right: 18.w,
children: <Widget>[ ),
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( Container(
width: itemW, padding: EdgeInsets.all(10.w),
height: itemW, decoration: BoxDecoration(
margin: const EdgeInsets.all(0), borderRadius: BorderRadius.circular(58.w),
color: Colors.white, color: AppColors.mainColor,
child: ClipRRect( ),
borderRadius: BorderRadius.circular(10.w), child: Icon(
child: _buildImageOrVideoItem(recordData), _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),
)
], ],
), ),
), Container(
Positioned( width: 118.w,
top: 0.w, height: 118.w,
right: 0.w, margin: const EdgeInsets.all(0),
child: GestureDetector( color: Colors.white,
onTap: () { child: ClipRRect(
recordData.isSelect = !recordData.isSelect!; borderRadius: BorderRadius.circular(10.w),
if (recordData.isSelect! == true) { child: _buildImageOrVideoItem(recordData),
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'),
), ),
), ),
) ],
], ),
), ),
); );
} }
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) { _buildImageOrVideoItem(RecordListData recordData) {
if (recordData.videoUrl != null && recordData.videoUrl!.isNotEmpty) { if (recordData.videoUrl != null && recordData.videoUrl!.isNotEmpty) {
return _buildVideoItem(recordData); return _buildVideoItem(recordData);

View File

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:star_lock/appRouters.dart'; import 'package:star_lock/appRouters.dart';
@ -63,8 +63,17 @@ class VideoLogLogic extends BaseGetXController {
final content = await File(logFilePath).readAsString(); final content = await File(logFilePath).readAsString();
final logData = Map<String, int>.from(json.decode(content)); final logData = Map<String, int>.from(json.decode(content));
//
logData.forEach((filePath, timestamp) { 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 downloadDateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final dateKey = final dateKey =
'${downloadDateTime.year}-${downloadDateTime.month.toString().padLeft(2, '0')}-${downloadDateTime.day.toString().padLeft(2, '0')}'; '${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')) { if (filePath.endsWith('.jpg')) {
groupedDownloads[dateKey]?.add( groupedDownloads[dateKey]?.add(
RecordListData(operateDate: timestamp, imagesUrl: filePath), RecordListData(
operateDate: timestamp,
imagesUrl: filePath,
recordType: number),
); );
} else if (filePath.endsWith('.mp4')) { } else if (filePath.endsWith('.mp4')) {
groupedDownloads[dateKey]?.add( groupedDownloads[dateKey]?.add(
RecordListData(operateDate: timestamp, videoUrl: filePath), RecordListData(
operateDate: timestamp, videoUrl: filePath, recordType: number),
); );
} }
}); });

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:star_lock/appRouters.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/flavors.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart'; import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_state.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> { class _VideoLogPageState extends State<VideoLogPage> {
final VideoLogLogic logic = Get.put(VideoLogLogic()); final VideoLogLogic logic = Get.put(VideoLogLogic());
final VideoLogState state = Get final VideoLogState state = Get.find<VideoLogLogic>().state;
.find<VideoLogLogic>()
.state;
@override @override
void initState() { void initState() {
@ -56,66 +55,71 @@ class _VideoLogPageState extends State<VideoLogPage> {
// title加编辑按钮 // title加编辑按钮
editVideoTip(), editVideoTip(),
Obx( Obx(
() => () => Visibility(
Visibility( visible: !state.isNavLocal.value,
visible: !state.isNavLocal.value, child: state.videoLogList.length > 0
child: state.videoLogList.length > 0 ? Expanded(
? Expanded( child: ListView.builder(
child: ListView.builder( itemCount: state.videoLogList.length,
itemCount: state.videoLogList.length, itemBuilder: (BuildContext c, int index) {
itemBuilder: (BuildContext c, int index) { final CloudStorageData item =
final CloudStorageData item = state.videoLogList[index];
state.videoLogList[index]; return ExpansionTile(
return Column( shape: Border(),
children: <Widget>[ collapsedShape: Border(),
Container( expansionAnimationStyle: AnimationStyle(
margin: EdgeInsets.only( curve: Curves.easeInOut,
left: 20.w, top: 15.w, bottom: 15.w), duration: Duration(milliseconds: 400),
child: Row(children: <Widget>[ ),
Text(item.date ?? '', initiallyExpanded: true,
style: TextStyle(fontSize: 20.sp)), title: Text(
])), item.date ?? '',
mainListView(index, item) style: TextStyle(
], fontSize: 24.sp,
); fontWeight: FontWeight.w600,
}, ),
), ),
) children: mainListView(index, item),
: _buildNotData(), );
), },
),
)
: _buildNotData(),
),
), ),
// //
Obx( Obx(
() => () => Visibility(
Visibility( visible: state.isNavLocal.value,
visible: state.isNavLocal.value, child: state.lockVideoList.length > 0
child: state.lockVideoList.length > 0 ? Expanded(
? Expanded( child: ListView.builder(
child: ListView.builder( itemCount: state.lockVideoList.length,
itemCount: state.lockVideoList.length, itemBuilder: (BuildContext c, int index) {
itemBuilder: (BuildContext c, int index) { final CloudStorageData item =
final CloudStorageData item = state.lockVideoList[index];
state.lockVideoList[index]; return ExpansionTile(
return Column( shape: Border(),
children: <Widget>[ collapsedShape: Border(),
Container( expansionAnimationStyle: AnimationStyle(
margin: EdgeInsets.only( curve: Curves.easeInOut,
left: 20.w, top: 15.w, bottom: 15.w), duration: Duration(milliseconds: 400),
child: Row( ),
children: <Widget>[ initiallyExpanded: true,
Text(item.date ?? '', title: Text(
style: TextStyle(fontSize: 20.sp)), item.date ?? '',
], style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
), ),
), ),
lockMainListView(index, item) children: mainListView(index, item),
], );
); },
}, ),
), )
) : _buildNotData(),
: _buildNotData(), ),
),
), ),
], ],
), ),
@ -154,24 +158,28 @@ class _VideoLogPageState extends State<VideoLogPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
TextButton( TextButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
state.isNavLocal.value = false; state.isNavLocal.value = false;
state.lockVideoList.clear(); state.lockVideoList.clear();
// logic.clearDownloads(); // logic.clearDownloads();
}); });
}, },
child: Obx(() => child: Obx(
Text('云存'.tr, () => Text(
style: state.isNavLocal.value == true '云存'.tr,
? TextStyle( style: state.isNavLocal.value == true
color: Colors.grey, ? TextStyle(
fontSize: 26.sp, color: Colors.grey,
fontWeight: FontWeight.w600) fontSize: 26.sp,
: TextStyle( fontWeight: FontWeight.w600)
color: Colors.white, : TextStyle(
fontSize: 28.sp, color: Colors.white,
fontWeight: FontWeight.w600)))), fontSize: 28.sp,
fontWeight: FontWeight.w600),
),
),
),
TextButton( TextButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
@ -180,19 +188,18 @@ class _VideoLogPageState extends State<VideoLogPage> {
}); });
}, },
child: Obx( child: Obx(
() => () => Text(
Text( '已下载'.tr,
'已下载'.tr, style: state.isNavLocal.value == true
style: state.isNavLocal.value == true ? TextStyle(
? TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 28.sp, fontSize: 28.sp,
fontWeight: FontWeight.w600) fontWeight: FontWeight.w600)
: TextStyle( : TextStyle(
color: Colors.grey, color: Colors.grey,
fontSize: 26.sp, fontSize: 26.sp,
fontWeight: FontWeight.w600), fontWeight: FontWeight.w600),
), ),
), ),
), ),
], ],
@ -212,93 +219,88 @@ class _VideoLogPageState extends State<VideoLogPage> {
// height: 150.h, // height: 150.h,
margin: EdgeInsets.all(15.w), margin: EdgeInsets.all(15.w),
padding: 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( decoration: BoxDecoration(
color: const Color(0xFFF6F7F8), color: const Color(0xFFF6F7F8),
borderRadius: BorderRadius.circular(20.h)), borderRadius: BorderRadius.circular(
20.h,
),
),
child: Obx( child: Obx(
() => () => Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Row(
Row( children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Expanded( Text('3天滚动储存'.tr, style: TextStyle(fontSize: 24.sp)),
child: Column( SizedBox(height: 10.h),
crossAxisAlignment: CrossAxisAlignment.start, Text("${F.navTitle}${"已为本设备免费提供3大滚动视频储存服务".tr}",
children: <Widget>[ style:
Text('3天滚动储存'.tr, TextStyle(fontSize: 22.sp, color: Colors.grey)),
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'))
], ],
), )),
SizedBox( SizedBox(width: 15.w),
height: 16.h, Text('去升级'.tr, style: TextStyle(fontSize: 22.sp)),
), Image(
Text( width: 40.w,
'云存服务状态:${_handlerValidityPeriodStatsText()}', height: 24.w,
style: TextStyle( image: const AssetImage('images/icon_right_black.png'))
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(
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, // height: 130.h,
margin: EdgeInsets.all(15.w), margin: EdgeInsets.all(15.w),
padding: 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( decoration: BoxDecoration(
color: const Color(0xFFF6F7F8), color: const Color(0xFFF6F7F8),
borderRadius: BorderRadius.circular(20.h)), borderRadius: BorderRadius.circular(20.h)),
@ -337,15 +339,15 @@ class _VideoLogPageState extends State<VideoLogPage> {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
// SizedBox(height: 20.h), // SizedBox(height: 20.h),
Text('下载列表'.tr, style: TextStyle(fontSize: 24.sp)), Text('下载列表'.tr, style: TextStyle(fontSize: 24.sp)),
SizedBox(height: 15.h), SizedBox(height: 15.h),
Text('暂无下载内容'.tr, Text('暂无下载内容'.tr,
style: TextStyle(fontSize: 22.sp, color: Colors.grey)), style: TextStyle(fontSize: 22.sp, color: Colors.grey)),
], ],
)), )),
SizedBox(width: 15.w), SizedBox(width: 15.w),
// Text("去升级", style: TextStyle(fontSize: 24.sp)), // Text("去升级", style: TextStyle(fontSize: 24.sp)),
Image( Image(
@ -412,48 +414,12 @@ class _VideoLogPageState extends State<VideoLogPage> {
double itemH = (1.sw - 15.w * 4) / 3 + 40.h; double itemH = (1.sw - 15.w * 4) / 3 + 40.h;
// //
Widget mainListView(int index, CloudStorageData itemData) { List<Widget> mainListView(int index, CloudStorageData itemData) {
return GridView.builder( return itemData.recordList!.map((e) => videoItem(e)).toList();
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);
},
);
} }
Widget lockMainListView(int index, CloudStorageData itemData) { List<Widget> lockMainListView(int index, CloudStorageData itemData) {
return GridView.builder( return itemData.recordList!.map((e) => videoItem(e)).toList();
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);
},
);
} }
Widget videoItem(RecordListData recordData) { Widget videoItem(RecordListData recordData) {
@ -469,22 +435,86 @@ class _VideoLogPageState extends State<VideoLogPage> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) => FullScreenImagePage(
FullScreenImagePage( imageUrl: recordData.imagesUrl!,
imageUrl: recordData.imagesUrl!, ),
),
), ),
); );
} }
}, },
child: SizedBox( child: Container(
width: itemW, padding: EdgeInsets.symmetric(horizontal: 20.w),
height: itemH, margin: EdgeInsets.only(
child: Column( 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>[ 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( Container(
width: itemW, width: 118.w,
height: itemW, height: 118.w,
margin: const EdgeInsets.all(0), margin: const EdgeInsets.all(0),
color: Colors.white, color: Colors.white,
child: ClipRRect( child: ClipRRect(
@ -492,12 +522,6 @@ class _VideoLogPageState extends State<VideoLogPage> {
child: _buildImageOrVideoItem(recordData), 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) { _buildVideoItem(RecordListData recordData) {
return VideoThumbnailImage(videoUrl: recordData.videoUrl!); return VideoThumbnailImage(videoUrl: recordData.videoUrl!);
} }

View File

@ -1,9 +1,13 @@
import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.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/videoLog/videoLog_entity.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/controlsOverlay_page.dart'; import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/controlsOverlay_page.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/videoLogDetail_state.dart'; import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/videoLogDetail_state.dart';
@ -30,11 +34,11 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
AppLog.log(
'state.recordData.value.videoUrl!' + state.recordData.value.videoUrl!);
state.videoController = VideoPlayerController.networkUrl( state.videoController =
Uri.parse(state.recordData.value.videoUrl!), createVideoController(state.recordData.value.videoUrl!);
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
state.videoController.addListener(() { state.videoController.addListener(() {
setState(() {}); setState(() {});
@ -47,10 +51,8 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
if (state.videoController != null) { if (state.videoController != null) {
await state.videoController.dispose(); // await state.videoController.dispose(); //
} }
state.videoController = VideoPlayerController.networkUrl( state.videoController =
Uri.parse(videoUrl), createVideoController(state.recordData.value.videoUrl!);
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
// //
await state.videoController.initialize(); await state.videoController.initialize();
@ -60,6 +62,22 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
setState(() {}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -104,6 +122,7 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
], ],
), ),
), ),
// _buildTitleRow(),
_buildOther(), _buildOther(),
], ],
) )
@ -135,14 +154,79 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
); );
} }
}, },
child: SizedBox( child: Container(
width: itemW, padding: EdgeInsets.symmetric(horizontal: 20.w),
height: itemH, margin: EdgeInsets.only(
child: Column( 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>[ 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( Container(
width: itemW, width: 118.w,
height: itemW, height: 118.w,
margin: const EdgeInsets.all(0), margin: const EdgeInsets.all(0),
color: Colors.white, color: Colors.white,
child: ClipRRect( child: ClipRRect(
@ -213,11 +297,13 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
margin: EdgeInsets.only(left: 20.w, top: 15.w, bottom: 15.w), margin: EdgeInsets.only(left: 20.w, top: 15.w, bottom: 15.w),
child: Row( child: Row(
children: <Widget>[ 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( List<Widget> mainListView(int index, CloudStorageData itemData) {
itemCount: itemData.recordList!.length, return itemData.recordList!.map((e) => videoItem(e)).toList();
shrinkWrap: true, }
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( String _buildTitleByType(RecordListData item) {
// final recordType = item.recordType;
crossAxisCount: 3, switch (recordType) {
), case 130:
itemBuilder: (BuildContext context, int index) { return '防拆报警'.tr;
return _buildItem(itemData.recordList![index]); 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) { _buildItem(itemData) {
return videoItem(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,
),
),
],
),
);
}
} }

View File

@ -1,53 +1,48 @@
import 'dart:io'; // dart:io 使 File import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:path_provider/path_provider.dart'; // path_provider import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:video_thumbnail/video_thumbnail.dart'; // video_thumbnail import 'package:path_provider/path_provider.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
class VideoThumbnailImage extends StatefulWidget { class VideoThumbnailImage extends StatefulWidget {
final String videoUrl; final String videoUrl;
VideoThumbnailImage({required this.videoUrl}); const VideoThumbnailImage({Key? key, required this.videoUrl})
: super(key: key);
@override @override
_VideoThumbnailState createState() => _VideoThumbnailState(); _VideoThumbnailState createState() => _VideoThumbnailState();
} }
class _VideoThumbnailState extends State<VideoThumbnailImage> { class _VideoThumbnailState extends State<VideoThumbnailImage> {
final Map<String, String> _thumbnailCache = {}; // // 使 static
late Future<String?> _thumbnailFuture; // Future static final Map<String, Future<String?>> _pendingThumbnails = {};
late Future<String?> _thumbnailFuture;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_thumbnailFuture = _generateThumbnail(); // initState Future // URL Future
_thumbnailFuture = _pendingThumbnails.putIfAbsent(widget.videoUrl, () {
return _generateThumbnail(widget.videoUrl);
});
} }
// // per URL
Future<String?> _generateThumbnail() async { Future<String?> _generateThumbnail(String url) async {
try { try {
//
if (_thumbnailCache.containsKey(widget.videoUrl)) {
return _thumbnailCache[widget.videoUrl];
}
//
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final thumbnailPath = await VideoThumbnail.thumbnailFile( final thumbnail = await VideoThumbnail.thumbnailFile(
video: widget.videoUrl, video: url,
// URL
thumbnailPath: tempDir.path, thumbnailPath: tempDir.path,
//
imageFormat: ImageFormat.JPEG, imageFormat: ImageFormat.JPEG,
//
maxHeight: 200, maxHeight: 200,
// quality: 100,
quality: 100, // (0-100)
); );
return thumbnail;
//
_thumbnailCache[widget.videoUrl] = thumbnailPath!;
return thumbnailPath;
} catch (e) { } catch (e) {
print('Failed to generate thumbnail: $e'); print('Failed to generate thumbnail: $e');
return null; return null;
@ -57,27 +52,25 @@ class _VideoThumbnailState extends State<VideoThumbnailImage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<String?>( return FutureBuilder<String?>(
future: _thumbnailFuture, // Future future: _thumbnailFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
//
return Center(child: CircularProgressIndicator()); return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError || !snapshot.hasData) { } else if (snapshot.hasError || !snapshot.hasData) {
// return Center(
return Image.asset( child: Image.asset(
'images/icon_unHaveData.png', // 'images/icon_unHaveData.png',
fit: BoxFit.cover, fit: BoxFit.cover,
),
); );
} else { } else {
//
final thumbnailPath = snapshot.data!;
return Stack( return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: <Widget>[ children: <Widget>[
RotatedBox( RotatedBox(
quarterTurns: -1, quarterTurns: -1,
child: Image.file( child: Image.file(
File(thumbnailPath), // File(snapshot.data!),
width: 200, width: 200,
height: 200, height: 200,
fit: BoxFit.cover, fit: BoxFit.cover,
@ -85,7 +78,7 @@ class _VideoThumbnailState extends State<VideoThumbnailImage> {
), ),
Icon( Icon(
Icons.play_arrow_rounded, Icons.play_arrow_rounded,
size: 80, size: 88.sp,
color: Colors.white.withOpacity(0.8), color: Colors.white.withOpacity(0.8),
), ),
], ],

View File

@ -297,6 +297,11 @@ class LockListInfoItemEntity {
LockListInfoItemEntity copy() { LockListInfoItemEntity copy() {
return LockListInfoItemEntity.fromJson(toJson()); 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 { class NetworkInfo {
@ -323,6 +328,11 @@ class NetworkInfo {
data['isOnline'] = isOnline; data['isOnline'] = isOnline;
return data; return data;
} }
@override
String toString() {
return 'NetworkInfo{peerId: $peerId, wifiName: $wifiName, isOnline: $isOnline}';
}
} }
class Bluetooth { class Bluetooth {
@ -356,6 +366,11 @@ class Bluetooth {
data['signKey'] = signKey; data['signKey'] = signKey;
return data; return data;
} }
@override
String toString() {
return 'Bluetooth{bluetoothDeviceId: $bluetoothDeviceId, bluetoothDeviceName: $bluetoothDeviceName, publicKey: $publicKey, privateKey: $privateKey, signKey: $signKey}';
}
} }
class LockFeature { class LockFeature {
@ -442,6 +457,11 @@ class LockFeature {
data['isMJpeg'] = isMJpeg; data['isMJpeg'] = isMJpeg;
return data; 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 { class LockSetting {
@ -486,6 +506,11 @@ class LockSetting {
} }
return data; return data;
} }
@override
String toString() {
return 'LockSetting{attendance: $attendance, appUnlockOnline: $appUnlockOnline, remoteUnlock: $remoteUnlock, catEyeConfig: $catEyeConfig}';
}
} }
// CatEyeConfig // CatEyeConfig

View File

@ -11,6 +11,8 @@ import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_colors.dart'; import 'package:star_lock/app_settings/app_colors.dart';
import 'package:star_lock/blue/blue_manage.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_getDeviceModel.dart';
import 'package:star_lock/blue/io_protocol/io_readVoicePackageFinalResult.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_voicePackageConfigure.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart'; import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart';
import 'package:star_lock/blue/io_reply.dart'; import 'package:star_lock/blue/io_reply.dart';
@ -50,9 +52,14 @@ class LockVoiceSettingLogic extends BaseGetXController {
_handlerVoicePackageConfigureProcess(reply); _handlerVoicePackageConfigureProcess(reply);
} else if (reply is VoicePackageConfigureConfirmationReply) { } else if (reply is VoicePackageConfigureConfirmationReply) {
handleVoiceConfigureThrottled(reply); handleVoiceConfigureThrottled(reply);
} else if (reply is ReadLockCurrentVoicePacketReply) {
handleLockCurrentVoicePacketResult(reply);
} else if (reply is SetVoicePackageFinalResultReply) {
handleSetResult(reply);
} }
}); });
initList(); initList();
readLockLanguage();
} }
void handleVoiceConfigureThrottled( void handleVoiceConfigureThrottled(
@ -73,6 +80,10 @@ class LockVoiceSettingLogic extends BaseGetXController {
Future<void> _executeLogic( Future<void> _executeLogic(
VoicePackageConfigureConfirmationReply reply) async { VoicePackageConfigureConfirmationReply reply) async {
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
final LoginEntity entity = await ApiRepository.to.settingCurrentVoiceTimbre( final LoginEntity entity = await ApiRepository.to.settingCurrentVoiceTimbre(
data: { data: {
'lang': state.tempLangStr.value, 'lang': state.tempLangStr.value,
@ -81,18 +92,47 @@ class LockVoiceSettingLogic extends BaseGetXController {
lockId: state.lockSetInfoData.value.lockId!, lockId: state.lockSetInfoData.value.lockId!,
); );
if (entity.errorCode!.codeIsSuccessful) { if (entity.errorCode!.codeIsSuccessful) {
showSuccess('设置成功'.tr, something: () { showSuccess('设置成功'.tr, something: () async {
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre?.lang = state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre?.lang =
state.tempLangStr.value; state.tempLangStr.value;
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre
?.timbre = state.tempTimbreStr.value; ?.timbre = state.tempTimbreStr.value;
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();
}
});
await Future.delayed(Duration(seconds: 1));
eventBus eventBus
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
Get.offAllNamed(Routers.starLockMain); Get.offAllNamed(Routers.starLockMain);
}); });
} }
dismissEasyLoading(); }
void handleSetResult(SetVoicePackageFinalResultReply reply) async {
final int status = reply.data[2];
switch (status) {
case 0x00:
cancelBlueConnetctToastTimer();
dismissEasyLoading();
break;
default:
showToast('设置'.tr + '失败'.tr);
break;
}
} }
void saveSpeechLanguageSettings() async { void saveSpeechLanguageSettings() async {
@ -202,9 +242,7 @@ class LockVoiceSettingLogic extends BaseGetXController {
case 0x00: case 0x00:
// //
cancelBlueConnetctToastTimer(); cancelBlueConnetctToastTimer();
_startSendLanguageFile(); _startSendLanguageFile();
break; break;
case 0x06: case 0x06:
// //
@ -214,7 +252,8 @@ class LockVoiceSettingLogic extends BaseGetXController {
} }
break; break;
default: default:
showToast('获取设备型号失败'.tr); dismissEasyLoading();
cancelBlueConnetctToastTimer();
break; break;
} }
} }
@ -250,11 +289,10 @@ class LockVoiceSettingLogic extends BaseGetXController {
if (lang == 'zh_TW') { if (lang == 'zh_TW') {
// //
List<String> parts = lang.split('_'); List<String> parts = lang.split('_');
final indexOf = locales.indexOf(Locale(parts[0], parts[1]));
final passthroughItem = PassthroughItem( final passthroughItem = PassthroughItem(
lang: element.lang, lang: element.lang,
timbres: element.timbres, timbres: element.timbres,
langText: '简体中文'.tr + '(中国台湾)'.tr, langText: '简体中文'.tr + '(中国台湾)'.tr + 'Simplified Chinese TW',
name: element.name, name: element.name,
); );
state.languages.add(passthroughItem); state.languages.add(passthroughItem);
@ -268,6 +306,7 @@ class LockVoiceSettingLogic extends BaseGetXController {
ExtensionLanguageType.fromLocale(locales[indexOf]).lanTitle, ExtensionLanguageType.fromLocale(locales[indexOf]).lanTitle,
name: element.name, name: element.name,
); );
state.languages.add(passthroughItem); state.languages.add(passthroughItem);
} }
}); });
@ -403,4 +442,77 @@ class LockVoiceSettingLogic extends BaseGetXController {
state.data = null; state.data = null;
super.onClose(); super.onClose();
} }
void readLockLanguage() async {
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
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[2];
switch (status) {
case 0x00:
//
cancelBlueConnetctToastTimer();
const int languageCodeStartIndex = 3;
const int languageCodeLength = 20;
const int languageCodeEndIndex =
languageCodeStartIndex + languageCodeLength; // 23
if (reply.data.length < languageCodeEndIndex) {
throw Exception(
'Reply data is too short to contain LanguageCode. Expected at least $languageCodeEndIndex bytes, got ${reply.data.length}');
}
List<int> languageCodeBytes =
reply.data.sublist(languageCodeStartIndex, languageCodeEndIndex);
String languageCode = String.fromCharCodes(languageCodeBytes);
languageCode = languageCode.trim(); //
languageCode =
languageCode.replaceAll('\u0000', ''); // (null bytes)
print('LanguageCode: $languageCode'); // : zh_CN, en_US
if (languageCode != null && languageCode != '') {
final indexWhere = state.languages
.indexWhere((element) => element.lang == languageCode);
if (indexWhere != -1) {
print('锁板上的语言是:$languageCode,下标是:$indexWhere');
state.selectPassthroughListIndex.value = indexWhere;
}
}
dismissEasyLoading();
break;
case 0x06:
//
final List<int> token = reply.data.sublist(2, 6);
if (state.data != null) {
sendFileToDevice(state.data!, token);
}
break;
default:
break;
}
}
} }

View File

@ -95,6 +95,12 @@ class _LockVoiceSettingState extends State<LockVoiceSetting> {
final soundType = state.soundTypeList.value[index]; final soundType = state.soundTypeList.value[index];
return CommonItem( return CommonItem(
leftTitel: soundType, leftTitel: soundType,
leftTitleStyle: TextStyle(
fontSize: 20.sp,
fontWeight: state.selectSoundTypeIndex.value == index
? FontWeight.bold
: null,
),
rightTitle: '', rightTitle: '',
isHaveLine: !isLastItem, isHaveLine: !isLastItem,
isHaveDirection: false, isHaveDirection: false,
@ -135,10 +141,18 @@ class _LockVoiceSettingState extends State<LockVoiceSetting> {
final item = state.languages[index]; final item = state.languages[index];
return CommonItem( return CommonItem(
leftTitel: item.langText, leftTitel: item.langText,
leftTitleStyle: TextStyle(
fontSize: 20.sp,
fontWeight:
state.selectPassthroughListIndex.value == index
? FontWeight.bold
: null,
),
rightTitle: '', rightTitle: '',
isHaveLine: true, isHaveLine: true,
isHaveDirection: false, isHaveDirection: false,
isHaveRightWidget: true, isHaveRightWidget: true,
leftTitleMaxWidth: 0.85.sw,
rightWidget: rightWidget:
state.selectPassthroughListIndex.value == index state.selectPassthroughListIndex.value == index
? Image( ? Image(

View File

@ -8,7 +8,7 @@ import 'messageDetail_state.dart';
class MessageDetailLogic extends BaseGetXController { class MessageDetailLogic extends BaseGetXController {
final MessageDetailState state = MessageDetailState(); final MessageDetailState state = MessageDetailState();
// //
Future<void> readMessageDataRequest() async { Future<void> readMessageDataRequest() async {
final MessageListEntity entity = await ApiRepository.to.readMessageLoadData(messageId:state.itemData.value.id!); final MessageListEntity entity = await ApiRepository.to.readMessageLoadData(messageId:state.itemData.value.id!);
if (entity.errorCode!.codeIsSuccessful) { if (entity.errorCode!.codeIsSuccessful) {

View File

@ -24,14 +24,22 @@ class MessageListEntity {
} }
return data; return data;
} }
@override
String toString() {
return 'MessageListEntity{errorCode: $errorCode, description: $description, errorMsg: $errorMsg, data: $data}';
}
} }
class Data { class Data {
List<MessageItemEntity>? list; List<MessageItemEntity>? list;
int? pageNo; int? pageNo;
int? pageSize; 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) { Data.fromJson(Map<String, dynamic> json) {
if (json['list'] != null) { if (json['list'] != null) {
@ -42,6 +50,9 @@ class Data {
} }
pageNo = json['pageNo']; pageNo = json['pageNo'];
pageSize = json['pageSize']; pageSize = json['pageSize'];
total = json['total'];
readCount = json['readCount'];
unreadCount = json['unreadCount'];
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -51,8 +62,16 @@ class Data {
} }
data['pageNo'] = pageNo; data['pageNo'] = pageNo;
data['pageSize'] = pageSize; data['pageSize'] = pageSize;
data['total'] = total;
data['readCount'] = readCount;
data['unreadCount'] = unreadCount;
return data; return data;
} }
@override
String toString() {
return 'Data{list: $list, pageNo: $pageNo, pageSize: $pageSize, total: $total, readCount: $readCount, unreadCount: $unreadCount}';
}
} }
class MessageItemEntity { class MessageItemEntity {
@ -78,4 +97,9 @@ class MessageItemEntity {
data['readAt'] = readAt; data['readAt'] = readAt;
return data; return data;
} }
@override
String toString() {
return 'MessageItemEntity{id: $id, data: $data, createdAt: $createdAt, readAt: $readAt}';
}
} }

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.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 'package:star_lock/tools/baseGetXController.dart';
import '../../../network/api_repository.dart'; import '../../../network/api_repository.dart';
import '../../../tools/eventBusEventManage.dart'; import '../../../tools/eventBusEventManage.dart';
@ -18,6 +19,10 @@ class MessageListLogic extends BaseGetXController {
final MessageListEntity entity = await ApiRepository.to final MessageListEntity entity = await ApiRepository.to
.messageListLoadData(pageNo: pageNo.toString(), pageSize: pageSize); .messageListLoadData(pageNo: pageNo.toString(), pageSize: pageSize);
if (entity.errorCode!.codeIsSuccessful) { if (entity.errorCode!.codeIsSuccessful) {
AppLog.log('消息列表数据请求成功:${entity.data!.total}');
//
await FlutterAppBadger.updateBadgeCount(entity.data!.unreadCount!);
if (pageNo == 1) { if (pageNo == 1) {
state.itemDataList.value = entity.data!.list!; state.itemDataList.value = entity.data!.list!;
pageNo++; pageNo++;

View File

@ -104,6 +104,7 @@ class _MineMultiLanguagePageState extends State<MineMultiLanguagePage> {
isHaveLine: true, isHaveLine: true,
isHaveDirection: false, isHaveDirection: false,
isHaveRightWidget: true, isHaveRightWidget: true,
leftTitleMaxWidth: 0.9.sw, //
rightWidget: state.currentLanguageType.value == lanType rightWidget: state.currentLanguageType.value == lanType
? Image( ? Image(
image: const AssetImage('images/icon_item_checked.png'), image: const AssetImage('images/icon_item_checked.png'),

View File

@ -2169,7 +2169,8 @@ class ApiProvider extends BaseProvider {
readMessageURL.toUrl, readMessageURL.toUrl,
jsonEncode({ jsonEncode({
'id': messageId, 'id': messageId,
})); }),
isUnShowLoading: true);
// //
Future<Response> deletMessageLoadData(String messageId) => post( Future<Response> deletMessageLoadData(String messageId) => post(

View File

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart'; import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
import 'package:get/get.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/main/lockMian/entity/lockListInfo_entity.dart';
import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
import 'package:star_lock/talk/starChart/constant/talk_status.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_accept.pb.dart';
import 'package:star_lock/talk/starChart/proto/talk_expect.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/commonDataManage.dart';
import 'package:star_lock/tools/eventBusEventManage.dart';
import 'package:star_lock/tools/storage.dart'; import 'package:star_lock/tools/storage.dart';
import '../../star_chart_manage.dart'; import '../../star_chart_manage.dart';
@ -43,6 +45,25 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle
// //
talkeRequestOverTimeTimerManager.renew(); talkeRequestOverTimeTimerManager.renew();
talkeRequestOverTimeTimerManager.cancel(); 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数据 // rbcuInfo数据
// startChartManage.startSendingRbcuInfoMessages( // startChartManage.startSendingRbcuInfoMessages(
// ToPeerId: startChartManage.lockPeerId); // ToPeerId: startChartManage.lockPeerId);

View File

@ -1,11 +1,45 @@
import 'dart:async';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get/get.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/network/start_chart_api.dart';
import 'package:star_lock/talk/starChart/constant/talk_status.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/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'; import 'package:star_lock/tools/storage.dart';
class AppLifecycleObserver extends WidgetsBindingObserver { 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 @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
@ -37,6 +71,7 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
Get.back(); Get.back();
} }
StartChartManage().destruction(); StartChartManage().destruction();
_readMessageRefreshUIEvent?.cancel();
} }
void onAppResumed() async { void onAppResumed() async {
@ -52,6 +87,9 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
StartChartApi.to.startChartHost = StartChartApi.to.startChartHost =
loginData!.starchart!.scdUrl ?? StartChartApi.to.startChartHost; loginData!.starchart!.scdUrl ?? StartChartApi.to.startChartHost;
} }
//
_readMessageRefreshUIAction();
print('App has resumed to the foreground.'); print('App has resumed to the foreground.');
} }

View File

@ -110,22 +110,8 @@ class ImageTransmissionLogic extends BaseGetXController {
// //
switch (contentType) { switch (contentType) {
case TalkData_ContentTypeE.G711: case TalkData_ContentTypeE.G711:
// // //
if (_isFirstAudioFrame) { if (!state.isOpenVoice.value && state.isRecordingAudio.value) {
_startAudioTime = currentTime;
_isFirstAudioFrame = false;
}
//
final expectedTime = _startAudioTime + talkData.durationMs;
final audioDelay = currentTime - expectedTime;
//
if (audioDelay > 500) {
state.audioBuffer.clear();
if (state.isOpenVoice.value) {
_playAudioFrames();
}
return; return;
} }
if (state.audioBuffer.length >= audioBufferSize) { if (state.audioBuffer.length >= audioBufferSize) {
@ -212,7 +198,8 @@ class ImageTransmissionLogic extends BaseGetXController {
/// ///
void _playAudioData(TalkData talkData) async { void _playAudioData(TalkData talkData) async {
if (state.isOpenVoice.value) { if (state.isOpenVoice.value &&
state.isRecordingAudio.value == false) {
final list = final list =
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150); G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
// // PCM PcmArrayInt16 // // PCM PcmArrayInt16
@ -561,6 +548,8 @@ class ImageTransmissionLogic extends BaseGetXController {
state.voiceProcessor = VoiceProcessor.instance; state.voiceProcessor = VoiceProcessor.instance;
} }
Timer? _startProcessingAudioTimer;
// //
Future<void> startProcessingAudio() async { Future<void> startProcessingAudio() async {
try { try {
@ -580,7 +569,6 @@ class ImageTransmissionLogic extends BaseGetXController {
} on PlatformException catch (ex) { } on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex'; // state.errorMessage.value = 'Failed to start recorder: $ex';
} }
state.isOpenVoice.value = false;
} }
/// ///
@ -600,47 +588,65 @@ class ImageTransmissionLogic extends BaseGetXController {
} on PlatformException catch (ex) { } on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to stop recorder: $ex'; // state.errorMessage.value = 'Failed to stop recorder: $ex';
} finally { } finally {
//
if (_startProcessingAudioTimer != null) {
// 53200
for (int i = 0; i < 5; i++) {
_bufferedAudioFrames.addAll(List.filled(chunkSize, 0));
}
Future.delayed(const Duration(milliseconds: 300), () {
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
});
} else {
_bufferedAudioFrames.clear();
}
final bool? isRecording = await state.voiceProcessor?.isRecording(); final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!; state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
} }
} }
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 40; // 40ms发送一次4chunk
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 { Future<void> _onFrame(List<int> frame) async {
// final applyGain = _applyGain(frame, 1.6);
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); //
return;
}
//
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// G711数据 // G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0A-law List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData); _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) { if (_startProcessingAudioTimer == null &&
try { _bufferedAudioFrames.length > chunkSize) {
await StartChartManage().sendTalkDataMessage( _startProcessingAudioTimer =
talkData: TalkData( Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
content: _bufferedAudioFrames,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
} finally {
_bufferedAudioFrames.clear(); //
}
} else {
_bufferedAudioFrames.addAll(encodedData);
} }
} }
@ -649,27 +655,11 @@ class ImageTransmissionLogic extends BaseGetXController {
AppLog.log(error.message!); AppLog.log(error.message!);
} }
//
List<int> _applyGain(List<int> pcmData, double gainFactor) { List<int> _applyGain(List<int> pcmData, double gainFactor) {
List<int> result = List<int>.filled(pcmData.length, 0); return pcmData.map((sample) {
//
for (int i = 0; i < pcmData.length; i++) { int amplified = (sample * gainFactor).round();
// PCM数据通常是有符号的16位整数 return amplified.clamp(-32768, 32767);
int sample = pcmData[i]; }).toList();
//
double amplified = sample * gainFactor;
//
if (amplified > 32767) {
amplified = 32767;
} else if (amplified < -32768) {
amplified = -32768;
}
result[i] = amplified.toInt();
}
return result;
} }
} }

View File

@ -9,7 +9,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart'; import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
import 'package:flutter_voice_processor/flutter_voice_processor.dart'; import 'package:flutter_voice_processor/flutter_voice_processor.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.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/login/login/entity/LoginEntity.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.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/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/main/lockMian/entity/lockListInfo_entity.dart';
import 'package:star_lock/network/api_repository.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/constant/talk_status.dart';
import 'package:star_lock/talk/starChart/entity/scp_message.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/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.pb.dart';
import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.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/proto/talk_expect.pb.dart';
import 'package:star_lock/talk/starChart/star_chart_manage.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/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/G711Tool.dart';
import 'package:star_lock/tools/bugly/bugly_tool.dart';
import 'package:star_lock/tools/callkit_handler.dart'; import 'package:star_lock/tools/callkit_handler.dart';
import 'package:star_lock/tools/commonDataManage.dart'; import 'package:star_lock/tools/commonDataManage.dart';
import 'package:star_lock/tools/storage.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 'package:video_decode_plugin/video_decode_plugin.dart';
import '../../../../tools/baseGetXController.dart'; import '../../../../tools/baseGetXController.dart';
@ -51,7 +42,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
int bufferSize = 25; // int bufferSize = 25; //
int audioBufferSize = 2; // 2 int audioBufferSize = 20; // 2
// frameSeq较小时阈值也小 // frameSeq较小时阈值也小
int _getFrameSeqRolloverThreshold(int lastSeq) { int _getFrameSeqRolloverThreshold(int lastSeq) {
@ -107,12 +98,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
state.isLoading.value = true; state.isLoading.value = true;
// //
final config = VideoDecoderConfig( final config = VideoDecoderConfig(
width: 864, width: StartChartManage().videoWidth,
// //
height: 480, height: StartChartManage().videoHeight,
codecType: 'h264', codecType: 'h264',
); );
// textureId // textureId
AppLog.log(
'StartChartManage().videoWidth:${StartChartManage().videoWidth}');
AppLog.log(
'StartChartManage().videoHeight:${StartChartManage().videoHeight}');
final textureId = await VideoDecodePlugin.initDecoder(config); final textureId = await VideoDecodePlugin.initDecoder(config);
if (textureId != null) { if (textureId != null) {
Future.microtask(() => state.textureId.value = textureId); Future.microtask(() => state.textureId.value = textureId);
@ -144,11 +139,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
FlutterPcmSound.setLogLevel(LogLevel.none); FlutterPcmSound.setLogLevel(LogLevel.none);
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1); FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
// feed // feed
if (Platform.isAndroid) { // if (Platform.isAndroid) {
FlutterPcmSound.setFeedThreshold(1024); // Android // FlutterPcmSound.setFeedThreshold(1024); // Android
} else { // } else {
FlutterPcmSound.setFeedThreshold(2000); // Android // FlutterPcmSound.setFeedThreshold(4096); // Android
} // }
} }
/// ///
@ -498,16 +493,21 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
/// ///
void _playAudioData(TalkData talkData) async { void _playAudioData(TalkData talkData) async {
if (state.isOpenVoice.value && state.isLoading.isFalse) { if (state.isOpenVoice.value &&
final list = state.isLoading.isFalse &&
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150); state.isRecordingAudio.value == false) {
// // PCM PcmArrayInt16 List<int> encodedData = G711Tool.decode(talkData.content, 0); // 0A-law
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list); // PCM PcmArrayInt16
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(encodedData);
FlutterPcmSound.feed(fromList); FlutterPcmSound.feed(fromList);
if (!state.isPlaying.value) { if (!state.isPlaying.value) {
AppLog.log('play');
FlutterPcmSound.play(); FlutterPcmSound.play();
state.isPlaying.value = true; state.isPlaying.value = true;
} }
} else if (state.isOpenVoice.isFalse) {
FlutterPcmSound.pause();
state.isPlaying.value = false;
} }
} }
@ -573,8 +573,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
//
_startListenTalkData();
// //
_startListenTalkStatus(); _startListenTalkStatus();
// //
@ -596,6 +594,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// H264帧缓冲区 // H264帧缓冲区
state.h264FrameBuffer.clear(); state.h264FrameBuffer.clear();
state.isProcessingFrame = false; state.isProcessingFrame = false;
//
_startListenTalkData();
} }
@override @override
@ -639,7 +640,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// I帧集合 // I帧集合
_decodedIFrames.clear(); _decodedIFrames.clear();
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
super.onClose(); super.onClose();
} }
@ -652,33 +655,12 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
/// ///
void updateTalkExpect() { void updateTalkExpect() {
// VideoTypeE的映射
final Map<String, VideoTypeE> qualityToVideoType = {
'标清': VideoTypeE.H264,
'高清': VideoTypeE.H264_720P,
//
};
TalkExpectReq talkExpectReq = TalkExpectReq();
state.isOpenVoice.value = !state.isOpenVoice.value; state.isOpenVoice.value = !state.isOpenVoice.value;
// videoType if (state.isOpenVoice.isTrue) {
VideoTypeE currentVideoType = FlutterPcmSound.play();
qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264;
if (!state.isOpenVoice.value) {
talkExpectReq = TalkExpectReq(
videoType: [currentVideoType],
audioType: [],
);
showToast('已静音'.tr);
} else { } else {
talkExpectReq = TalkExpectReq( FlutterPcmSound.pause();
videoType: [currentVideoType],
audioType: [AudioTypeE.G711],
);
} }
///
StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
talkExpect: talkExpectReq);
} }
/// ///
@ -762,6 +744,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
state.voiceProcessor = VoiceProcessor.instance; state.voiceProcessor = VoiceProcessor.instance;
} }
Timer? _startProcessingAudioTimer;
// //
Future<void> startProcessingAudio() async { Future<void> startProcessingAudio() async {
try { try {
@ -781,7 +765,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
} on PlatformException catch (ex) { } on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex'; // state.errorMessage.value = 'Failed to start recorder: $ex';
} }
state.isOpenVoice.value = false;
} }
/// ///
@ -801,47 +784,74 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
} on PlatformException catch (ex) { } on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to stop recorder: $ex'; // state.errorMessage.value = 'Failed to stop recorder: $ex';
} finally { } finally {
//
if (_startProcessingAudioTimer != null) {
// 53200
for (int i = 0; i < 5; i++) {
_bufferedAudioFrames.addAll(List.filled(chunkSize, 0));
}
Future.delayed(const Duration(milliseconds: 300), () {
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
});
} else {
_bufferedAudioFrames.clear();
}
final bool? isRecording = await state.voiceProcessor?.isRecording(); final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!; state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
} }
} }
//
List<int> _applyGain(List<int> pcmData, double gainFactor) {
return pcmData.map((sample) {
//
int amplified = (sample * gainFactor).round();
return amplified.clamp(-32768, 32767);
}).toList();
}
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 35; // 40ms发送一次4chunk
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 { Future<void> _onFrame(List<int> frame) async {
// final applyGain = _applyGain(frame, 1.6);
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); //
return;
}
//
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// G711数据 // G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0A-law List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData); _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) { if (_startProcessingAudioTimer == null &&
try { _bufferedAudioFrames.length > chunkSize) {
await StartChartManage().sendTalkDataMessage( _startProcessingAudioTimer =
talkData: TalkData( Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
content: _bufferedAudioFrames,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
} finally {
_bufferedAudioFrames.clear(); //
}
} else {
_bufferedAudioFrames.addAll(encodedData);
} }
} }
@ -850,476 +860,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
AppLog.log(error.message!); AppLog.log(error.message!);
} }
//
List<int> _applyGain(List<int> pcmData, double gainFactor) {
List<int> result = List<int>.filled(pcmData.length, 0);
for (int i = 0; i < pcmData.length; i++) {
// PCM数据通常是有符号的16位整数
int sample = pcmData[i];
//
double amplified = sample * gainFactor;
//
if (amplified > 32767) {
amplified = 32767;
} else if (amplified < -32768) {
amplified = -32768;
}
result[i] = amplified.toInt();
}
return result;
}
/// 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;
}
// 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');
}
}
// 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;
}
}
/// 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();
// ROMDownload
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帧包IDRtype 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 { void onQualityChanged(String quality) async {
state.currentQuality.value = quality; state.currentQuality.value = quality;
@ -1432,6 +972,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// //
switch (contentType) { switch (contentType) {
case TalkData_ContentTypeE.G711: case TalkData_ContentTypeE.G711:
//
if (!state.isOpenVoice.value || state.isRecordingAudio.value) {
return;
}
if (state.audioBuffer.length >= audioBufferSize) { if (state.audioBuffer.length >= audioBufferSize) {
state.audioBuffer.removeAt(0); // state.audioBuffer.removeAt(0); //
} }

View File

@ -109,22 +109,10 @@ class TalkViewLogic extends BaseGetXController {
// //
switch (contentType) { switch (contentType) {
case TalkData_ContentTypeE.G711: case TalkData_ContentTypeE.G711:
// // //
if (_isFirstAudioFrame) { if (!state.isOpenVoice.value || state.isRecordingAudio.value) {
_startAudioTime = currentTime; print(
_isFirstAudioFrame = false; '录音时丢弃数据:${state.isOpenVoice.value}-${state.isRecordingAudio.value}');
}
//
final expectedTime = _startAudioTime + talkData.durationMs;
final audioDelay = currentTime - expectedTime;
//
if (audioDelay > 500) {
state.audioBuffer.clear();
if (state.isOpenVoice.value) {
_playAudioFrames();
}
return; return;
} }
if (state.audioBuffer.length >= audioBufferSize) { if (state.audioBuffer.length >= audioBufferSize) {
@ -388,9 +376,11 @@ class TalkViewLogic extends BaseGetXController {
if (state.videoBuffer.isNotEmpty) { if (state.videoBuffer.isNotEmpty) {
final TalkData oldestFrame = state.videoBuffer.removeAt(0); final TalkData oldestFrame = state.videoBuffer.removeAt(0);
if (oldestFrame.content.isNotEmpty) { if (oldestFrame.content.isNotEmpty) {
state.listData.value = Uint8List.fromList(oldestFrame.content); // state.listData.value =
Uint8List.fromList(oldestFrame.content); //
final int decodeStart = DateTime.now().millisecondsSinceEpoch; final int decodeStart = DateTime.now().millisecondsSinceEpoch;
decodeImageFromList(Uint8List.fromList(oldestFrame.content)).then((ui.Image img) { decodeImageFromList(Uint8List.fromList(oldestFrame.content))
.then((ui.Image img) {
final int decodeEnd = DateTime.now().millisecondsSinceEpoch; final int decodeEnd = DateTime.now().millisecondsSinceEpoch;
state.currentImage.value = img; state.currentImage.value = img;
_renderedFrameCount++; _renderedFrameCount++;
@ -524,7 +514,7 @@ class TalkViewLogic extends BaseGetXController {
final lockPeerId = StartChartManage().lockPeerId; final lockPeerId = StartChartManage().lockPeerId;
final LockListInfoGroupEntity? lockListInfoGroupEntity = final LockListInfoGroupEntity? lockListInfoGroupEntity =
await Storage.getLockMainListData(); await Storage.getLockMainListData();
if (lockListInfoGroupEntity != null) { if (lockListInfoGroupEntity != null) {
lockListInfoGroupEntity!.groupList?.forEach((element) { lockListInfoGroupEntity!.groupList?.forEach((element) {
final lockList = element.lockList; final lockList = element.lockList;
@ -558,6 +548,8 @@ class TalkViewLogic extends BaseGetXController {
state.voiceProcessor = VoiceProcessor.instance; state.voiceProcessor = VoiceProcessor.instance;
} }
Timer? _startProcessingAudioTimer;
// //
Future<void> startProcessingAudio() async { Future<void> startProcessingAudio() async {
try { try {
@ -577,7 +569,6 @@ class TalkViewLogic extends BaseGetXController {
} on PlatformException catch (ex) { } on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex'; // state.errorMessage.value = 'Failed to start recorder: $ex';
} }
state.isOpenVoice.value = false;
} }
/// ///
@ -597,47 +588,65 @@ class TalkViewLogic extends BaseGetXController {
} on PlatformException catch (ex) { } on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to stop recorder: $ex'; // state.errorMessage.value = 'Failed to stop recorder: $ex';
} finally { } finally {
//
if (_startProcessingAudioTimer != null) {
// 53200
for (int i = 0; i < 5; i++) {
_bufferedAudioFrames.addAll(List.filled(chunkSize, 0));
}
Future.delayed(const Duration(milliseconds: 300), () {
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
});
} else {
_bufferedAudioFrames.clear();
}
final bool? isRecording = await state.voiceProcessor?.isRecording(); final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!; state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
} }
} }
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 40; // 40ms发送一次4chunk
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 { Future<void> _onFrame(List<int> frame) async {
// final applyGain = _applyGain(frame, 1.6);
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); //
return;
}
//
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// G711数据 // G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0A-law List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData); _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) { if (_startProcessingAudioTimer == null &&
try { _bufferedAudioFrames.length > chunkSize) {
await StartChartManage().sendTalkDataMessage( _startProcessingAudioTimer =
talkData: TalkData( Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
content: _bufferedAudioFrames,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
} finally {
_bufferedAudioFrames.clear(); //
}
} else {
_bufferedAudioFrames.addAll(encodedData);
} }
} }
@ -648,25 +657,10 @@ class TalkViewLogic extends BaseGetXController {
// //
List<int> _applyGain(List<int> pcmData, double gainFactor) { List<int> _applyGain(List<int> pcmData, double gainFactor) {
List<int> result = List<int>.filled(pcmData.length, 0); return pcmData.map((sample) {
//
for (int i = 0; i < pcmData.length; i++) { int amplified = (sample * gainFactor).round();
// PCM数据通常是有符号的16位整数 return amplified.clamp(-32768, 32767);
int sample = pcmData[i]; }).toList();
//
double amplified = sample * gainFactor;
//
if (amplified > 32767) {
amplified = 32767;
} else if (amplified < -32768) {
amplified = -32768;
}
result[i] = amplified.toInt();
}
return result;
} }
} }

View File

@ -20,7 +20,8 @@ class CommonItem extends StatelessWidget {
this.rightWidget, this.rightWidget,
this.isTipsImg, this.isTipsImg,
this.action, this.action,
this.leftTitleMaxWidth, // this.leftTitleMaxWidth, //
this.leftTitleStyle, //
this.tipsImgAction}) this.tipsImgAction})
: super(key: key); : super(key: key);
String? leftTitel; String? leftTitel;
@ -35,6 +36,7 @@ class CommonItem extends StatelessWidget {
bool? setHeight; bool? setHeight;
bool? isTipsImg; bool? isTipsImg;
bool? isPadding; bool? isPadding;
TextStyle? leftTitleStyle; //
final double? leftTitleMaxWidth; // final double? leftTitleMaxWidth; //
@override @override
@ -65,7 +67,7 @@ class CommonItem extends StatelessWidget {
), ),
child: Text( child: Text(
leftTitel!, leftTitel!,
style: TextStyle(fontSize: 22.sp), style: leftTitleStyle ?? TextStyle(fontSize: 22.sp),
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis, //
maxLines: 3, // 2 maxLines: 3, // 2
), ),

View File

@ -131,6 +131,13 @@ class ReadMessageRefreshUI {
ReadMessageRefreshUI(); ReadMessageRefreshUI();
} }
///
class ReadTalkMessageRefreshUI {
ReadTalkMessageRefreshUI(this.lockName);
String lockName;
}
/// ///
class ElectronicKeyListRefreshUI { class ElectronicKeyListRefreshUI {
ElectronicKeyListRefreshUI(); ElectronicKeyListRefreshUI();

View File

@ -206,130 +206,130 @@ extension ExtensionLanguageType on LanguageType {
var str = ''; var str = '';
switch (this) { switch (this) {
case LanguageType.english: case LanguageType.english:
str = '英文'.tr; str = '英文'.tr + 'English';
break; break;
case LanguageType.chinese: case LanguageType.chinese:
str = '简体中文'.tr; str = '简体中文'.tr + 'Simplified Chinese';
break; break;
case LanguageType.traditionalChineseTW: case LanguageType.traditionalChineseTW:
str = '繁体中文(中国台湾)'.tr; str = '繁体中文(中国台湾)'.tr + 'Traditional Chinese TW';
break; break;
case LanguageType.traditionalChineseHK: case LanguageType.traditionalChineseHK:
str = '繁体中文(中国香港)'.tr; str = '繁体中文(中国香港)'.tr + 'Traditional Chinese HK';
break; break;
case LanguageType.french: case LanguageType.french:
str = '法语'.tr; str = '法语'.tr + 'French';
break; break;
case LanguageType.russian: case LanguageType.russian:
str = '俄语'.tr; str = '俄语'.tr + 'Russian';
break; break;
case LanguageType.german: case LanguageType.german:
str = '德语'.tr; str = '德语'.tr + 'German';
break; break;
case LanguageType.japanese: case LanguageType.japanese:
str = '日语'.tr; str = '日语'.tr + 'Japanese';
break; break;
case LanguageType.korean: case LanguageType.korean:
str = '韩语'.tr; str = '韩语'.tr + 'Korean';
break; break;
case LanguageType.italian: case LanguageType.italian:
str = '意大利语'.tr; str = '意大利语'.tr + 'Italian';
break; break;
case LanguageType.portuguese: case LanguageType.portuguese:
str = '葡萄牙语'.tr; str = '葡萄牙语'.tr + 'Portuguese';
break; break;
case LanguageType.spanish: case LanguageType.spanish:
str = '西班牙语'.tr; str = '西班牙语'.tr + 'Spanish';
break; break;
case LanguageType.arabic: case LanguageType.arabic:
str = '阿拉伯语'.tr; str = '阿拉伯语'.tr + 'Arabic';
break; break;
case LanguageType.vietnamese: case LanguageType.vietnamese:
str = '越南语'.tr; str = '越南语'.tr + 'Vietnamese';
break; break;
case LanguageType.malay: case LanguageType.malay:
str = '马来语'.tr; str = '马来语'.tr + 'Malay';
break; break;
case LanguageType.dutch: case LanguageType.dutch:
str = '荷兰语'.tr; str = '荷兰语'.tr + 'Dutch';
break; break;
case LanguageType.romanian: case LanguageType.romanian:
str = '罗马尼亚语'.tr; str = '罗马尼亚语'.tr + 'Romanian';
break; break;
case LanguageType.lithuanian: case LanguageType.lithuanian:
str = '立陶宛语'.tr; str = '立陶宛语'.tr + 'Lithuanian';
break; break;
case LanguageType.swedish: case LanguageType.swedish:
str = '瑞典语'.tr; str = '瑞典语'.tr + 'Swedish';
break; break;
case LanguageType.estonian: case LanguageType.estonian:
str = '爱沙尼亚语'.tr; str = '爱沙尼亚语'.tr + 'Estonian';
break; break;
case LanguageType.polish: case LanguageType.polish:
str = '波兰语'.tr; str = '波兰语'.tr + 'Polish';
break; break;
case LanguageType.slovak: case LanguageType.slovak:
str = '斯洛伐克语'.tr; str = '斯洛伐克语'.tr + 'Slovak';
break; break;
case LanguageType.czech: case LanguageType.czech:
str = '捷克语'.tr; str = '捷克语'.tr + 'Czech';
break; break;
case LanguageType.greek: case LanguageType.greek:
str = '希腊语'.tr; str = '希腊语'.tr + 'Greek';
break; break;
case LanguageType.hebrew: case LanguageType.hebrew:
str = '希伯来语'.tr; str = '希伯来语'.tr + 'Hebrew';
break; break;
case LanguageType.serbian: case LanguageType.serbian:
str = '塞尔维亚语'.tr; str = '塞尔维亚语'.tr + 'Serbian';
break; break;
case LanguageType.turkish: case LanguageType.turkish:
str = '土耳其语'.tr; str = '土耳其语'.tr + 'Turkish';
break; break;
case LanguageType.hungarian: case LanguageType.hungarian:
str = '匈牙利语'.tr; str = '匈牙利语'.tr + 'Hungarian';
break; break;
case LanguageType.bulgarian: case LanguageType.bulgarian:
str = '保加利亚语'.tr; str = '保加利亚语'.tr + 'Bulgarian';
break; break;
case LanguageType.kazakh: case LanguageType.kazakh:
str = '哈萨克斯坦语'.tr; str = '哈萨克斯坦语'.tr + 'Kazakh';
break; break;
case LanguageType.bengali: case LanguageType.bengali:
str = '孟加拉语'.tr; str = '孟加拉语'.tr + 'Bengali';
break; break;
case LanguageType.croatian: case LanguageType.croatian:
str = '克罗地亚语'.tr; str = '克罗地亚语'.tr + 'Croatian';
break; break;
case LanguageType.thai: case LanguageType.thai:
str = '泰语'.tr; str = '泰语'.tr + 'Thai';
break; break;
case LanguageType.indonesian: case LanguageType.indonesian:
str = '印度尼西亚语'.tr; str = '印度尼西亚语'.tr + 'Indonesian';
break; break;
case LanguageType.finnish: case LanguageType.finnish:
str = '芬兰语'.tr; str = '芬兰语'.tr + 'Finnish';
break; break;
case LanguageType.danish: case LanguageType.danish:
str = '丹麦语'.tr; str = '丹麦语'.tr + 'Danish';
break; break;
case LanguageType.ukrainian: case LanguageType.ukrainian:
str = '乌克兰语'.tr; str = '乌克兰语'.tr + 'Ukrainian';
break; break;
case LanguageType.hindi: case LanguageType.hindi:
str = '印地语'.tr; str = '印地语'.tr + 'Hindi';
break; break;
case LanguageType.urdu: case LanguageType.urdu:
str = '乌尔都语'.tr; str = '乌尔都语'.tr + 'Urdu';
break; break;
case LanguageType.armenian: case LanguageType.armenian:
str = '亚美尼亚语'.tr; str = '亚美尼亚语'.tr + 'Armenian';
break; break;
case LanguageType.georgian: case LanguageType.georgian:
str = '格鲁吉亚语'.tr; str = '格鲁吉亚语'.tr + 'Georgian';
break; break;
case LanguageType.brazilianPortuguese: case LanguageType.brazilianPortuguese:
str = '巴西葡萄牙语'.tr; str = '巴西葡萄牙语'.tr + 'Brazilian Portuguese';
break; break;
} }
return str; return str;