diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index fc2da7300..89bcfdef7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -5,6 +5,7 @@ repositories { mavenCentral() flatDir { dirs(project(":spotify_sdk").projectDir.resolve("libs")) + dirs(project(":jl_ota").projectDir.resolve("libs")) } } diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 24d8d3c95..5a7b3b7d5 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -123,6 +123,7 @@ const Map zhCN = { "allRoundAssistant": "全能助理", "allRoundAssistantInfo": "帮你解决各类问题", "simultaneousTranslation": "同声翻译", + "faceToFaceTranslation": "面对面翻译", 'audioVideoTranslation': '音视频翻译', 'callTranslation': '通话翻译', "simultaneousTranslationInfo": "实时翻译80+种语言", diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 0f826a7c4..8ce3b77b6 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -54,7 +54,7 @@ class DeviceInfo { this.rightEarCharging = false, this.caseCharging = false, this.firmwareVersion = '', - this.firmwareProductId = '', + this.firmwareProductId = 's3', this.firmwareProductColor = '', DateTime? lastUpdated, }) : lastUpdated = lastUpdated ?? DateTime.now(); diff --git a/lib/data/services/db/sqflite_api.dart b/lib/data/services/db/sqflite_api.dart index cdb2e8d57..bf2d5a8cf 100644 --- a/lib/data/services/db/sqflite_api.dart +++ b/lib/data/services/db/sqflite_api.dart @@ -123,9 +123,12 @@ class SqfliteApi { try { final batch = db.batch(); for (var item in data) { + Map values = item['content'] != null + ? {'content': item['content']} + : {'speaker': item['speaker']}; batch.update( 'meetingspeaker', - {'content': item['content']}, + values, where: 'id = ?', whereArgs: [item['id']], ); diff --git a/lib/data/services/deapsound_ai_service.dart b/lib/data/services/deapsound_ai_service.dart index 186116ab4..022f2aa2e 100644 --- a/lib/data/services/deapsound_ai_service.dart +++ b/lib/data/services/deapsound_ai_service.dart @@ -1,17 +1,20 @@ import 'dart:convert'; -import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart' as http; import '../../core/utils/logger.dart'; +import '../models/appconfig.dart'; import 'ai_service.dart'; class DeapsoundAIService implements AiService { + late final String baseUrl; late final String apiKey; + late final String model; DeapsoundAIService() { - final GetStorage storage = GetStorage(); - apiKey = storage.read('logintoken'); + apiKey = AppConfig.env('OPENAI_API_KEY') ?? ''; + baseUrl = AppConfig.env('OPENAI_BASE_URL') ?? ''; + model = AppConfig.env('OPENAI_MODEL') ?? ''; } @override @@ -20,7 +23,34 @@ class DeapsoundAIService implements AiService { String? systemPrompt, String? userProperties, }) async { - return ''; + final url = Uri.parse(baseUrl); + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $apiKey', + }; + + final requestBody = { + "model": model, + "messages": messages, + }; + + try { + final response = await http.post( + url, + headers: headers, + body: jsonEncode(requestBody), + ); + if (response.statusCode != 200) { + throw Exception('Unknown error'); + } + + final result = jsonDecode(response.body); + + return result['choices'].first['message']['content']; + } catch (e) { + throw Exception('Unknown error'); + } } @override diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index e67639842..1bb13c1c8 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -4,7 +4,6 @@ import 'nw_method.dart'; class Api { // 登录 static login([params]) { - print("请求后台数据login params=${params}"); return DioManager().request( NWMethod.post, '/api/home/user_sgin', diff --git a/lib/data/services/network/dio_manager.dart b/lib/data/services/network/dio_manager.dart index ff1306b5c..7833ed582 100644 --- a/lib/data/services/network/dio_manager.dart +++ b/lib/data/services/network/dio_manager.dart @@ -20,7 +20,7 @@ class DioManager { ); dio = Dio(options); -//fdp + dio.interceptors.add(AuthInterceptor()); } diff --git a/lib/data/services/network/nw_api.dart b/lib/data/services/network/nw_api.dart index 293c014b9..8feff1db5 100644 --- a/lib/data/services/network/nw_api.dart +++ b/lib/data/services/network/nw_api.dart @@ -1,4 +1,5 @@ class NWApi { // static const baseApi = "https://www.deapsound.com"; static const baseApi = "https://api.deapsound.com"; + // static const baseApi = "https://api.ideapsound.com"; } diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index a90eefba7..637b5199a 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -275,6 +275,12 @@ class MessageBubble extends GetView { now, daylist, ); + } else if (mate!.keys.contains('card_travel')) { + final rawList = mate!['card_travel']['items'] as List; + final travellist = + rawList.map((e) => Map.from(e)).toList(); + // 旅游 + return _buildTravelContent(context, travellist); } // if (message.contains('播放列表') || message.contains('音乐')) { @@ -472,12 +478,13 @@ class MessageBubble extends GetView { } /// 构建旅游内容卡片 - Widget _buildTravelContent(BuildContext context) { + Widget _buildTravelContent( + BuildContext context, List> travellist) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - '以下是一些推荐的景点:', + const Text( + '以下是一些推搜索结果:', style: TextStyle( color: Colors.black87, fontSize: 14, @@ -503,32 +510,54 @@ class MessageBubble extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Icon( - Icons.place, - size: 16, - color: Color(0xFFE11D48), - ), - const SizedBox(width: 6), - Text( - '热门景点 · 东京', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Colors.grey[700], - ), - ), - ], - ), - const SizedBox(height: 10), + // Row( + // children: [ + // Icon( + // Icons.place, + // size: 16, + // color: Color(0xFFE11D48), + // ), + // const SizedBox(width: 6), + // Text( + // '热门景点 · 东京', + // style: TextStyle( + // fontSize: 13, + // fontWeight: FontWeight.w600, + // color: Colors.grey[700], + // ), + // ), + // ], + // ), + // const SizedBox(height: 10), - // 景点列表 - _buildLocationItem('东京塔', '标志性建筑,可俯瞰东京全景', context), - Divider(height: 1, thickness: 1, color: Colors.grey[100]), - _buildLocationItem('涩谷十字路口', '世界上最繁忙的十字路口之一', context), - Divider(height: 1, thickness: 1, color: Colors.grey[100]), - _buildLocationItem('浅草寺', '东京最古老的寺庙', context), + // 动态生成音乐条目 + ...travellist.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + final name = item["name"] ?? "未知歌曲"; //名字 + final addr = item["addr"] ?? ""; //地址 + final tag = item["tag"] ?? "未知歌手"; //标签 + final rating = item["rating"] ?? ""; //评分 + final distance = item["distance"] ?? ""; //距离 + final images = item["images"] ?? []; // 图片列表 + return Column( + children: [ + _buildLocationItem( + context, + name, + addr, + ), + if (index != travellist.length - 1) + Divider(height: 1, thickness: 1, color: Colors.grey[100]), + ], + ); + }).toList(), + // // 景点列表 + // _buildLocationItem('东京塔', '标志性建筑,可俯瞰东京全景', context), + // Divider(height: 1, thickness: 1, color: Colors.grey[100]), + // _buildLocationItem('涩谷十字路口', '世界上最繁忙的十字路口之一', context), + // Divider(height: 1, thickness: 1, color: Colors.grey[100]), + // _buildLocationItem('浅草寺', '东京最古老的寺庙', context), const SizedBox(height: 8), Center( @@ -712,7 +741,7 @@ class MessageBubble extends GetView { /// 构建地点项目 Widget _buildLocationItem( - String name, String description, BuildContext context) { + BuildContext context, String name, String description) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Padding( diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index fa2baa534..4eb9c327c 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -695,8 +695,8 @@ class LoginController extends GetxController { await _storage.write("logintoken", response['token'].toString()); // 存储到 SharedPreferences (仅存储用户数据) //final prefs = await GetStorage(); - await _storage.write( - 'user', jsonEncode(user.toJson())); // 使用 user.toJson() + await _storage.write('user_info', user.toJson()); + //获取app的配置信息 await getAppConfig(); Get.offAllNamed(Routes.pairing); diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index b906902ed..f60bfd8d6 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -7,14 +7,12 @@ import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart' as http; -import 'package:uuid/uuid.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/upload_oss.dart'; -import '../../../data/models/appconfig_model.dart'; -import '../../../data/services/ai_service.dart'; +import '../../../data/models/appconfig.dart'; import '../../../data/services/db/sqflite_api.dart'; -import '../../../data/services/open_ai_service_adapter.dart'; +import '../../../data/services/deapsound_ai_service.dart'; import '../model/meeting_details_model.dart'; import '../model/meeting_model.dart'; @@ -40,11 +38,8 @@ class MeetingDetailsController extends GetxController RxInt editSpeakerTextId = 0.obs; String editSpeakerTextChange = ''; - final AiService _aiService = Get.find(); - // Agent 信息 - late final DBAgent _agent; + String _systemPrompt = ''; String _userId = ''; - StreamSubscription? _aiResponseSubscription; bool _isDisposed = false; @@ -78,7 +73,6 @@ class MeetingDetailsController extends GetxController scrollController.dispose(); tabController.dispose(); playerController.dispose(); - _aiResponseSubscription?.cancel(); _isDisposed = true; super.onClose(); } @@ -110,10 +104,9 @@ class MeetingDetailsController extends GetxController void _getAgent() async { final GetStorage storage = GetStorage(); - // final List agents = storage.read("AGENTS"); Map userInfo = storage.read("user_info"); _userId = userInfo['user']['uid']; - // _agent = agents.firstWhere((agent) => agent.id == 'meet_assistant'); + _systemPrompt = AppConfig.agentsystemPrompt("personal_assistant") ?? ''; } void _getDataDetails() async { @@ -173,8 +166,6 @@ class MeetingDetailsController extends GetxController } } - void editSpeakerName() {} - void speakerCreate() async { EasyLoading.show(); await _audioSpeaker(); @@ -196,28 +187,20 @@ class MeetingDetailsController extends GetxController } allText = text; } - _aiServiceListen(); - // const uuid = Uuid(); - // _aiService.send( - // sid: uuid.v4(), - // messages: [ - // {'role': 'user', 'content': allText}, - // ], - // systemPrompt: _agent.systemPrompt, - // ); + await _aiServiceSummary(); + EasyLoading.dismiss(); } - void _aiServiceListen() { - String summaryText = ''; - // _aiResponseSubscription = _aiService.start().listen((AIStreamEvent event) { - // summaryText += event.content ?? ''; - // if (event.type == StreamEventType.complete) { - // SqfliteApi.editMeeting(id, {'summary': summaryText}); - // meetingDetails.value.summary = summaryText; - // meetingDetails.refresh(); - // EasyLoading.dismiss(); - // } - // }); + Future _aiServiceSummary() async { + String summaryText = await DeapsoundAIService().sendMessage( + messages: [ + {'role': 'system', 'content': _systemPrompt}, + {'role': 'user', 'content': allText}, + ], + ); + SqfliteApi.editMeeting(id, {'summary': summaryText}); + meetingDetails.value.summary = summaryText; + meetingDetails.refresh(); } Future _audioSpeaker() async { @@ -345,12 +328,14 @@ class MeetingDetailsController extends GetxController Future _addTextList(List utterances) async { String text = ''; + String personnel = ''; List> list = []; for (var element in utterances) { String speaker = ''; if (element['additions'].isNotEmpty) { speaker = 'Speaker ${element['additions']['speaker']}'; text += '$speaker:'; + personnel += '[$speaker] '; } list.add({ 'meetingid': id, @@ -363,10 +348,11 @@ class MeetingDetailsController extends GetxController } var result = await SqfliteApi.insertMeetingSpeaker(list); if (result != null) { - await SqfliteApi.editMeeting(id, {'tasktype': 1}); + await SqfliteApi.editMeeting(id, {'tasktype': 1, 'personnel': personnel}); List speakerList = await SqfliteApi.getMeetingSpeaker(id); textList.value = List.from(speakerList); meetingDetails.value.tasktype = 1; + meetingDetails.value.personnel = personnel; meetingDetails.refresh(); } allText = text; diff --git a/lib/modules/meeting/views/speaker_all_bottom_sheet.dart b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart new file mode 100644 index 000000000..48b331c8f --- /dev/null +++ b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart @@ -0,0 +1,497 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import '../../../data/services/db/sqflite_api.dart'; +import '../controllers/meeting_details_controller.dart'; + +class SpeakerAllBottomSheet extends StatefulWidget { + const SpeakerAllBottomSheet({super.key}); + + @override + State createState() => _SpeakerAllBottomSheetState(); +} + +class _SpeakerAllBottomSheetState extends State { + final _controller = Get.find(); + final GetStorage _storage = GetStorage(); + + List _dataList = []; + + List _historySpeakerList = []; + List _speakerList = []; + + @override + void initState() { + super.initState(); + _getSpeakerList(); + } + + void _getSpeakerList() { + _historySpeakerList = _storage.read("speaker_list") ?? []; + _speakerList = List.from(_historySpeakerList); + List textList = _controller.textList; + List dataList = []; + for (var item in textList) { + String speaker = item['speaker']; + Map? foundElement = dataList.firstWhere( + (element) => element['speaker'] == speaker, + orElse: () => null, + ); + if (foundElement != null) { + int time = item['endtime'] - item['starttime']; + foundElement['totaltime'] = foundElement['totaltime'] + time; + if (!foundElement['isOmit']) { + if (foundElement['list'].length < 2) { + foundElement['list'].add(Map.from(item)); + } else { + foundElement['isOmit'] = true; + } + } + } else { + dataList.add({ + 'speaker': speaker, + 'speakerText': TextEditingController(), + 'isShow': false, + 'totaltime': item['endtime'] - item['starttime'], + 'isOmit': false, + 'list': [Map.from(item)], + }); + } + if (speaker.isNotEmpty && + !RegExp(r'^Speaker \d+$').hasMatch(speaker) && + !_speakerList.contains(speaker)) { + _speakerList.add(speaker); + } + } + setState(() { + _dataList = dataList; + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + left: 12.w, + right: 12.w, + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '命名发言者', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 26.w, + height: 26.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(26.r), + ), + child: Icon( + Icons.close, + size: 18.w, + color: Colors.grey[400], + ), + ), + ) + ], + ), + ), + Divider( + height: 1, + color: Colors.grey[300], + ), + Container( + constraints: BoxConstraints( + maxHeight: + 1.sh - 240.w - MediaQuery.of(context).viewInsets.bottom, + ), + child: ListView.builder( + itemCount: _dataList.length, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + Map item = _dataList[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only(top: 15.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: item['isShow'] && _speakerList.isNotEmpty + ? BorderRadius.vertical( + top: Radius.circular(12.r), + ) + : BorderRadius.circular(12.r), + ), + child: Focus( + onFocusChange: (bool hasFocus) { + item['isShow'] = hasFocus; + }, + child: TextField( + controller: item['speakerText'], + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + letterSpacing: 0, + height: 1.4, + ), + decoration: InputDecoration( + hintText: item['speaker'], + hintStyle: TextStyle( + fontSize: 16.sp, + color: Colors.grey[400], + letterSpacing: 0, + height: 1.4, + ), + isDense: true, + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 12.w, + ), + ), + ), + ), + ), + if (item['isShow'] && _speakerList.isNotEmpty) + Container( + padding: EdgeInsets.only(left: 10.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(12.r), + ), + border: Border( + top: BorderSide( + width: 1, + color: Colors.grey[300]!, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 10.w, bottom: 5.w), + child: Text( + '最近使用过的名称', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600]!, + ), + ), + ), + Container( + constraints: BoxConstraints( + maxHeight: 150.h, + ), + child: ListView.builder( + itemCount: _speakerList.length, + shrinkWrap: true, + itemBuilder: (BuildContext context, int idx) { + return GestureDetector( + onTap: () { + item['speakerText'].text = + _speakerList[idx]; + FocusScope.of(context).unfocus(); + }, + behavior: HitTestBehavior.translucent, + child: SizedBox( + height: 42.w, + child: Align( + alignment: Alignment.centerLeft, + child: Text( + _speakerList[idx], + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 8.w, + ), + child: Row( + children: [ + Text( + '总计', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey, + ), + ), + 5.horizontalSpace, + Text( + '${item['totaltime'] / 1000}s', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black87, + ), + ), + ], + ), + ), + ...List.generate( + item['list'].length, + (int idx) { + Map data = item['list'][idx]; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.play_circle, + color: Colors.red[400], + size: 15.w, + ), + 2.horizontalSpace, + Text( + DateUtil.formatDateMs( + data['starttime'], + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + Padding( + padding: + EdgeInsets.symmetric(horizontal: 2.w), + child: Text( + '-', + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + ), + Text( + DateUtil.formatDateMs( + data['endtime'], + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + 5.horizontalSpace, + Text( + '${(data['endtime'] - data['starttime']) / 1000}s', + style: TextStyle( + color: Colors.black54, + fontSize: 12.sp, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8.w), + child: Text( + data['content'], + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: TextStyle( + color: Colors.grey[700], + fontSize: 12.sp, + ), + ), + ), + ], + ), + ); + }, + ), + if (item['isOmit']) + Padding( + padding: + EdgeInsets.only(left: 10.w, top: 5.w, bottom: 8.w), + child: Row( + children: List.generate( + 6, + (int idx) => Container( + width: 3.w, + height: 3.w, + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: Colors.grey[700], + borderRadius: BorderRadius.circular(3.r), + ), + ), + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 7.w), + child: _separator(), + ), + ], + ); + }, + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Text( + '总结的发言者将自动更新', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + ), + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 20.w), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '取消', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + ), + ), + ), + ), + ), + 10.horizontalSpace, + Expanded( + child: GestureDetector( + onTap: _save, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '确认', + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _separator() { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final boxWidth = constraints.constrainWidth(); + const dashWidth = 2.0; + const dashHeight = 1.0; + final dashCount = (boxWidth / (2 * dashWidth)).floor(); + return Flex( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + direction: Axis.horizontal, + children: List.generate(dashCount, (_) { + return SizedBox( + width: dashWidth, + height: dashHeight, + child: DecoratedBox( + decoration: BoxDecoration(color: Colors.grey[300]), + ), + ); + }), + ); + }, + ); + } + + void _save() async { + EasyLoading.show(); + List textList = _controller.textList; + List newTextList = []; + List editSpeakerList = []; + String personnel = ''; + for (var element in textList) { + final dataMap = Map.from(element); + for (var data in _dataList) { + String speaker = data['speakerText'].text; + if (speaker.isNotEmpty && speaker != data['speaker']) { + if (dataMap['speaker'] == data['speaker']) { + dataMap['speaker'] = speaker; + editSpeakerList.add({ + 'id': dataMap['id'], + 'speaker': dataMap['speaker'], + }); + } + if (!RegExp(r'^Speaker \d+$').hasMatch(speaker) && + !_historySpeakerList.contains(speaker)) { + _historySpeakerList.insert(0, speaker); + } + } + } + if (!personnel.contains(dataMap['speaker'])) { + personnel += '[${dataMap['speaker']}] '; + } + newTextList.add(dataMap); + } + var result = await SqfliteApi.editMeetingSpeaker(editSpeakerList); + if (result != null) { + _controller.textList.value = newTextList; + SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); + _controller.meetingDetails.value.personnel = personnel; + _storage.write("speaker_list", _historySpeakerList); + Get.back(); + } + EasyLoading.dismiss(); + } +} diff --git a/lib/modules/meeting/views/speaker_bottom_sheet.dart b/lib/modules/meeting/views/speaker_bottom_sheet.dart index 2d0bcf665..3bc629643 100644 --- a/lib/modules/meeting/views/speaker_bottom_sheet.dart +++ b/lib/modules/meeting/views/speaker_bottom_sheet.dart @@ -1,14 +1,68 @@ import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; -class SpeakerBottomSheet extends StatelessWidget { - const SpeakerBottomSheet({super.key}); +import '../../../data/services/db/sqflite_api.dart'; +import '../controllers/meeting_details_controller.dart'; + +class SpeakerBottomSheet extends StatefulWidget { + final int id; + final String speakerName; + + const SpeakerBottomSheet(this.id, this.speakerName, {super.key}); + + @override + State createState() => _SpeakerBottomSheetState(); +} + +class _SpeakerBottomSheetState extends State { + final _controller = Get.find(); + final GetStorage _storage = GetStorage(); + + final TextEditingController _speakerController = TextEditingController(); + int _type = 1; + List _historySpeakerList = []; + List _speakerList = []; + + @override + void initState() { + super.initState(); + _getSpeakerList(); + } + + void _getSpeakerList() { + _historySpeakerList = _storage.read("speaker_list") ?? []; + List speakerList = List.from(_historySpeakerList); + List textList = _controller.textList; + for (var element in textList) { + String speaker = element['speaker']; + if (speaker.isNotEmpty && + !RegExp(r'^Speaker \d+$').hasMatch(speaker) && + !speakerList.contains(speaker)) { + speakerList.add(speaker); + } + } + setState(() { + _speakerList = speakerList; + }); + } + + @override + void dispose() { + _speakerController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), + padding: EdgeInsets.only( + left: 12.w, + right: 12.w, + bottom: MediaQuery.of(context).viewInsets.bottom, + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -49,17 +103,45 @@ class SpeakerBottomSheet extends StatelessWidget { color: Colors.grey[300], ), Container( - height: 46.w, margin: EdgeInsets.symmetric(vertical: 15.w), + alignment: Alignment.center, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), ), + child: TextField( + controller: _speakerController, + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + letterSpacing: 0, + height: 1.4, + ), + decoration: InputDecoration( + hintText: widget.speakerName, + hintStyle: TextStyle( + fontSize: 16.sp, + color: Colors.grey[400], + letterSpacing: 0, + height: 1.4, + ), + isDense: true, + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 12.w, + ), + ), + ), ), Container( - constraints: BoxConstraints(maxHeight: 1.sh - 390.w), + constraints: BoxConstraints( + maxHeight: + 1.sh - 390.w - MediaQuery.of(context).viewInsets.bottom, + ), child: ListView.builder( - itemCount: 30, + itemCount: _speakerList.length, shrinkWrap: true, itemBuilder: (BuildContext context, int index) { return Container( @@ -83,15 +165,21 @@ class SpeakerBottomSheet extends StatelessWidget { color: Colors.grey[600]!, ), ), - SizedBox( - height: 42.w, - child: Align( - alignment: Alignment.centerLeft, - child: Text( - '龙哥', - style: TextStyle( - fontSize: 14.sp, - color: Colors.black87, + GestureDetector( + onTap: () { + _speakerController.text = _speakerList[index]; + }, + behavior: HitTestBehavior.translucent, + child: SizedBox( + height: 42.w, + child: Align( + alignment: Alignment.centerLeft, + child: Text( + _speakerList[index], + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), ), ), ), @@ -102,12 +190,25 @@ class SpeakerBottomSheet extends StatelessWidget { }, ), ), - _row('应用到当前段落', false), - _row('应用到这个发言者的所有段落', true, description: '总结将根据模版自动更新'), + _row('应用到当前段落', _type == 0, onTap: () { + setState(() { + _type = 0; + }); + }), + _row( + '应用到这个发言者的所有段落', + _type == 1, + description: '总结将根据模版自动更新', + onTap: () { + setState(() { + _type = 1; + }); + }, + ), Padding( padding: EdgeInsets.only(top: 10.w, bottom: 20.w), child: GestureDetector( - onTap: () {}, + onTap: _save, child: Container( height: 48.w, alignment: Alignment.center, @@ -130,35 +231,39 @@ class SpeakerBottomSheet extends StatelessWidget { ); } - Widget _row(String name, bool isActive, {String? description}) { - return Padding( - padding: EdgeInsets.symmetric(vertical: 10.w), - child: Row( - children: [ - isActive ? _round() : _circle(), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: TextStyle( - fontSize: 14.sp, - color: Colors.black87, - ), - ), - if (description != null) + Widget _row(String name, bool isActive, + {String? description, Function()? onTap}) { + return GestureDetector( + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + children: [ + isActive ? _round() : _circle(), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - description, + name, style: TextStyle( - fontSize: 10.sp, - color: Colors.grey[500]!, + fontSize: 14.sp, + color: Colors.black87, ), ), - ], + if (description != null) + Text( + description, + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey[500]!, + ), + ), + ], + ), ), - ), - ], + ], + ), ), ); } @@ -199,4 +304,82 @@ class SpeakerBottomSheet extends StatelessWidget { ), ); } + + void _save() { + if (_speakerController.text.isNotEmpty && + _speakerController.text != widget.speakerName) { + if (_type == 0) { + _single(); + } else { + _all(); + } + } else { + Get.back(); + } + } + + void _single() async { + EasyLoading.show(); + List list = [ + { + 'id': widget.id, + 'speaker': _speakerController.text, + } + ]; + var result = await SqfliteApi.editMeetingSpeaker(list); + if (result != null) { + int index = + _controller.textList.indexWhere((item) => item['id'] == widget.id); + if (index != -1) { + final dataMap = Map.from(_controller.textList[index]); + dataMap['speaker'] = _speakerController.text; + _controller.textList[index] = dataMap; + } + _addHistorySpeaker(); + Get.back(); + } + EasyLoading.dismiss(); + } + + void _all() async { + EasyLoading.show(); + List textList = _controller.textList; + List newTextList = []; + List editSpeakerList = []; + String personnel = ''; + for (var element in textList) { + final dataMap = Map.from(element); + if (dataMap['speaker'] == widget.speakerName) { + dataMap['speaker'] = _speakerController.text; + editSpeakerList.add({ + 'id': dataMap['id'], + 'speaker': dataMap['speaker'], + }); + } + String speaker = dataMap['speaker']; + if (!personnel.contains(speaker)) { + personnel += '[$speaker] '; + } + newTextList.add(dataMap); + } + var result = await SqfliteApi.editMeetingSpeaker(editSpeakerList); + if (result != null) { + _controller.textList.value = newTextList; + SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); + _controller.meetingDetails.value.personnel = personnel; + _addHistorySpeaker(); + Get.back(); + } + EasyLoading.dismiss(); + } + + void _addHistorySpeaker() { + String speaker = _speakerController.text; + if (speaker.isNotEmpty && + !RegExp(r'^Speaker \d+$').hasMatch(speaker) && + !_historySpeakerList.contains(speaker)) { + _historySpeakerList.insert(0, speaker); + _storage.write("speaker_list", _historySpeakerList); + } + } } diff --git a/lib/modules/meeting/views/speech_tab.dart b/lib/modules/meeting/views/speech_tab.dart index d0c58e85c..f7a4bb0f3 100644 --- a/lib/modules/meeting/views/speech_tab.dart +++ b/lib/modules/meeting/views/speech_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/meeting_details_controller.dart'; +import 'speaker_all_bottom_sheet.dart'; import 'speaker_bottom_sheet.dart'; class SpeechTab extends GetView { @@ -59,7 +60,8 @@ class SpeechTab extends GetView { ), ), builder: (BuildContext context) { - return const SpeakerBottomSheet(); + return SpeakerBottomSheet( + item['id'], item['speaker']); }, ); }, @@ -199,7 +201,7 @@ class SpeechTab extends GetView { ), ), builder: (BuildContext context) { - return const SpeakerBottomSheet(); + return const SpeakerAllBottomSheet(); }, ); }, diff --git a/lib/modules/meeting/views/summary_tab.dart b/lib/modules/meeting/views/summary_tab.dart index 073df9df2..d663ee748 100644 --- a/lib/modules/meeting/views/summary_tab.dart +++ b/lib/modules/meeting/views/summary_tab.dart @@ -5,7 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/meeting_details_controller.dart'; -import 'speaker_bottom_sheet.dart'; +import 'speaker_all_bottom_sheet.dart'; class SummaryTab extends GetView { const SummaryTab({super.key}); @@ -126,7 +126,7 @@ class SummaryTab extends GetView { ), ), Text( - '与会人员:${controller.meetingDetails.value.address}', + '与会人员:${controller.meetingDetails.value.personnel.isNotEmpty ? controller.meetingDetails.value.personnel : '[输入与会人员]'}', style: TextStyle( color: isDarkMode ? Colors.grey[400] : Colors.grey[700], @@ -169,7 +169,7 @@ class SummaryTab extends GetView { ), ), builder: (BuildContext context) { - return const SpeakerBottomSheet(); + return const SpeakerAllBottomSheet(); }, ); }, diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index 1e59b45c3..4aa7f04f6 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -7,6 +7,7 @@ import '../model/ota_upgrade_model.dart'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:jl_ota/jl_ota.dart'; import '../../../data/services/ble_manager.dart'; class OtaUpgradeController extends GetxController { @@ -40,6 +41,8 @@ class OtaUpgradeController extends GetxController { Future startUpdate() async { try { isUpdating.value = true; + checkUpdate(); + // 实现更新逻辑... } catch (e) { errorMessage.value = e.toString(); @@ -50,64 +53,68 @@ class OtaUpgradeController extends GetxController { } Future _getCurrentVersion() async { - try { - // 添加获取设备信息的重试机制 - DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; - - // 第一次检查设备信息 - if (deviceInfo == null) { - // 显示加载提示 - isFetchingDeviceInfo.value = true; - - // 尝试重新获取设备信息 - await _bleManager.fetchDeviceInfo(); - - // 等待后再次获取设备信息 - deviceInfo = _bleManager.deviceInfo.value; - isFetchingDeviceInfo.value = false; - - // 仍然为空则抛出异常 - if (deviceInfo == null) { - throw const FormatException('无法获取设备信息,请检查设备连接'); - } - } - - // 检查固件信息有效性 - if (deviceInfo.firmwareVersion.isEmpty || - deviceInfo.firmwareProductId.isEmpty) { - throw const FormatException('设备固件信息不完整'); - } - - // 正常流程 - currentVersion.value = deviceInfo.firmwareVersion; - - // API请求 - final response = await Api.getequipmentinfo({ - "id": deviceInfo.firmwareProductId, - }); - - // 处理API响应 - if (response['info'] == null) { - throw const FormatException('API 响应格式错误'); - } - - final resp = OtaConfigResp.fromJson(response); - latestVersion.value = resp.systemversion!; - updatepackageaddress = resp.updatepackageaddress!; - - // 版本检查 - isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!); - } catch (e) { - // 统一错误处理 - errorMessage.value = e.toString(); - Get.snackbar('错误', e.toString(), - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white); - // 重要:重新抛出异常以便上层处理(可选) - } finally { - isFetchingDeviceInfo.value = false; - } + JlOta().getPlatformVersion(); + //try { + + // // 添加获取设备信息的重试机制 + // DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; + + // // 第一次检查设备信息 + // if (deviceInfo == null) { + // // 显示加载提示 + // isFetchingDeviceInfo.value = true; + + // // 尝试重新获取设备信息 + // await _bleManager.fetchDeviceInfo(); + + // // 等待后再次获取设备信息 + // deviceInfo = _bleManager.deviceInfo.value; + // isFetchingDeviceInfo.value = false; + + // // 仍然为空则抛出异常 + // if (deviceInfo == null) { + // throw const FormatException('无法获取设备信息,请检查设备连接'); + // } + // } + + // // 检查固件信息有效性 + // if (deviceInfo.firmwareVersion.isEmpty + // // || + // // deviceInfo.firmwareProductId.isEmpty + // ) { + // throw const FormatException('设备固件信息不完整'); + // } + + // // 正常流程 + // currentVersion.value = "0"; //deviceInfo.firmwareVersion; + + // // API请求 + // final response = await Api.getequipmentinfo({ + // "id": "s3", + // }); + + // // 处理API响应 + // if (response['info'] == null) { + // throw const FormatException('API 响应格式错误'); + // } + + // final resp = OtaConfigResp.fromJson(response); + // latestVersion.value = resp.systemversion!; + // updatepackageaddress = resp.updatepackageaddress!; + + // // 版本检查 + // isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!); + // } catch (e) { + // // 统一错误处理 + // errorMessage.value = e.toString(); + // Get.snackbar('错误', e.toString(), + // snackPosition: SnackPosition.BOTTOM, + // backgroundColor: Colors.red, + // colorText: Colors.white); + // // 重要:重新抛出异常以便上层处理(可选) + // } finally { + // isFetchingDeviceInfo.value = false; + // } } // 版本比较方法优化 @@ -288,6 +295,7 @@ class OtaUpgradeController extends GetxController { // 开始升级 void _installApp(String path) async { + // JlOtaPlugin() // final File file = File(path); // if (await file.exists()) { // // OpenResult result = await OpenFile.open(path); diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 7962d629f..1b2c8afd3 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -59,6 +59,9 @@ class TranslationController extends GetxController { return 'audioVideoTranslation'; case 'call': return 'callTranslation'; + case 'faceToFace': + return 'faceToFaceTranslation'; + default: return 'simultaneousTranslation'; } diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 5908b0691..257e32a63 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -57,6 +57,7 @@ class TranslationView extends GetView { color: isDarkMode ? AppColors.cardBackground : Colors.white, itemBuilder: (BuildContext context) => [ _buildPopupMenuItem('simultaneous', 'simultaneousTranslation'.tr), + _buildPopupMenuItem('faceToFace', 'faceToFaceTranslation'.tr), _buildPopupMenuItem('audioVideo', 'audioVideoTranslation'.tr), _buildPopupMenuItem('call', 'callTranslation'.tr), ], @@ -86,12 +87,11 @@ class TranslationView extends GetView { // TTS toggle button Obx(() => IconButton( icon: Icon( - controller.isTtsEnabled.value - ? Icons.volume_up - : Icons.volume_off, + controller.isTtsEnabled.value ? Icons.mic : Icons.mic_off, color: isDarkMode ? Colors.white : Colors.black, size: 24.sp, ), + // onPressed: controller.toggleTts,默认播放 onPressed: controller.toggleTts, tooltip: '自动播放翻译结果', )), diff --git a/local_plugins/jieli/android/build.gradle.kts b/local_plugins/jieli/android/build.gradle.kts index e804da665..a8a6e7ac4 100644 --- a/local_plugins/jieli/android/build.gradle.kts +++ b/local_plugins/jieli/android/build.gradle.kts @@ -54,5 +54,4 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") implementation(project(":azure_speech")) implementation(project(":jl_opus")) - implementation(project(":jl_opus")) } \ No newline at end of file diff --git a/local_plugins/jl_ota/.gitignore b/local_plugins/jl_ota/.gitignore new file mode 100644 index 000000000..e7d347d9d --- /dev/null +++ b/local_plugins/jl_ota/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/local_plugins/jl_ota/.metadata b/local_plugins/jl_ota/.metadata new file mode 100644 index 000000000..6015b831a --- /dev/null +++ b/local_plugins/jl_ota/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: android + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: ios + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/local_plugins/jl_ota/CHANGELOG.md b/local_plugins/jl_ota/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/local_plugins/jl_ota/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/local_plugins/jl_ota/LICENSE b/local_plugins/jl_ota/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/local_plugins/jl_ota/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/local_plugins/jl_ota/README.md b/local_plugins/jl_ota/README.md new file mode 100644 index 000000000..673c7a29a --- /dev/null +++ b/local_plugins/jl_ota/README.md @@ -0,0 +1,15 @@ +# jl_ota + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/to/develop-plugins), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/local_plugins/jl_ota/analysis_options.yaml b/local_plugins/jl_ota/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/local_plugins/jl_ota/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/local_plugins/jl_ota/android/.gitignore b/local_plugins/jl_ota/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/local_plugins/jl_ota/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/local_plugins/jl_ota/android/build.gradle b/local_plugins/jl_ota/android/build.gradle new file mode 100644 index 000000000..76e296450 --- /dev/null +++ b/local_plugins/jl_ota/android/build.gradle @@ -0,0 +1,71 @@ +group = "com.yunqiinnovation.jl_ota" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "1.8.22" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.7.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + flatDir { + dirs 'libs' + } + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "com.yunqiinnovation.jl_ota" + + compileSdk = 35 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 21 + } + + dependencies { + implementation(name: 'jl_bt_ota_V1.9.0-release', ext: 'aar') + implementation(project(":ble_service")) + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.0-release.aar b/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.0-release.aar new file mode 100644 index 000000000..c33c59d48 Binary files /dev/null and b/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.0-release.aar differ diff --git a/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.2-release.aar b/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.2-release.aar new file mode 100644 index 000000000..9ecf85489 Binary files /dev/null and b/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.2-release.aar differ diff --git a/local_plugins/jl_ota/android/settings.gradle b/local_plugins/jl_ota/android/settings.gradle new file mode 100644 index 000000000..d5825d78e --- /dev/null +++ b/local_plugins/jl_ota/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'jl_ota' diff --git a/local_plugins/jl_ota/android/src/main/AndroidManifest.xml b/local_plugins/jl_ota/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..efb57f32c --- /dev/null +++ b/local_plugins/jl_ota/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt b/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt new file mode 100644 index 000000000..c61dfc70d --- /dev/null +++ b/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt @@ -0,0 +1,279 @@ +package com.yunqiinnovation.jl_ota + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothDevice +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.annotation.NonNull + +import com.jieli.jl_bt_ota.constant.ErrorCode; +import com.jieli.jl_bt_ota.constant.StateCode; +import com.jieli.jl_bt_ota.impl.BluetoothOTAManager; +import com.jieli.jl_bt_ota.interfaces.BtEventCallback; +import com.jieli.jl_bt_ota.interfaces.IActionCallback; +import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback; +import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure; +import com.jieli.jl_bt_ota.model.base.BaseError; +import com.jieli.jl_bt_ota.model.response.TargetInfoResponse; + + +import com.yunqiinnovation.ble_service.BleService +/** JlOtaPlugin */ +class JlOtaPlugin(): FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + private lateinit var context: Context + private lateinit var eventChannel: EventChannel + private var streamSink: EventChannel.EventSink? = null + /// BLE服务实例 + private var otaManager: OTAManager? = null + + + companion object { + private const val TAG = "JlOtaPlugin" + private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" + private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" + + } + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "jl_ota") + channel.setMethodCallHandler(this) + + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) + + eventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + streamSink = events + } + + override fun onCancel(arguments: Any?) { + streamSink = null + } + }) + + + otaManager = OTAManager(context) + +// otaManager.getBluetoothOption().setFirmwareFilePath(filePath); +// otaManager.startOTA(new IUpgradeCallback() { +// } + + } + + override fun onMethodCall(call: MethodCall, result: Result) { + if (call.method == "getPlatformVersion") { + + val bluetoothOption = BluetoothOTAConfigure() + //选择通讯方式 + bluetoothOption.priority = BluetoothOTAConfigure.PREFER_BLE + //是否需要自定义回连方式(默认不需要,如需要自定义回连方式,需要客户自行实现) + bluetoothOption.isUseReconnect = true + //是否启用设备认证流程(与固件工程师确认) + bluetoothOption.isUseAuthDevice = true + //设置BLE的MTU + bluetoothOption.mtu = 500 + //是否需要改变BLE的MTU + bluetoothOption.isNeedChangeMtu = false + //是否启用杰理服务器(暂时不支持) + bluetoothOption.isUseJLServer = false + Log.d(TAG, "bluetoothOTAManager初始化成功") + //配置OTA参数 + otaManager?.configure(bluetoothOption) + + + var filePath= "/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files/downloads/ota_upgrades/1.ufw" +otaManager?.bluetoothOption?.firmwareFilePath = filePath +Log.d(TAG, "OpusManager路径成功${filePath}") +// Add ?. after bluetoothOption ^ + otaManager?.startOTA( + CustomUpdateCallback( + context, null, this + ) + ) + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + +} +class OTAManager(context: Context) : BluetoothOTAManager(context) { + override fun getConnectedDevice(): BluetoothDevice? { + // 这里可以添加空实现的注释说明 + return null +} + override fun getConnectedBluetoothGatt(): BluetoothGatt? { + // 这里可以添加空实现的注释说明 + return null +} + override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { + // 这里可以添加空实现的注释说明 + +} +override fun disconnectBluetoothDevice +(bluetoothDevice: BluetoothDevice?) { + // 这里可以添加空实现的注释说明 + +} + /** 数据发送方法(协议无关) */ + override fun sendDataToDevice( + bluetoothDevice: BluetoothDevice?, + bytes: ByteArray? + ): Boolean {return true} +} + +private class CustomUpdateCallback( + val context: Context, + val device: BluetoothDevice?, + val viewModel: JlOtaPlugin + ) : IUpgradeCallback{ + companion object { + private const val TAG = "JlOtaPlugin" + private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" + private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" + + } + override fun onStartOTA() { + Log.d(TAG, "onStartOTA开始升级") + // viewModel.otaStateMLD.value = OTAStart(device) + } + + override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) { + Log.d(TAG, "onNeedReconnect需要重新链接") + // viewModel.otaStateMLD.value = OTAReconnect(device, addr, isNewReconnectWay) + // if (viewModel.isUseReconnectWay()) { + // viewModel.reconnectDev(addr, isNewReconnectWay) + // } + } + + override fun onProgress(type: Int, progress: Float) { + Log.d(TAG, "onProgress正在进展") + //viewModel.otaStateMLD.value = OTAWorking(device, type, progress) + } + + override fun onStopOTA() { + Log.d(TAG, "onStartOTA停止升级") +// viewModel.otaStateMLD.value = +// OTAEnd(device, ErrorCode.ERR_NONE, context.getString(R.string.ota_complete)) +// val handler = Handler(Looper.getMainLooper()) +// handler.postDelayed({ +// // Log.d("ZHM", "onStopOTA: disconnectDevice connected : "+viewModel.bluetoothHelper.isConnected()+ " getConnectedDevice: "+viewModel.bluetoothHelper.getConnectedDevice()) +// //变地址导致无法判断device是否一致 +// if (viewModel.bluetoothHelper.isConnected()/* && BluetoothUtil.deviceEquals( +// device, +// viewModel.bluetoothHelper.getConnectedDevice() +// )*/ +// ) { +// Log.d("ZHM", "onStopOTA: disconnectDevice") +// viewModel.bluetoothHelper.disconnectDevice(viewModel.otaManager.connectedDevice) +// } +// }, 500) + } + + override fun onCancelOTA() { + Log.d(TAG, "onCancelOTA取消升级") + // viewModel.otaStateMLD.value = + // OTAEnd( + // device, + // ErrorCode.ERR_UNKNOWN, + // context.getString(R.string.ota_upgrade_cancel) + // ) + } + + override fun onError(error: BaseError?) { + Log.d(TAG, "onError升级错误") + // error?.let { + // viewModel.otaStateMLD.value = OTAEnd(device, it.subCode, it.message) + // } + // viewModel.bluetoothHelper.disconnectDevice(viewModel.otaManager.connectedDevice)//升级失败,断开设备,方便自动化继续升级 + } + } +// class OtaDemo { +// private val otaManager = OTAManager(MainApplication.getInstance()) + // fun initOTA(context: Context, firmwarePath: String) { + // val otaManager = JlOtaPlugin() + // val bluetoothOption = BluetoothOTAConfigure.createDefault().apply { + // priority = BluetoothOTAConfigure.PREFER_BLE + // useAuthDevice = true + // bleIntervalMs = 500 + // timeoutMs = 3000 + // mtu = 500 + // needChangeMtu = false + // useReconnect = false + // setFirmwareFilePath(firmwarePath) + // } + // otaManager.configure(bluetoothOption) + // } + + // fun otaFirmware(filePath: String) { // 方法名改为小写开头 + // val otaManager = JlOtaPlugin() + + // otaManager.registerBluetoothCallback(object : BtEventCallback { + // override fun onConnection(device: BluetoothDevice, status: Int) { + // if (status == StateCode.CONNECTION_OK) { + // if (otaManager.isOTA) return + + // otaManager.queryMandatoryUpdate(object : IActionCallback { + // override fun onSuccess(deviceInfo: TargetInfoResponse) { + // // 处理设备信息 + // deviceInfo.versionCode + // deviceInfo.versionName + // deviceInfo.projectCode + // deviceInfo.uid + // deviceInfo.pid + // } + + // override fun onError(baseError: BaseError) { + // if (baseError.code == ErrorCode.ERR_NONE && + // baseError.subCode == ErrorCode.ERR_NONE) { + // val deviceInfo = otaManager.deviceInfo + // deviceInfo?.let { + // it.versionCode + // it.versionName + // it.projectCode + // it.uid + // it.pid + // } + // } + // } + // }) + // } + // } + // }) + + // otaManager.bluetoothOption?.setFirmwareFilePath(filePath) + + // otaManager.startOTA(object : IUpgradeCallback { + // override fun onStartOTA() = Unit + // override fun onNeedReconnect(addr: String, isNewReconnectWay: Boolean) { + // if (otaManager.bluetoothOption?.useReconnect == true) { + // // 处理自定义回连 + // } + // } + // override fun onProgress(type: Int, progress: Float) = Unit + // override fun onStopOTA() = Unit + // override fun onCancelOTA() = Unit + // override fun onError(error: BaseError) = Unit + // }) + + // otaManager.release() + // } +//} \ No newline at end of file diff --git a/local_plugins/jl_ota/ios/.gitignore b/local_plugins/jl_ota/ios/.gitignore new file mode 100644 index 000000000..034771fc9 --- /dev/null +++ b/local_plugins/jl_ota/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh diff --git a/local_plugins/jl_ota/ios/Assets/.gitkeep b/local_plugins/jl_ota/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/local_plugins/jl_ota/ios/Classes/JlOtaPlugin.swift b/local_plugins/jl_ota/ios/Classes/JlOtaPlugin.swift new file mode 100644 index 000000000..e5b6434f2 --- /dev/null +++ b/local_plugins/jl_ota/ios/Classes/JlOtaPlugin.swift @@ -0,0 +1,19 @@ +import Flutter +import UIKit + +public class JlOtaPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "jl_ota", binaryMessenger: registrar.messenger()) + let instance = JlOtaPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/local_plugins/jl_ota/ios/Resources/PrivacyInfo.xcprivacy b/local_plugins/jl_ota/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..a34b7e2e6 --- /dev/null +++ b/local_plugins/jl_ota/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/local_plugins/jl_ota/ios/jl_ota.podspec b/local_plugins/jl_ota/ios/jl_ota.podspec new file mode 100644 index 000000000..ebd8d61ed --- /dev/null +++ b/local_plugins/jl_ota/ios/jl_ota.podspec @@ -0,0 +1,29 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint jl_ota.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'jl_ota' + s.version = '0.0.1' + s.summary = 'A new Flutter project.' + s.description = <<-DESC +A new Flutter project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '12.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + # If your plugin requires a privacy manifest, for example if it uses any + # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your + # plugin's privacy impact, and then uncomment this line. For more information, + # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + # s.resource_bundles = {'jl_ota_privacy' => ['Resources/PrivacyInfo.xcprivacy']} +end diff --git a/local_plugins/jl_ota/lib/jl_ota.dart b/local_plugins/jl_ota/lib/jl_ota.dart new file mode 100644 index 000000000..ed1f427bd --- /dev/null +++ b/local_plugins/jl_ota/lib/jl_ota.dart @@ -0,0 +1,8 @@ + +import 'jl_ota_platform_interface.dart'; + +class JlOta { + Future getPlatformVersion() { + return JlOtaPlatform.instance.getPlatformVersion(); + } +} diff --git a/local_plugins/jl_ota/lib/jl_ota_method_channel.dart b/local_plugins/jl_ota/lib/jl_ota_method_channel.dart new file mode 100644 index 000000000..aa4647443 --- /dev/null +++ b/local_plugins/jl_ota/lib/jl_ota_method_channel.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'jl_ota_platform_interface.dart'; + +/// An implementation of [JlOtaPlatform] that uses method channels. +class MethodChannelJlOta extends JlOtaPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('jl_ota'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/local_plugins/jl_ota/lib/jl_ota_platform_interface.dart b/local_plugins/jl_ota/lib/jl_ota_platform_interface.dart new file mode 100644 index 000000000..f4c504da7 --- /dev/null +++ b/local_plugins/jl_ota/lib/jl_ota_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'jl_ota_method_channel.dart'; + +abstract class JlOtaPlatform extends PlatformInterface { + /// Constructs a JlOtaPlatform. + JlOtaPlatform() : super(token: _token); + + static final Object _token = Object(); + + static JlOtaPlatform _instance = MethodChannelJlOta(); + + /// The default instance of [JlOtaPlatform] to use. + /// + /// Defaults to [MethodChannelJlOta]. + static JlOtaPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [JlOtaPlatform] when + /// they register themselves. + static set instance(JlOtaPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/local_plugins/jl_ota/pubspec.yaml b/local_plugins/jl_ota/pubspec.yaml new file mode 100644 index 000000000..3684363bf --- /dev/null +++ b/local_plugins/jl_ota/pubspec.yaml @@ -0,0 +1,72 @@ +name: jl_ota +description: "A new Flutter project." +version: 0.0.1 +homepage: + +environment: + sdk: ^3.7.0 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.yunqiinnovation.jl_ota + pluginClass: JlOtaPlugin + ios: + pluginClass: JlOtaPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/pubspec.yaml b/pubspec.yaml index 608ba936e..2d0ff76aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,8 @@ dependencies: path: amap_flutter jl_opus: path: local_plugins/jl_opus + jl_ota: + path: local_plugins/jl_ota ble_service: path: local_plugins/ble_service deepvoice_utils: