Merge branch 'develop_sky_liyi' into 'develop_sky'

Develop sky liyi

See merge request StarlockTeam/app-starlock!253
This commit is contained in:
李仪 2025-08-20 06:29:43 +00:00
commit 832c72df4a
36 changed files with 1915 additions and 1468 deletions

View File

@ -0,0 +1,54 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../io_reply.dart';
import '../io_sender.dart';
import '../io_tool/io_tool.dart';
import '../io_type.dart';
import '../sm4Encipher/sm4.dart';
//oat升级
class ReadLockCurrentVoicePacket extends SenderProtocol {
ReadLockCurrentVoicePacket({
this.lockID,
}) : super(CommandType.readLockCurrentVoicePacket);
String? lockID;
@override
String toString() {
return 'ReadLockCurrentVoicePacket{lockID: $lockID}';
}
@override
List<int> messageDetail() {
List<int> data = <int>[];
//
final int type = commandType!.typeValue;
final double typeDouble = type / 256;
final int type1 = typeDouble.toInt();
final int type2 = type % 256;
data.add(type1);
data.add(type2);
// id 40
final int lockIDLength = utf8.encode(lockID!).length;
data.addAll(utf8.encode(lockID!));
data = getFixedLengthList(data, 40 - lockIDLength);
printLog(data);
return data;
}
}
class ReadLockCurrentVoicePacketReply extends Reply {
ReadLockCurrentVoicePacketReply.parseData(
CommandType commandType, List<int> dataDetail)
: super.parseData(commandType, dataDetail) {
data = dataDetail;
status = data[6];
errorWithStstus(status);
}
}

View File

@ -0,0 +1,61 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../io_reply.dart';
import '../io_sender.dart';
import '../io_tool/io_tool.dart';
import '../io_type.dart';
import '../sm4Encipher/sm4.dart';
//oat升级
class SetVoicePackageFinalResult extends SenderProtocol {
SetVoicePackageFinalResult({
this.lockID,
this.languageCode,
}) : super(CommandType.setLockCurrentVoicePacket);
String? lockID;
String? languageCode;
@override
String toString() {
return 'SetVoicePackageFinalResult{lockID: $lockID, languageCode: $languageCode}';
}
@override
List<int> messageDetail() {
List<int> data = <int>[];
//
final int type = commandType!.typeValue;
final double typeDouble = type / 256;
final int type1 = typeDouble.toInt();
final int type2 = type % 256;
data.add(type1);
data.add(type2);
// id 40
final int lockIDLength = utf8.encode(lockID!).length;
data.addAll(utf8.encode(lockID!));
data = getFixedLengthList(data, 40 - lockIDLength);
//languageCode 20
final int languageCodeLength = utf8.encode(languageCode!).length;
data.addAll(utf8.encode(languageCode!));
data = getFixedLengthList(data, 20 - languageCodeLength);
printLog(data);
return data;
}
}
class SetVoicePackageFinalResultReply extends Reply {
SetVoicePackageFinalResultReply.parseData(
CommandType commandType, List<int> dataDetail)
: super.parseData(commandType, dataDetail) {
data = dataDetail;
status = data[6];
errorWithStstus(status);
}
}

View File

@ -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 = '读星锁状态信息';

View File

@ -80,7 +80,11 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
),
],
),
body: ListView(
body: GestureDetector(
onTap: (){
FocusScope.of(context).unfocus();
},
child: ListView(
padding: EdgeInsets.only(top: 120.h, left: 40.w, right: 40.w),
children: <Widget>[
Container(
@ -305,6 +309,7 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
],
),
],
),
));
}

View File

@ -62,10 +62,8 @@ FutureOr<void> main() async {
}
});
// //ToDo:
// runApp(MultiProvider(providers: [
// ChangeNotifierProvider(create: (_) => DebugInfoModel()),
// ], child: MyApp(isLogin: isLogin)));
// ios则初始化获取到voip token
// token callkit
if (Platform.isIOS) {
CallKitHandler.setupListener();
String? token = await CallKitHandler.getVoipToken();
@ -111,20 +109,4 @@ Future<void> privacySDKInitialization() async {
await jpushProvider.initJPushService();
NotificationService().init(); //
// /// ip如果属于国内才进行初始化
// final CheckIPEntity entity = await ApiRepository.to.checkIpAction(ip: '');
// if (entity.errorCode!.codeIsSuccessful) {
// String currentLanguage =
// CurrentLocaleTool.getCurrentLocaleString(); //
// // ip是国内的且选的是中文才初始化一键登录
// if (entity.data!.abbreviation?.toLowerCase() == 'cn' &&
// currentLanguage == 'zh_CN') {
// //
// final StarLockLoginLogic loginLogic = Get.put(StarLockLoginLogic());
// await JverifyOneClickLoginManage();
// loginLogic.state.isCheckVerifyEnable.value =
// await JverifyOneClickLoginManage().checkVerifyEnable();
// eventBus.fire(AgreePrivacyAgreement());
// }
// }
}

View File

@ -0,0 +1,11 @@
extension DateTimeExtensions on DateTime {
/// DateTime 00:00:00.000
DateTime get withoutTime {
return DateTime(year, month, day);
}
///
bool isSameDate(DateTime other) {
return year == other.year && month == other.month && day == other.day;
}
}

View File

@ -4,6 +4,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart';
import 'package:star_lock/apm/apm_helper.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/date_time_extensions.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_state.dart';
@ -235,13 +236,15 @@ class DoorLockLogLogic extends BaseGetXController {
lockId: state.keyInfos.value.lockId!,
lockEventType: state.dropdownValue.value,
pageNo: pageNo,
pageSize: int.parse(pageSize),
pageSize: 1000,
startDate: state.startDate.value,
endDate: state.endDate.value);
if (entity.errorCode!.codeIsSuccessful) {
//
state.lockLogItemList.addAll(entity.data!.itemList!);
state.lockLogItemList.refresh();
state.weekEventList.addAll(entity.data!.itemList!);
state.weekEventList.refresh();
//
pageNo++;
}
@ -358,6 +361,7 @@ class DoorLockLogLogic extends BaseGetXController {
@override
Future<void> onInit() async {
_setWeekRange();
super.onInit();
//
@ -370,6 +374,35 @@ class DoorLockLogLogic extends BaseGetXController {
}
}
void _setWeekRange() {
final now = DateTime.now();
// 1=7=
int weekday = now.weekday; // 1-7
//
// : 0, : 1, ..., : 6
int daysToSubtract = weekday - 1; // 1
// 00:00:00.000
DateTime startOfWeek = DateTime(now.year, now.month, now.day)
.subtract(Duration(days: daysToSubtract));
// 23:59:59.999
DateTime endOfWeek = startOfWeek
.add(Duration(days: 6)) // 6
.add(Duration(hours: 23, minutes: 59, seconds: 59, milliseconds: 999));
//
state.startDate.value = startOfWeek.millisecondsSinceEpoch;
state.endDate.value = endOfWeek.millisecondsSinceEpoch;
}
//
void refreshWeek() {
_setWeekRange();
}
@override
Future<void> onClose() async {
super.onClose();

View File

@ -5,10 +5,13 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/date_time_extensions.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_logic.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_state.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/exportRecordDialog/exportRecordDialog_page.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/week_calendar_view.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
import 'package:star_lock/main/lockDetail/videoLog/widget/full_screenImage_page.dart';
import 'package:star_lock/main/lockDetail/videoLog/widget/video_thumbnail_image.dart';
@ -34,8 +37,39 @@ class DoorLockLogPage extends StatefulWidget {
}
class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
final ScrollController _scrollController = ScrollController();
final DoorLockLogLogic logic = Get.put(DoorLockLogLogic());
final DoorLockLogState state = Get.find<DoorLockLogLogic>().state;
bool _isAtBottom = false;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
final max = _scrollController.position.maxScrollExtent;
final current = _scrollController.position.pixels;
AppLog.log('current:${current}');
// 5
if (current >= max - 5) {
if (!_isAtBottom) {
setState(() {
_isAtBottom = true;
});
print('✅ 已滑动到 timelines 列表底部!');
//
}
} else {
if (_isAtBottom) {
setState(() {
_isAtBottom = false;
});
}
}
}
@override
Widget build(BuildContext context) {
@ -97,16 +131,31 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
topAdvancedCalendarWidget(),
Divider(
height: 1,
color: AppColors.greyLineColor,
indent: 30.w,
endIndent: 30.w,
),
eventDropDownWidget(),
Expanded(child: timeLineView())
],
),
floatingActionButton: Visibility(
visible: _isAtBottom,
child: FloatingActionButton(
onPressed: () {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(48.w),
),
backgroundColor: AppColors.mainColor,
child: Icon(
Icons.arrow_upward,
color: Colors.white,
size: 48.w,
),
),
),
);
}
@ -152,88 +201,14 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
}
}
// switch (value) {
// case "读取记录".tr:
// {
// logic.mockNetworkDataRequest(isRefresh: true);
// }
// break;
// case '清空记录'.tr:
// {
// ShowCupertinoAlertView().showClearOperationRecordAlert(
// clearClick: () {
// logic.clearOperationRecordRequest();
// });
// }
// break;
// case '导出记录':
// {
// showDialog(
// context: context,
// builder: (BuildContext context) {
// return ExportRecordDialog(
// onExport: (String filePath) {
// Get.toNamed(Routers.exportSuccessPage,
// arguments: <String, String>{'filePath': filePath});
// },
// );
// },
// );
// }
// break;
// }
// }
//
Widget topAdvancedCalendarWidget() {
final ThemeData theme = Theme.of(context);
return Theme(
data: theme.copyWith(
textTheme: theme.textTheme.copyWith(
titleMedium: theme.textTheme.titleMedium!.copyWith(
fontSize: 16,
color: theme.colorScheme.secondary,
),
bodyLarge: theme.textTheme.bodyLarge!.copyWith(
fontSize: 14,
color: Colors.black54,
),
bodyMedium: theme.textTheme.bodyMedium!.copyWith(
fontSize: 12,
color: Colors.black87,
),
),
primaryColor: AppColors.mainColor,
highlightColor: Colors.yellow,
disabledColor: Colors.grey,
),
child: Stack(
children: <Widget>[
AdvancedCalendar(
controller: state.calendarControllerCustom,
events: state.events,
weekLineHeight: 48.0,
startWeekDay: 1,
innerDot: true,
keepLineSize: true,
calendarTextStyle: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
height: 1.3125,
letterSpacing: 0,
),
),
Positioned(
top: 8.0,
right: 8.0,
child: Obx(() => Text(
'${state.currentSelectDate.value.year}${''.tr}${state.currentSelectDate.value.month}${''.tr}',
style: theme.textTheme.titleMedium!.copyWith(
fontSize: 16,
color: theme.colorScheme.secondary,
),
)),
),
return Container(
margin: EdgeInsets.only(top: 20.h, left: 30.w, bottom: 10.h, right: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWeekCalendar(),
],
),
);
@ -269,15 +244,10 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
),
child: Obx(() => EasyRefreshTool(
onRefresh: () async {
logic.mockNetworkDataRequest(isRefresh: true);
},
onLoad: () async {
logic.mockNetworkDataRequest(isRefresh: false);
},
child: state.lockLogItemList.isNotEmpty
child: Obx(
() => state.lockLogItemList.isNotEmpty
? Timeline.tileBuilder(
controller: _scrollController,
builder: _timelineBuilderWidget(),
theme: TimelineThemeData(
nodePosition: 0.04, //
@ -293,14 +263,19 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> 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);
@ -325,7 +300,8 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
return '${formatTimestampToHHmm(item.operateDate!)} ' +
'密码'.tr +
'开锁'.tr +
'${'昵称'.tr}${item.username}'+'${'密码'.tr}${item.keyboardPwd}';
'${'昵称'.tr}${item.username}' +
'${'密码'.tr}${item.keyboardPwd}';
case 30:
return '${formatTimestampToHHmm(item.operateDate!)} ' +
''.tr +
@ -444,6 +420,11 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'${formatTimestampToDateTimeYYYYMMDD(timelineData.operateDate!)}',
style: TextStyle(
fontSize: 20.sp,
)),
// 使 SingleChildScrollView
SingleChildScrollView(
scrollDirection: Axis.horizontal, //
@ -622,4 +603,56 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
}
state.ifCurrentScreen.value = false;
}
List<DateTime> getCurrentWeekDates() {
final now = DateTime.now();
// weekday: 1=, 2=, ..., 7=
//
// weekday == 7 0
final int daysSinceSunday = now.weekday % 7; // =1 -> %7=1, =7 -> %7=0
final List<DateTime> weekDates = [];
for (int i = 0; i < 7; i++) {
final DateTime day = DateTime(
now.year,
now.month,
now.day - daysSinceSunday + i, //
);
weekDates.add(day);
}
return weekDates;
}
Widget _buildWeekCalendar() {
return Obx(() {
final list = state.weekEventList.value;
final dateSet = list
.map((e) => DateTime.fromMillisecondsSinceEpoch(e.operateDate!))
.map((dt) => dt.withoutTime) //
.toSet(); // Set
AppLog.log('dateSet:${dateSet}');
return WeekCalendarView(
hasData: (DateTime date) {
return dateSet.contains(date.withoutTime);
},
onDateSelected: (DateTime date) async {
print('外部收到选中: $date');
state.operateDate = date.millisecondsSinceEpoch;
state.startDate.value =
DateTime(date.year, date.month, date.day).millisecondsSinceEpoch;
state.endDate.value =
DateTime(date.year, date.month, date.day, 23, 59, 59, 999)
.millisecondsSinceEpoch;
await logic.mockNetworkDataRequest(isRefresh: true);
},
onWeekChanged: (DateTime start, DateTime end) {
state.startDate.value = start.millisecondsSinceEpoch;
state.endDate.value = end.millisecondsSinceEpoch;
logic.mockNetworkDataRequest(isRefresh: true);
},
);
});
}
}

View File

@ -1,4 +1,3 @@
import 'package:get/get.dart';
import 'package:star_lock/common/XSConstantMacro/XSConstantMacro.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
@ -13,10 +12,15 @@ class DoorLockLogState {
DoorLockLogState() {
keyInfos.value = Get.arguments['keyInfo'];
}
final Rx<DoorLockLogEntity> lockLogEntity = DoorLockLogEntity().obs;
final Rx<LockListInfoItemEntity> keyInfos = LockListInfoItemEntity().obs;
final RxList<DoorLockLogDataItem> lockLogItemList =
<DoorLockLogDataItem>[].obs;
final RxList<DoorLockLogDataItem> weekEventList =
<DoorLockLogDataItem>[].obs;
final RxList<DoorLockLogDataItem> dayEventList =
<DoorLockLogDataItem>[].obs;
final AdvancedCalendarController calendarControllerToday =
AdvancedCalendarController.today();
final AdvancedCalendarController calendarControllerCustom =

View File

@ -0,0 +1,220 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:star_lock/app_settings/app_colors.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/date_time_extensions.dart';
class WeekCalendarView extends StatefulWidget {
//
final bool Function(DateTime date)? hasData;
final void Function(DateTime date)? onDateSelected; //
final void Function(DateTime start, DateTime end)? onWeekChanged;
const WeekCalendarView({
Key? key,
this.hasData,
this.onDateSelected,
this.onWeekChanged,
}) : super(key: key);
@override
_WeekCalendarViewState createState() => _WeekCalendarViewState();
}
class _WeekCalendarViewState extends State<WeekCalendarView> {
final PageController _pageController = PageController(initialPage: 500);
int _currentPage = 500;
// DateTime
late DateTime _selectedDate;
@override
void initState() {
super.initState();
_selectedDate = DateTime.now().withoutTime; //
}
// page
List<DateTime> _getWeekDatesForPage(int page) {
final now = DateTime.now();
final baseSunday =
DateTime(now.year, now.month, now.day - (now.weekday % 7));
final daysOffset = (page - 500) * 7;
final targetSunday = baseSunday.add(Duration(days: daysOffset));
return List.generate(
7,
(i) => DateTime(
targetSunday.year, targetSunday.month, targetSunday.day + i));
}
//
bool _isToday(DateTime date) {
final now = DateTime.now();
return date.year == now.year &&
date.month == now.month &&
date.day == now.day;
}
//
bool _isSelected(DateTime date) {
return date.year == _selectedDate.year &&
date.month == _selectedDate.month &&
date.day == _selectedDate.day;
}
//
bool _hasData(DateTime date) {
return widget.hasData?.call(date.withoutTime) ?? false;
}
void _onDateSelected(DateTime date) {
setState(() {
_selectedDate = date.withoutTime;
});
//
widget.onDateSelected?.call(date);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
_buildWeekRangeLabel(_currentPage),
SizedBox(height: 10.h),
SizedBox(
height: 100.h,
child: PageView.builder(
controller: _pageController,
itemCount: 1000,
itemBuilder: (context, page) {
final weekDates = _getWeekDatesForPage(page);
return Row(
children: weekDates.asMap().entries.map((entry) {
final int index = entry.key;
final DateTime date = entry.value;
final bool isSelected = _isSelected(date);
final bool hasData = _hasData(date);
final bool isToday = _isToday(date);
//
Color textColor;
if (isSelected) {
textColor = Colors.white; //
} else if (hasData) {
textColor = Colors.black; //
} else if (isToday) {
textColor = Colors.black; //
} else {
textColor = Colors.grey; //
}
//
Color? bgColor;
if (isSelected) {
bgColor = AppColors.mainColor; //
}
//
return GestureDetector(
onTap: () => _onDateSelected(date),
child: Container(
padding: EdgeInsets.all(4.w),
width: 75.w,
height: 75.w,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(50.r),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
[
'简写周日',
'简写周一',
'简写周二',
'简写周三',
'简写周四',
'简写周五',
'简写周六'
][index]
.tr,
style: TextStyle(
fontSize: 14.sp,
color: textColor,
fontWeight: FontWeight.w400,
),
),
Text(
date.day.toString(),
style: TextStyle(
fontSize: 26.sp,
color: textColor,
fontWeight: FontWeight.w600,
),
),
if (isToday && !isSelected) //
SizedBox(height: 2.h),
if (isToday && !isSelected)
Container(
width: 6.w,
height: 6.w,
decoration: BoxDecoration(
color: AppColors.mainColor,
shape: BoxShape.circle,
),
),
],
),
),
);
}).toList(),
);
},
onPageChanged: (page) {
setState(() {
_currentPage = page;
});
//
final dates = _getWeekDatesForPage(page);
final startOfWeek = dates.first;
final endOfWeek = dates.last;
//
widget.onWeekChanged?.call(startOfWeek, endOfWeek);
},
),
),
],
);
}
Widget _buildWeekRangeLabel(int page) {
final dates = _getWeekDatesForPage(page);
final start = dates[0];
final end = dates[6];
String label;
if (start.year == end.year) {
// "2025年8月18日 - 8月24日"
label =
'${start.year}${''.tr}${start.month}${''.tr}${start.day}${''.tr} - ${end.month}${''.tr}${end.day}${''.tr}';
} else {
// "2024年12月31日 - 2025年1月6日"
label =
'${start.year}${''.tr}${start.month}${''.tr}${start.day}${''.tr} - ${end.year}${''.tr}${end.month}${''.tr}${end.day}${''.tr}';
}
return Text(
label,
style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.w600),
);
}
}

