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