View File

@ -253,10 +253,10 @@ class AddFingerprintLogic extends BaseGetXController {
final List<int> getTokenList = changeStringListToIntList(token!);
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',

View File

@ -12,6 +12,7 @@ import 'package:star_lock/blue/blue_manage.dart';
import 'package:star_lock/blue/io_protocol/io_getDeviceModel.dart';
import 'package:star_lock/blue/io_protocol/io_otaUpgrade.dart';
import 'package:star_lock/blue/io_protocol/io_processOtaUpgrade.dart';
import 'package:star_lock/blue/io_protocol/io_setVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigure.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart';
import 'package:star_lock/blue/io_reply.dart';
@ -52,6 +53,8 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
_handlerVoicePackageConfigureProcess(reply);
} else if (reply is VoicePackageConfigureConfirmationReply) {
handleVoiceConfigureThrottled(reply);
} else if (reply is SetVoicePackageFinalResultReply) {
handleSetResult(reply);
}
});
await initList();
@ -93,7 +96,7 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
final passthroughItem = PassthroughItem(
lang: element.lang,
timbres: element.timbres,
langText: '简体中文'.tr + '(中国台湾)'.tr,
langText: '简体中文'.tr + '(中国台湾)'.tr + 'Simplified Chinese TW',
name: element.name,
);
state.languages.add(passthroughItem);
@ -432,6 +435,33 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
_handlerVoicePackageConfigureConfirmation(
VoicePackageConfigureConfirmationReply reply,
) async {
final int status = reply.data[2];
switch (status) {
case 0x00:
await BlueManage().blueSendData(BlueManage().connectDeviceName,
(BluetoothConnectionState deviceConnectionState) async {
if (deviceConnectionState == BluetoothConnectionState.connected) {
await BlueManage().writeCharacteristicWithResponse(
SetVoicePackageFinalResult(
lockID: BlueManage().connectDeviceName,
languageCode: state.tempLangStr.value,
).packageData(),
);
} else if (deviceConnectionState ==
BluetoothConnectionState.disconnected) {
dismissEasyLoading();
cancelBlueConnetctToastTimer();
showBlueConnetctToast();
}
});
break;
default:
showToast('设置'.tr + '失败'.tr);
break;
}
}
void handleSetResult(SetVoicePackageFinalResultReply reply) async {
final int status = reply.data[2];
switch (status) {
case 0x00:

View File

@ -107,6 +107,7 @@ class _SpeechLanguageSettingsPageState
isHaveLine: true,
isHaveDirection: false,
isHaveRightWidget: true,
leftTitleMaxWidth: 0.9.sw, //
rightWidget:
state.selectPassthroughListIndex.value == index
? Image(
@ -131,66 +132,6 @@ class _SpeechLanguageSettingsPageState
);
}
Widget _buildBody() {
return Obx(
() => SingleChildScrollView(
child: Column(
children: [
ListView.builder(
itemCount: state.soundTypeList.length,
itemBuilder: (BuildContext context, int index) {
// itemCount - 1
final isLastItem = index == state.soundTypeList.length - 1;
// platFormSet RxList<Platform>
final platform = state.soundTypeList.value[index];
return CommonItem(
leftTitel: state.soundTypeList.value[index],
rightTitle: '',
isHaveLine: !isLastItem,
// 线
isHaveDirection: false,
isHaveRightWidget: true,
rightWidget: Radio<String>(
// Radio 使 id
value: platform,
// selectPlatFormIndex id
groupValue: state.soundTypeList
.value[state.selectSoundTypeIndex.value],
//
activeColor: AppColors.mainColor,
// Radio
onChanged: (value) {
if (value != null) {
setState(() {
// id
final newIndex = state.soundTypeList.value
.indexWhere((p) => p == value);
if (newIndex != -1) {
state.selectSoundTypeIndex.value = newIndex;
}
});
}
},
),
action: () {
setState(() {
state.selectSoundTypeIndex.value = index;
});
},
);
},
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics() //add this line,
),
Column(
children: _buildList(),
),
],
),
),
);
}
List<Widget> _buildList() {
final appLocalLanguages = state.languages;

View File

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

View File

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

View File

@ -76,23 +76,31 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
body: Column(
children: <Widget>[
Expanded(
child: Obx(() => ListView.builder(
child: Obx(
() => ListView.builder(
itemCount: state.videoLogList.length,
itemBuilder: (BuildContext c, int index) {
final CloudStorageData item = state.videoLogList[index];
return Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(
left: 20.w, top: 15.w, bottom: 15.w),
child: Row(children: <Widget>[
Text(item.date ?? '',
style: TextStyle(fontSize: 20.sp)),
])),
mainListView(index, item)
],
return ExpansionTile(
shape: Border(),
collapsedShape: Border(),
expansionAnimationStyle: AnimationStyle(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 400),
),
initiallyExpanded: true,
title: Text(
item.date ?? '',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
children: mainListView(index, item),
);
})),
},
),
),
),
bottomBottomBtnWidget()
],
@ -100,29 +108,35 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
);
}
Widget _buildNotData() {
return Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
'images/icon_noData.png',
width: 160.w,
height: 180.h,
),
Text(
'暂无数据'.tr,
style: TextStyle(
color: AppColors.darkGrayTextColor, fontSize: 22.sp),
)
],
),
),
);
}
double itemW = (1.sw - 15.w * 4) / 3;
double itemH = (1.sw - 15.w * 4) / 3 + 40.h;
Widget mainListView(int index, CloudStorageData itemData) {
return GridView.builder(
padding: EdgeInsets.only(left: 15.w, right: 15.w),
itemCount: itemData.recordList!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//
crossAxisCount: 3,
//
mainAxisSpacing: 10.w,
//
crossAxisSpacing: 15.w,
//
childAspectRatio: itemW / itemH),
itemBuilder: (BuildContext context, int index) {
final RecordListData recordData = itemData.recordList![index];
return videoItem(recordData);
},
);
//
List<Widget> mainListView(int index, CloudStorageData itemData) {
return itemData.recordList!.map((e) => videoItem(e)).toList();
}
// Widget videoItem(RecordListData recordData, int index) {
@ -237,9 +251,9 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
if (state.selectVideoLogList.value.isNotEmpty) {
state.selectVideoLogList.value.forEach((element) {
if (element.videoUrl != null && element.videoUrl != '') {
logic.downloadFile(element.videoUrl ?? '');
logic.downloadFile(element.videoUrl ?? '', element.recordType!);
} else if (element.imagesUrl != null && element.imagesUrl != '') {
logic.downloadFile(element.imagesUrl ?? '');
logic.downloadFile(element.imagesUrl ?? '', element.recordType!);
}
});
// double _progress = 0.0;
@ -338,55 +352,6 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
Widget videoItem(RecordListData recordData) {
return GestureDetector(
onTap: () {
if (recordData.videoUrl != null && recordData.videoUrl!.isNotEmpty) {
Get.toNamed(Routers.videoLogDetailPage, arguments: <String, Object>{
'recordData': recordData,
'videoDataList': state.videoLogList.value
});
} else if (recordData.imagesUrl != null &&
recordData.imagesUrl!.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FullScreenImagePage(
imageUrl: recordData.imagesUrl!,
),
),
);
}
},
child: Stack(
children: [
SizedBox(
width: itemW,
height: itemH,
child: Column(
children: <Widget>[
Container(
width: itemW,
height: itemW,
margin: const EdgeInsets.all(0),
color: Colors.white,
child: ClipRRect(
borderRadius: BorderRadius.circular(10.w),
child: _buildImageOrVideoItem(recordData),
),
),
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) {
@ -396,20 +361,146 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
}
setState(() {});
},
child: Image(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w),
margin: EdgeInsets.only(
bottom: 20.h,
left: 18.w,
right: 18.w,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.w),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
width: 1.sw,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: [
Image(
width: 36.w,
height: 36.w,
image: state.selectVideoLogList.value.contains(recordData)
? const AssetImage('images/icon_round_select.png')
: const AssetImage('images/icon_round_unSelect.png'),
),
SizedBox(
width: 14.w,
),
Container(
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: 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);

View File

@ -1,7 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:star_lock/appRouters.dart';
@ -63,8 +63,17 @@ class VideoLogLogic extends BaseGetXController {
final content = await File(logFilePath).readAsString();
final logData = Map<String, int>.from(json.decode(content));
//
logData.forEach((filePath, timestamp) {
String fileName = filePath
.split('/')
.last; // : 220_f5e371111918ff70cb3532bec20e38c4.mp4
String withoutExt = fileName.replaceAll('.mp4', ''); // 使 substring
String numberStr = withoutExt.split('_').first; // : 220
int number = int.parse(numberStr);
print(number); // : 220
final downloadDateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final dateKey =
'${downloadDateTime.year}-${downloadDateTime.month.toString().padLeft(2, '0')}-${downloadDateTime.day.toString().padLeft(2, '0')}';
@ -77,11 +86,15 @@ class VideoLogLogic extends BaseGetXController {
//
if (filePath.endsWith('.jpg')) {
groupedDownloads[dateKey]?.add(
RecordListData(operateDate: timestamp, imagesUrl: filePath),
RecordListData(
operateDate: timestamp,
imagesUrl: filePath,
recordType: number),
);
} else if (filePath.endsWith('.mp4')) {
groupedDownloads[dateKey]?.add(
RecordListData(operateDate: timestamp, videoUrl: filePath),
RecordListData(
operateDate: timestamp, videoUrl: filePath, recordType: number),
);
}
});

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/flavors.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_state.dart';
@ -26,9 +27,7 @@ class VideoLogPage extends StatefulWidget {
class _VideoLogPageState extends State<VideoLogPage> {
final VideoLogLogic logic = Get.put(VideoLogLogic());
final VideoLogState state = Get
.find<VideoLogLogic>()
.state;
final VideoLogState state = Get.find<VideoLogLogic>().state;
@override
void initState() {
@ -56,8 +55,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
// title加编辑按钮
editVideoTip(),
Obx(
() =>
Visibility(
() => Visibility(
visible: !state.isNavLocal.value,
child: state.videoLogList.length > 0
? Expanded(
@ -66,17 +64,22 @@ class _VideoLogPageState extends State<VideoLogPage> {
itemBuilder: (BuildContext c, int index) {
final CloudStorageData item =
state.videoLogList[index];
return Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(
left: 20.w, top: 15.w, bottom: 15.w),
child: Row(children: <Widget>[
Text(item.date ?? '',
style: TextStyle(fontSize: 20.sp)),
])),
mainListView(index, item)
],
return ExpansionTile(
shape: Border(),
collapsedShape: Border(),
expansionAnimationStyle: AnimationStyle(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 400),
),
initiallyExpanded: true,
title: Text(
item.date ?? '',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
children: mainListView(index, item),
);
},
),
@ -86,8 +89,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
),
//
Obx(
() =>
Visibility(
() => Visibility(
visible: state.isNavLocal.value,
child: state.lockVideoList.length > 0
? Expanded(
@ -96,20 +98,22 @@ class _VideoLogPageState extends State<VideoLogPage> {
itemBuilder: (BuildContext c, int index) {
final CloudStorageData item =
state.lockVideoList[index];
return Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(
left: 20.w, top: 15.w, bottom: 15.w),
child: Row(
children: <Widget>[
Text(item.date ?? '',
style: TextStyle(fontSize: 20.sp)),
],
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)
],
children: mainListView(index, item),
);
},
),
@ -161,8 +165,9 @@ class _VideoLogPageState extends State<VideoLogPage> {
// logic.clearDownloads();
});
},
child: Obx(() =>
Text('云存'.tr,
child: Obx(
() => Text(
'云存'.tr,
style: state.isNavLocal.value == true
? TextStyle(
color: Colors.grey,
@ -171,7 +176,10 @@ class _VideoLogPageState extends State<VideoLogPage> {
: TextStyle(
color: Colors.white,
fontSize: 28.sp,
fontWeight: FontWeight.w600)))),
fontWeight: FontWeight.w600),
),
),
),
TextButton(
onPressed: () {
setState(() {
@ -180,8 +188,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
});
},
child: Obx(
() =>
Text(
() => Text(
'已下载'.tr,
style: state.isNavLocal.value == true
? TextStyle(
@ -215,10 +222,12 @@ class _VideoLogPageState extends State<VideoLogPage> {
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)),
borderRadius: BorderRadius.circular(
20.h,
),
),
child: Obx(
() =>
Column(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
@ -227,15 +236,11 @@ class _VideoLogPageState extends State<VideoLogPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('3天滚动储存'.tr,
style: TextStyle(fontSize: 24.sp)),
Text('3天滚动储存'.tr, style: TextStyle(fontSize: 24.sp)),
SizedBox(height: 10.h),
Text("${F
.navTitle}${"已为本设备免费提供3大滚动视频储存服务"
.tr}",
Text("${F.navTitle}${"已为本设备免费提供3大滚动视频储存服务".tr}",
style:
TextStyle(fontSize: 22.sp, color: Colors
.grey)),
TextStyle(fontSize: 22.sp, color: Colors.grey)),
],
)),
SizedBox(width: 15.w),
@ -243,8 +248,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
Image(
width: 40.w,
height: 24.w,
image: const AssetImage(
'images/icon_right_black.png'))
image: const AssetImage('images/icon_right_black.png'))
],
),
SizedBox(
@ -263,8 +267,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
visible: state.validityPeriodInfo.value != null &&
state.validityPeriodInfo.value?.status == 1,
child: Text(
'过期时间:${state.validityPeriodInfo.value
?.validityPeriodEnd}',
'过期时间:${state.validityPeriodInfo.value?.validityPeriodEnd}',
style: TextStyle(
fontSize: 24.sp,
),
@ -290,8 +293,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
visible: state.validityPeriodInfo.value != null &&
state.validityPeriodInfo.value?.status == 1,
child: Text(
'剩余天数:${state.validityPeriodInfo.value
?.remainingDays} ',
'剩余天数:${state.validityPeriodInfo.value?.remainingDays}',
style: TextStyle(
fontSize: 24.sp,
),
@ -412,48 +414,12 @@ class _VideoLogPageState extends State<VideoLogPage> {
double itemH = (1.sw - 15.w * 4) / 3 + 40.h;
//
Widget mainListView(int index, CloudStorageData itemData) {
return GridView.builder(
padding: EdgeInsets.only(left: 15.w, right: 15.w),
itemCount: itemData.recordList!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//
crossAxisCount: 3,
//
mainAxisSpacing: 15.w,
//
crossAxisSpacing: 15.w,
//
childAspectRatio: itemW / itemH),
itemBuilder: (BuildContext context, int index) {
final RecordListData recordData = itemData.recordList![index];
return videoItem(recordData);
},
);
List<Widget> mainListView(int index, CloudStorageData itemData) {
return itemData.recordList!.map((e) => videoItem(e)).toList();
}
Widget lockMainListView(int index, CloudStorageData itemData) {
return GridView.builder(
padding: EdgeInsets.only(left: 15.w, right: 15.w),
itemCount: itemData.recordList!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//
crossAxisCount: 3,
//
mainAxisSpacing: 15.w,
//
crossAxisSpacing: 15.w,
//
childAspectRatio: itemW / itemH),
itemBuilder: (BuildContext context, int index) {
final RecordListData recordData = itemData.recordList![index];
return videoItem(recordData);
},
);
List<Widget> lockMainListView(int index, CloudStorageData itemData) {
return itemData.recordList!.map((e) => videoItem(e)).toList();
}
Widget videoItem(RecordListData recordData) {
@ -469,22 +435,86 @@ class _VideoLogPageState extends State<VideoLogPage> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
FullScreenImagePage(
builder: (context) => FullScreenImagePage(
imageUrl: recordData.imagesUrl!,
),
),
);
}
},
child: SizedBox(
width: itemW,
height: itemH,
child: Column(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w),
margin: EdgeInsets.only(
bottom: 20.h,
left: 18.w,
right: 18.w,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.w),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
width: 1.sw,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: [
Container(
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(58.w),
color: AppColors.mainColor,
),
child: Icon(
_buildIconByType(recordData),
size: 48.sp,
color: Colors.white,
),
),
SizedBox(
width: 14.w,
),
Container(
width: itemW,
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: 118.w,
height: 118.w,
margin: const EdgeInsets.all(0),
color: Colors.white,
child: ClipRRect(
@ -492,12 +522,6 @@ class _VideoLogPageState extends State<VideoLogPage> {
child: _buildImageOrVideoItem(recordData),
),
),
SizedBox(height: 5.h),
Text(
DateTool().dateToYMDHNString(recordData.operateDate.toString()),
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18.sp),
)
],
),
),
@ -512,6 +536,50 @@ class _VideoLogPageState extends State<VideoLogPage> {
}
}
String _buildTitleByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return '防拆报警'.tr;
case 160:
return '人脸'.tr + '开锁'.tr;
case 220:
return '逗留警告'.tr;
default:
return '';
}
}
IconData _buildIconByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return Icons.fmd_bad_outlined;
case 160:
return Icons.tag_faces_outlined;
case 220:
return Icons.wifi_tethering_error_rounded_outlined;
default:
return Icons.priority_high_rounded;
}
}
Color _buildTextColorByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 120:
case 150:
case 130:
case 190:
case 200:
case 210:
case 220:
return Colors.red;
default:
return Colors.black;
}
}
_buildVideoItem(RecordListData recordData) {
return VideoThumbnailImage(videoUrl: recordData.videoUrl!);
}

View File

@ -1,9 +1,13 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/controlsOverlay_page.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/videoLogDetail_state.dart';
@ -30,11 +34,11 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
@override
void initState() {
super.initState();
AppLog.log(
'state.recordData.value.videoUrl!' + state.recordData.value.videoUrl!);
state.videoController = VideoPlayerController.networkUrl(
Uri.parse(state.recordData.value.videoUrl!),
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
state.videoController =
createVideoController(state.recordData.value.videoUrl!);
state.videoController.addListener(() {
setState(() {});
@ -47,10 +51,8 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
if (state.videoController != null) {
await state.videoController.dispose(); //
}
state.videoController = VideoPlayerController.networkUrl(
Uri.parse(videoUrl),
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
state.videoController =
createVideoController(state.recordData.value.videoUrl!);
//
await state.videoController.initialize();
@ -60,6 +62,22 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
setState(() {});
}
VideoPlayerController createVideoController(String url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
return VideoPlayerController.networkUrl(
Uri.parse(url),
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
} else {
final file = File(
url.startsWith('file://') ? url.replaceFirst('file://', '') : url);
return VideoPlayerController.file(
file,
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -104,6 +122,7 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
],
),
),
// _buildTitleRow(),
_buildOther(),
],
)
@ -135,14 +154,79 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
);
}
},
child: SizedBox(
width: itemW,
height: itemH,
child: Column(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w),
margin: EdgeInsets.only(
bottom: 20.h,
left: 18.w,
right: 18.w,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.w),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
width: 1.sw,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: [
Container(
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(58.w),
color: AppColors.mainColor,
),
child: Icon(
_buildIconByType(recordData),
size: 48.sp,
color: Colors.white,
),
),
SizedBox(
width: 14.w,
),
Container(
width: itemW,
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: 118.w,
height: 118.w,
margin: const EdgeInsets.all(0),
color: Colors.white,
child: ClipRRect(
@ -213,11 +297,13 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
margin: EdgeInsets.only(left: 20.w, top: 15.w, bottom: 15.w),
child: Row(
children: <Widget>[
Text(item.date ?? '', style: TextStyle(fontSize: 20.sp)),
Text(item.date ?? '',
style: TextStyle(
fontSize: 24.sp, fontWeight: FontWeight.w600)),
],
),
),
mainListView(index, item),
...mainListView(index, item),
],
);
},
@ -225,22 +311,60 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
);
}
Widget mainListView(int index, CloudStorageData itemData) {
return GridView.builder(
itemCount: itemData.recordList!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//
crossAxisCount: 3,
),
itemBuilder: (BuildContext context, int index) {
return _buildItem(itemData.recordList![index]);
},
);
//
List<Widget> mainListView(int index, CloudStorageData itemData) {
return itemData.recordList!.map((e) => videoItem(e)).toList();
}
String _buildTitleByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return '防拆报警'.tr;
case 160:
return '人脸'.tr + '开锁'.tr;
case 220:
return '逗留警告'.tr;
default:
return '';
}
}
IconData _buildIconByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return Icons.fmd_bad_outlined;
case 160:
return Icons.tag_faces_outlined;
case 220:
return Icons.wifi_tethering_error_rounded_outlined;
default:
return Icons.priority_high_rounded;
}
}
_buildItem(itemData) {
return videoItem(itemData);
}
_buildTitleRow() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
),
padding: EdgeInsets.only(left: 15.w, top: 24.w, bottom: 24.w),
child: Row(
children: [
Text(
_buildTitleByType(state.recordData.value) ?? '',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}

View File

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

View File

@ -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

View File

@ -11,6 +11,7 @@ import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_colors.dart';
import 'package:star_lock/blue/blue_manage.dart';
import 'package:star_lock/blue/io_protocol/io_getDeviceModel.dart';
import 'package:star_lock/blue/io_protocol/io_readVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigure.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart';
import 'package:star_lock/blue/io_reply.dart';
@ -50,9 +51,12 @@ class LockVoiceSettingLogic extends BaseGetXController {
_handlerVoicePackageConfigureProcess(reply);
} else if (reply is VoicePackageConfigureConfirmationReply) {
handleVoiceConfigureThrottled(reply);
} else if (reply is ReadLockCurrentVoicePacketReply) {
handleLockCurrentVoicePacketResult(reply);
}
});
initList();
readLockLanguage();
}
void handleVoiceConfigureThrottled(
@ -197,7 +201,7 @@ class LockVoiceSettingLogic extends BaseGetXController {
//
void _handlerStartVoicePackageConfigure(
VoicePackageConfigureReply reply) async {
final int status = reply.data[6];
final int status = reply.data[3];
switch (status) {
case 0x00:
//
@ -250,11 +254,10 @@ class LockVoiceSettingLogic extends BaseGetXController {
if (lang == 'zh_TW') {
//
List<String> parts = lang.split('_');
final indexOf = locales.indexOf(Locale(parts[0], parts[1]));
final passthroughItem = PassthroughItem(
lang: element.lang,
timbres: element.timbres,
langText: '简体中文'.tr + '(中国台湾)'.tr,
langText: '简体中文'.tr + '(中国台湾)'.tr + 'Simplified Chinese TW',
name: element.name,
);
state.languages.add(passthroughItem);
@ -268,6 +271,7 @@ class LockVoiceSettingLogic extends BaseGetXController {
ExtensionLanguageType.fromLocale(locales[indexOf]).lanTitle,
name: element.name,
);
state.languages.add(passthroughItem);
}
});
@ -403,4 +407,71 @@ class LockVoiceSettingLogic extends BaseGetXController {
state.data = null;
super.onClose();
}
void readLockLanguage() async {
await BlueManage().blueSendData(BlueManage().connectDeviceName,
(BluetoothConnectionState deviceConnectionState) async {
if (deviceConnectionState == BluetoothConnectionState.connected) {
await BlueManage().writeCharacteristicWithResponse(
ReadLockCurrentVoicePacket(
lockID: BlueManage().connectDeviceName,
).packageData(),
);
} else if (deviceConnectionState ==
BluetoothConnectionState.disconnected) {
dismissEasyLoading();
cancelBlueConnetctToastTimer();
showBlueConnetctToast();
}
});
}
void handleLockCurrentVoicePacketResult(
ReadLockCurrentVoicePacketReply reply) {
final int status = reply.data[6];
switch (status) {
case 0x00:
//
cancelBlueConnetctToastTimer();
// 1. LanguageCode
// CmdID (2 bytes) + Status (1 byte) = 3 bytes -> LanguageCode 3
const int languageCodeStartIndex = 3;
const int languageCodeLength = 20;
const int languageCodeEndIndex =
languageCodeStartIndex + languageCodeLength; // 23
// 2.
if (reply.data.length < languageCodeEndIndex) {
throw Exception(
'Reply data is too short to contain LanguageCode. Expected at least $languageCodeEndIndex bytes, got ${reply.data.length}');
}
// 3. LanguageCode
List<int> languageCodeBytes =
reply.data.sublist(languageCodeStartIndex, languageCodeEndIndex);
// 4.
// UTF-8 ASCII
String languageCode = String.fromCharCodes(languageCodeBytes);
// 5. () '\0'
// 20 '\0'
languageCode = languageCode.trim(); //
languageCode =
languageCode.replaceAll('\u0000', ''); // (null bytes)
// 6. 使 languageCode
print('LanguageCode: $languageCode'); // : zh_CN, en_US
break;
case 0x06:
//
final List<int> token = reply.data.sublist(2, 6);
if (state.data != null) {
sendFileToDevice(state.data!, token);
}
break;
default:
break;
}
}
}

View File

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

View File

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

View File

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

View File

@ -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++;

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_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);

View File

@ -1,11 +1,45 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:star_lock/mine/message/messageList/messageList_entity.dart';
import 'package:star_lock/network/api_repository.dart';
import 'package:star_lock/network/start_chart_api.dart';
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
import 'package:star_lock/tools/baseGetXController.dart';
import 'package:star_lock/tools/eventBusEventManage.dart';
import 'package:star_lock/tools/storage.dart';
class AppLifecycleObserver extends WidgetsBindingObserver {
//
StreamSubscription? _readMessageRefreshUIEvent;
void _readMessageRefreshUIAction() {
// eventBus
_readMessageRefreshUIEvent =
eventBus.on<ReadTalkMessageRefreshUI>().listen((event) async {
//
final MessageListEntity entity = await ApiRepository.to
.messageListLoadData(pageNo: '1', pageSize: '1');
if (entity.errorCode!.codeIsSuccessful) {
final lockName = event.lockName;
if (lockName != null && lockName.isNotEmpty) {
final readAt = entity.data?.list?.first.readAt == 0;
final data = entity.data?.list?.first.data;
if (readAt && data != null && data.contains(lockName)) {
//
final entity2 = await ApiRepository.to
.readMessageLoadData(messageId: entity.data!.list!.first.id!);
if (entity2.errorCode!.codeIsSuccessful) {
eventBus.fire(ReadMessageRefreshUI());
}
}
}
}
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
@ -37,6 +71,7 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
Get.back();
}
StartChartManage().destruction();
_readMessageRefreshUIEvent?.cancel();
}
void onAppResumed() async {
@ -52,6 +87,9 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
StartChartApi.to.startChartHost =
loginData!.starchart!.scdUrl ?? StartChartApi.to.startChartHost;
}
//
_readMessageRefreshUIAction();
print('App has resumed to the foreground.');
}

View File

@ -561,8 +561,11 @@ class ImageTransmissionLogic extends BaseGetXController {
state.voiceProcessor = VoiceProcessor.instance;
}
Timer? _startProcessingAudioTimer;
//
Future<void> startProcessingAudio() async {
try {
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
@ -580,7 +583,6 @@ class ImageTransmissionLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex';
}
state.isOpenVoice.value = false;
}
///
@ -602,48 +604,53 @@ class ImageTransmissionLogic extends BaseGetXController {
} finally {
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
}
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
}
//
Future<void> _onFrame(List<int> frame) async {
//
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); //
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 40; // 40ms发送一次4chunk
void _sendAudioChunk(Timer timer) async {
if (_bufferedAudioFrames.length < chunkSize) {
//
return;
}
//
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
// 使
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使
int getFrameLength = state.frameLength;
if (Platform.isIOS) {
getFrameLength = state.frameLength * 2;
}
// 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()}...');
//
if (_bufferedAudioFrames.length >= state.frameLength) {
try {
await StartChartManage().sendTalkDataMessage(
talkData: TalkData(
content: _bufferedAudioFrames,
content: chunk,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
} finally {
_bufferedAudioFrames.clear(); //
}
} else {
_bufferedAudioFrames.addAll(encodedData);
}
}
//
Future<void> _onFrame(List<int> frame) async {
final applyGain = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
//
if (_startProcessingAudioTimer == null && _bufferedAudioFrames.length > chunkSize) {
_startProcessingAudioTimer = Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
}
}
//
void _onError(VoiceProcessorException error) {
AppLog.log(error.message!);

View File

@ -9,7 +9,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:get/get.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
@ -18,28 +17,20 @@ import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/login/login/entity/LoginEntity.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart';
import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart';
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
import 'package:star_lock/network/api_repository.dart';
import 'package:star_lock/talk/call/callTalk.dart';
import 'package:star_lock/talk/call/g711.dart';
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart';
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart';
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
import 'package:star_lock/tools/G711Tool.dart';
import 'package:star_lock/tools/bugly/bugly_tool.dart';
import 'package:star_lock/tools/callkit_handler.dart';
import 'package:star_lock/tools/commonDataManage.dart';
import 'package:star_lock/tools/storage.dart';
import 'package:video_decode_plugin/nalu_utils.dart';
import 'package:video_decode_plugin/video_decode_plugin.dart';
import '../../../../tools/baseGetXController.dart';
@ -51,7 +42,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
int bufferSize = 25; //
int audioBufferSize = 2; // 2
int audioBufferSize = 20; // 2
// frameSeq较小时阈值也小
int _getFrameSeqRolloverThreshold(int lastSeq) {
@ -107,12 +98,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
state.isLoading.value = true;
//
final config = VideoDecoderConfig(
width: 864,
width: StartChartManage().videoWidth,
//
height: 480,
height: StartChartManage().videoHeight,
codecType: 'h264',
);
// textureId
AppLog.log('StartChartManage().videoWidth:${StartChartManage()
.videoWidth}');
AppLog.log('StartChartManage().videoHeight:${StartChartManage()
.videoHeight}');
final textureId = await VideoDecodePlugin.initDecoder(config);
if (textureId != null) {
Future.microtask(() => state.textureId.value = textureId);
@ -144,11 +139,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
FlutterPcmSound.setLogLevel(LogLevel.none);
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
// feed
if (Platform.isAndroid) {
FlutterPcmSound.setFeedThreshold(1024); // Android
} else {
FlutterPcmSound.setFeedThreshold(2000); // Android
}
// if (Platform.isAndroid) {
// FlutterPcmSound.setFeedThreshold(1024); // Android
// } else {
// FlutterPcmSound.setFeedThreshold(4096); // Android
// }
}
///
@ -499,15 +494,18 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
///
void _playAudioData(TalkData talkData) async {
if (state.isOpenVoice.value && state.isLoading.isFalse) {
final list =
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
// // PCM PcmArrayInt16
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
List<int> encodedData = G711Tool.decode(talkData.content, 0); // 0A-law
// PCM PcmArrayInt16
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(encodedData);
FlutterPcmSound.feed(fromList);
if (!state.isPlaying.value) {
AppLog.log('play');
FlutterPcmSound.play();
state.isPlaying.value = true;
}
} else if (state.isOpenVoice.isFalse) {
FlutterPcmSound.pause();
state.isPlaying.value = false;
}
}
@ -573,8 +571,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
void onInit() {
super.onInit();
//
_startListenTalkData();
//
_startListenTalkStatus();
//
@ -596,6 +592,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// H264帧缓冲区
state.h264FrameBuffer.clear();
state.isProcessingFrame = false;
//
_startListenTalkData();
}
@override
@ -639,7 +638,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// I帧集合
_decodedIFrames.clear();
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
super.onClose();
}
@ -652,33 +653,12 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
///
void updateTalkExpect() {
// VideoTypeE的映射
final Map<String, VideoTypeE> qualityToVideoType = {
'标清': VideoTypeE.H264,
'高清': VideoTypeE.H264_720P,
//
};
TalkExpectReq talkExpectReq = TalkExpectReq();
state.isOpenVoice.value = !state.isOpenVoice.value;
// videoType
VideoTypeE currentVideoType =
qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264;
if (!state.isOpenVoice.value) {
talkExpectReq = TalkExpectReq(
videoType: [currentVideoType],
audioType: [],
);
showToast('已静音'.tr);
if (state.isOpenVoice.isTrue) {
FlutterPcmSound.play();
} else {
talkExpectReq = TalkExpectReq(
videoType: [currentVideoType],
audioType: [AudioTypeE.G711],
);
FlutterPcmSound.pause();
}
///
StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
talkExpect: talkExpectReq);
}
///
@ -762,8 +742,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
state.voiceProcessor = VoiceProcessor.instance;
}
Timer? _startProcessingAudioTimer;
//
Future<void> startProcessingAudio() async {
try {
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
@ -781,7 +764,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex';
}
state.isOpenVoice.value = false;
}
///
@ -803,51 +785,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
} finally {
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
}
}
//
Future<void> _onFrame(List<int> frame) async {
//
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); //
return;
}
//
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0A-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,452 +814,52 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
return result;
}
/// h264文件frameType
Future<void> _appendH264FrameToFile(
List<int> frameData, TalkDataH264Frame_FrameTypeE frameType) async {
try {
if (state.h264File == null) {
await _initH264File();
}
// NALU分割函数NALU的完整字节数组
List<List<int>> splitNalus(List<int> data) {
List<List<int>> nalus = [];
int i = 0;
while (i < data.length - 3) {
int start = -1;
int next = -1;
if (data[i] == 0x00 && data[i + 1] == 0x00) {
if (data[i + 2] == 0x01) {
start = i;
i += 3;
} else if (i + 3 < data.length &&
data[i + 2] == 0x00 &&
data[i + 3] == 0x01) {
start = i;
i += 4;
} else {
i++;
continue;
}
next = i;
while (next < data.length - 3) {
if (data[next] == 0x00 &&
data[next + 1] == 0x00 &&
((data[next + 2] == 0x01) ||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
break;
}
next++;
}
nalus.add(data.sublist(start, next));
i = next;
} else {
i++;
}
}
int nalusTotalLen =
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
if (nalus.isEmpty && data.isNotEmpty) {
nalus.add(data);
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
nalus.add(data.sublist(nalusTotalLen));
}
return nalus;
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 40; // 40ms发送一次4chunk
void _sendAudioChunk(Timer timer) async {
if (_bufferedAudioFrames.length < chunkSize) {
//
return;
}
// I帧前只缓存SPS/PPS/IDR
if (!_hasWrittenFirstIFrame) {
final nalus = splitNalus(frameData);
List<List<int>> spsList = [];
List<List<int>> ppsList = [];
List<List<int>> idrList = [];
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
// 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,
),
);
}
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');
//
Future<void> _onFrame(List<int> frame) async {
final applyGain = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
//
if (_startProcessingAudioTimer == null && _bufferedAudioFrames.length > chunkSize) {
_startProcessingAudioTimer = Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
}
}
// NALU起始码为0x00000001
List<int> _ensureStartCode(List<int> nalu) {
if (nalu.length >= 4 &&
nalu[0] == 0x00 &&
nalu[1] == 0x00 &&
nalu[2] == 0x00 &&
nalu[3] == 0x01) {
return nalu;
} else if (nalu.length >= 3 &&
nalu[0] == 0x00 &&
nalu[1] == 0x00 &&
nalu[2] == 0x01) {
return [0x00, 0x00, 0x00, 0x01] + nalu.sublist(3);
} else {
return [0x00, 0x00, 0x00, 0x01] + nalu;
//
void _onError(VoiceProcessorException error) {
AppLog.log(error.message!);
}
}
/// NALU头判断
Future<void> _writeSingleFrameToFile(List<int> frameData) async {
bool hasNaluHeader = false;
if (frameData.length >= 4 &&
frameData[0] == 0x00 &&
frameData[1] == 0x00 &&
((frameData[2] == 0x01) ||
(frameData[2] == 0x00 && frameData[3] == 0x01))) {
hasNaluHeader = true;
}
if (hasNaluHeader) {
await state.h264File!.writeAsBytes(frameData, mode: FileMode.append);
} else {
final List<int> naluHeader = [0x00, 0x00, 0x01];
await state.h264File!
.writeAsBytes(naluHeader + frameData, mode: FileMode.append);
}
}
/// h264文件
Future<void> _initH264File() async {
try {
if (state.h264File != null) return;
// Download目录
Directory? downloadsDir;
if (Platform.isAndroid) {
// Android 10+ getExternalStorageDirectory()
downloadsDir = await getExternalStorageDirectory();
// ROMDownload
final downloadPath = '/storage/emulated/0/Download';
if (Directory(downloadPath).existsSync()) {
downloadsDir = Directory(downloadPath);
}
} else {
downloadsDir = await getApplicationDocumentsDirectory();
}
final filePath =
'${downloadsDir!.path}/video_${DateTime.now().millisecondsSinceEpoch}.h264';
state.h264FilePath = filePath;
state.h264File = File(filePath);
if (!await state.h264File!.exists()) {
await state.h264File!.create(recursive: true);
}
AppLog.log('H264文件初始化: $filePath');
} catch (e) {
AppLog.log('H264文件初始化失败: $e');
}
}
/// h264文件
Future<void> _closeH264File() async {
try {
if (state.h264File != null) {
AppLog.log('H264文件已关闭: ${state.h264FilePath ?? ''}');
}
state.h264File = null;
state.h264FilePath = null;
_preIFrameCache.clear();
_hasWrittenFirstIFrame = false;
} catch (e) {
AppLog.log('关闭H264文件时出错: $e');
}
}
/// I帧数据中分割NALU并将SPS/PPS优先放入缓冲区
void _extractAndBufferSpsPpsForBuffer(
List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
List<List<int>> splitNalus(List<int> data) {
List<List<int>> nalus = [];
int i = 0;
while (i < data.length - 3) {
int start = -1;
int next = -1;
if (data[i] == 0x00 && data[i + 1] == 0x00) {
if (data[i + 2] == 0x01) {
start = i;
i += 3;
} else if (i + 3 < data.length &&
data[i + 2] == 0x00 &&
data[i + 3] == 0x01) {
start = i;
i += 4;
} else {
i++;
continue;
}
next = i;
while (next < data.length - 3) {
if (data[next] == 0x00 &&
data[next + 1] == 0x00 &&
((data[next + 2] == 0x01) ||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
break;
}
next++;
}
nalus.add(data.sublist(start, next));
i = next;
} else {
i++;
}
}
int nalusTotalLen =
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
if (nalus.isEmpty && data.isNotEmpty) {
nalus.add(data);
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
nalus.add(data.sublist(nalusTotalLen));
}
return nalus;
}
final nalus = splitNalus(frameData);
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
}
if (nalu.length > offset) {
int naluType = nalu[offset] & 0x1F;
if (naluType == 7) {
// SPS
hasSps = true;
//
if (spsCache == null || !_listEquals(spsCache!, nalu)) {
spsCache = List<int>.from(nalu);
}
} else if (naluType == 8) {
// PPS
hasPps = true;
if (ppsCache == null || !_listEquals(ppsCache!, nalu)) {
ppsCache = List<int>.from(nalu);
}
}
}
}
}
// List比较工具
bool _listEquals(List<int> a, List<int> b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
// I帧处理方法
// void _handleIFrameWithSpsPpsAndIdr(
// List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
// // I帧前所有未处理帧SPS/PPS/I帧
// state.h264FrameBuffer.clear();
// _extractAndBufferSpsPpsForBuffer(
// frameData, durationMs, frameSeq, frameSeqI);
// // SPS/PPS就先写入I帧本体IDR
// if (spsCache == null || ppsCache == null) {
// // SPS/PPS缓存I帧
// return;
// }
// // SPS/PPS
// _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
// frameSeq, frameSeqI);
// _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
// frameSeq, frameSeqI);
// // I帧包IDRtype 5
// List<List<int>> nalus = [];
// int i = 0;
// List<int> data = frameData;
// while (i < data.length - 3) {
// int start = -1;
// int next = -1;
// if (data[i] == 0x00 && data[i + 1] == 0x00) {
// if (data[i + 2] == 0x01) {
// start = i;
// i += 3;
// } else if (i + 3 < data.length &&
// data[i + 2] == 0x00 &&
// data[i + 3] == 0x01) {
// start = i;
// i += 4;
// } else {
// i++;
// continue;
// }
// next = i;
// while (next < data.length - 3) {
// if (data[next] == 0x00 &&
// data[next + 1] == 0x00 &&
// ((data[next + 2] == 0x01) ||
// (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
// break;
// }
// next++;
// }
// nalus.add(data.sublist(start, next));
// i = next;
// } else {
// i++;
// }
// }
// int nalusTotalLen =
// nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
// if (nalus.isEmpty && data.isNotEmpty) {
// nalus.add(data);
// } else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
// nalus.add(data.sublist(nalusTotalLen));
// }
// for (final nalu in nalus) {
// int offset = 0;
// if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
// if (nalu[2] == 0x01)
// offset = 3;
// else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
// }
// if (nalu.length > offset) {
// int naluType = nalu[offset] & 0x1F;
// if (naluType == 5) {
// _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs,
// frameSeq, frameSeqI);
// }
// }
// }
// }
// P帧处理方法
// void _handlePFrame(
// List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
// // P帧type 1
// List<List<int>> nalus = [];
// int i = 0;
// List<int> data = frameData;
// while (i < data.length - 3) {
// int start = -1;
// int next = -1;
// if (data[i] == 0x00 && data[i + 1] == 0x00) {
// if (data[i + 2] == 0x01) {
// start = i;
// i += 3;
// } else if (i + 3 < data.length &&
// data[i + 2] == 0x00 &&
// data[i + 3] == 0x01) {
// start = i;
// i += 4;
// } else {
// i++;
// continue;
// }
// next = i;
// while (next < data.length - 3) {
// if (data[next] == 0x00 &&
// data[next + 1] == 0x00 &&
// ((data[next + 2] == 0x01) ||
// (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
// break;
// }
// next++;
// }
// nalus.add(data.sublist(start, next));
// i = next;
// } else {
// i++;
// }
// }
// int nalusTotalLen =
// nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
// if (nalus.isEmpty && data.isNotEmpty) {
// nalus.add(data);
// } else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
// nalus.add(data.sublist(nalusTotalLen));
// }
// for (final nalu in nalus) {
// int offset = 0;
// if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
// if (nalu[2] == 0x01)
// offset = 3;
// else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
// }
// if (nalu.length > offset) {
// int naluType = nalu[offset] & 0x1F;
// if (naluType == 1) {
// _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs,
// frameSeq, frameSeqI);
// }
// }
// }
// }
//
void onQualityChanged(String quality) async {
@ -1432,6 +973,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
//
switch (contentType) {
case TalkData_ContentTypeE.G711:
if (!state.isOpenVoice.value) {
return;
}
if (state.audioBuffer.length >= audioBufferSize) {
state.audioBuffer.removeAt(0); //
}

View File

@ -558,8 +558,11 @@ class TalkViewLogic extends BaseGetXController {
state.voiceProcessor = VoiceProcessor.instance;
}
Timer? _startProcessingAudioTimer;
//
Future<void> startProcessingAudio() async {
try {
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
@ -577,7 +580,6 @@ class TalkViewLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex';
}
state.isOpenVoice.value = false;
}
///
@ -599,45 +601,51 @@ class TalkViewLogic extends BaseGetXController {
} finally {
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
}
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
}
//
Future<void> _onFrame(List<int> frame) async {
//
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); //
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 40; // 40ms发送一次4chunk
void _sendAudioChunk(Timer timer) async {
if (_bufferedAudioFrames.length < chunkSize) {
//
return;
}
//
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
// 使
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使
int getFrameLength = state.frameLength;
if (Platform.isIOS) {
getFrameLength = state.frameLength * 2;
}
// 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()}...');
//
if (_bufferedAudioFrames.length >= state.frameLength) {
try {
await StartChartManage().sendTalkDataMessage(
talkData: TalkData(
content: _bufferedAudioFrames,
content: chunk,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
} finally {
_bufferedAudioFrames.clear(); //
}
} else {
//
Future<void> _onFrame(List<int> frame) async {
final applyGain = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
//
if (_startProcessingAudioTimer == null && _bufferedAudioFrames.length > chunkSize) {
_startProcessingAudioTimer = Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
}
}

View File

@ -21,6 +21,7 @@ class CommonItem extends StatelessWidget {
this.isTipsImg,
this.action,
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
),

View File

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

View File

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