Explorar o código

Merge branch 'v1.0.0' of git.atmob.com:Atmob-Flutter/ElectronicAssistant into v1.0.0

# Conflicts:
#	lib/router/app_pages.dart
Destiny hai 1 ano
pai
achega
a4978b10b8
Modificáronse 73 ficheiros con 2271 adicións e 211 borrados
  1. 3 0
      android/app/src/main/AndroidManifest.xml
  2. BIN=BIN
      assets/anim/anim_recording.zip
  3. BIN=BIN
      assets/anim/talk_analyse.gif
  4. 2 0
      assets/color/color.xml
  5. BIN=BIN
      assets/images/icon_record_add_shortcut.webp
  6. BIN=BIN
      assets/images/icon_record_cancel_disable.webp
  7. BIN=BIN
      assets/images/icon_record_cancel_enable.webp
  8. BIN=BIN
      assets/images/icon_record_logo.webp
  9. BIN=BIN
      assets/images/icon_record_pause.webp
  10. BIN=BIN
      assets/images/icon_record_resume.webp
  11. BIN=BIN
      assets/images/icon_record_save_disable.webp
  12. BIN=BIN
      assets/images/icon_record_save_enable.webp
  13. BIN=BIN
      assets/images/icon_record_start.webp
  14. BIN=BIN
      assets/images/icon_talk_analyse_fail.webp
  15. BIN=BIN
      assets/images/icon_talk_audio_pause.webp
  16. BIN=BIN
      assets/images/icon_talk_audio_playing.webp
  17. BIN=BIN
      assets/images/icon_talk_collapse.webp
  18. BIN=BIN
      assets/images/icon_talk_electric_low.webp
  19. BIN=BIN
      assets/images/icon_talk_electric_low_txt.webp
  20. BIN=BIN
      assets/images/icon_talk_expand.webp
  21. BIN=BIN
      assets/images/icon_talk_logo.webp
  22. BIN=BIN
      assets/images/icon_task_arrow.webp
  23. 14 0
      assets/string/base/string.xml
  24. 2 0
      ios/Runner/Info.plist
  25. 16 0
      lib/base/base_page.dart
  26. 42 0
      lib/data/api/atmob_api.dart
  27. 18 0
      lib/data/api/request/agenda_todo_request.dart
  28. 19 0
      lib/data/api/request/talk_create_request.dart
  29. 18 0
      lib/data/api/request/talk_generate_request.dart
  30. 15 0
      lib/data/api/request/talk_request.dart
  31. 16 0
      lib/data/api/response/agenda_list_all_response.dart
  32. 15 0
      lib/data/api/response/agenda_list_mine_response.dart
  33. 14 0
      lib/data/api/response/talk_check_electric_response.dart
  34. 20 0
      lib/data/api/response/talk_info_response.dart
  35. 16 0
      lib/data/api/response/talk_original_response.dart
  36. 3 0
      lib/data/bean/agenda.dart
  37. 21 0
      lib/data/bean/agenda_list_all_bean.dart
  38. 51 0
      lib/data/bean/talk_info.dart
  39. 36 0
      lib/data/bean/talk_original.dart
  40. 13 4
      lib/data/bean/talks.dart
  41. 17 0
      lib/data/bean/template_bean.dart
  42. 35 0
      lib/data/repositories/agenda_repository.dart
  43. 1 6
      lib/data/repositories/home_repository.dart
  44. 60 0
      lib/data/repositories/talk_repository.dart
  45. 0 35
      lib/data/repositories/task_repository.dart
  46. 0 0
      lib/dialog/alert_dialog.dart
  47. 6 1
      lib/module/chat/view.dart
  48. 3 3
      lib/module/home/controller.dart
  49. 1 3
      lib/module/home/view.dart
  50. 1 1
      lib/module/login/controller.dart
  51. 58 0
      lib/module/record/constants.dart
  52. 195 0
      lib/module/record/controller.dart
  53. 215 2
      lib/module/record/view.dart
  54. 1 1
      lib/module/splash/view.dart
  55. 225 0
      lib/module/talk/common_view.dart
  56. 87 8
      lib/module/talk/controller.dart
  57. 44 1
      lib/module/talk/original/controller.dart
  58. 79 1
      lib/module/talk/original/view.dart
  59. 40 4
      lib/module/talk/summary/controller.dart
  60. 76 40
      lib/module/talk/summary/view.dart
  61. 75 1
      lib/module/talk/todo/controller.dart
  62. 167 1
      lib/module/talk/todo/view.dart
  63. 334 78
      lib/module/talk/view.dart
  64. 1 1
      lib/module/task/controller.dart
  65. 1 1
      lib/module/task/view.dart
  66. 8 0
      lib/router/app_pages.dart
  67. 3 0
      lib/utils/common_utils.dart
  68. 8 3
      lib/utils/expand.dart
  69. 63 0
      lib/utils/pcm_wav_converter.dart
  70. 0 0
      lib/utils/popup_util.dart
  71. 18 4
      lib/widget/frame_animation_view.dart
  72. 89 12
      pubspec.lock
  73. 6 0
      pubspec.yaml

+ 3 - 0
android/app/src/main/AndroidManifest.xml

@@ -2,6 +2,9 @@
     package="com.atmob.elec_asst">
 
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
     <application
         android:name="${applicationName}"

BIN=BIN
assets/anim/anim_recording.zip


BIN=BIN
assets/anim/talk_analyse.gif


+ 2 - 0
assets/color/color.xml

@@ -6,4 +6,6 @@
     <color name="primaryTextColor">#25262A</color>
     <color name="secondaryTextColor">#5F5F61</color>
     <color name="tertiaryTextColor">#AFAFAF</color>
+
+    <color name="recordBackgroundColor">#25262A</color>
 </resources>

BIN=BIN
assets/images/icon_record_add_shortcut.webp


BIN=BIN
assets/images/icon_record_cancel_disable.webp


BIN=BIN
assets/images/icon_record_cancel_enable.webp


BIN=BIN
assets/images/icon_record_logo.webp


BIN=BIN
assets/images/icon_record_pause.webp


BIN=BIN
assets/images/icon_record_resume.webp


BIN=BIN
assets/images/icon_record_save_disable.webp


BIN=BIN
assets/images/icon_record_save_enable.webp


BIN=BIN
assets/images/icon_record_start.webp


BIN=BIN
assets/images/icon_talk_analyse_fail.webp


BIN=BIN
assets/images/icon_talk_audio_pause.webp


BIN=BIN
assets/images/icon_talk_audio_playing.webp


BIN=BIN
assets/images/icon_talk_collapse.webp


BIN=BIN
assets/images/icon_talk_electric_low.webp


BIN=BIN
assets/images/icon_talk_electric_low_txt.webp


BIN=BIN
assets/images/icon_talk_expand.webp


BIN=BIN
assets/images/icon_talk_logo.webp


BIN=BIN
assets/images/icon_task_arrow.webp


+ 14 - 0
assets/string/base/string.xml

@@ -49,4 +49,18 @@
     <string name="talk_un_analyzed">谈话未分析</string>
     <string name="talk_un_analyzed_tips">谈话提交分析后,可查看相关内容</string>
     <string name="talk_analyzed_btn_txt">小听分析</string>
+    <string name="talk_analyzing">小听正在分析谈话,请稍等~</string>
+    <string name="talk_electric_low">电量不足无法分析谈话</string>
+    <string name="talk_go_store">去充电</string>
+    <string name="talk_analyse_fail">分析失败</string>
+    <string name="talk_analyse_low_toast">小听电量太低,不足以分析这段谈话</string>
+    <string name="talk_analyse_summary_fail">谈话分析失败,点击去</string>
+    <string name="talk_todo_title">待办事项</string>
+    <string name="talk_todo_item">项</string>
+    <string name="talk_todo_all">所有待办</string>
+    <string name="talk_todo_set_mine">设为我的</string>
+    <string name="talk_todo_cancel_mine">取消待办</string>
+    <string name="record_status_pending">准备开始录音</string>
+    <string name="record_status_recording">我正在听...</string>
+    <string name="record_status_paused">录音已暂停</string>
 </resources>

+ 2 - 0
ios/Runner/Info.plist

@@ -45,5 +45,7 @@
 	<true/>
 	<key>UIApplicationSupportsIndirectInputEvents</key>
 	<true/>
+	<key>NSMicrophoneUsageDescription</key>
+    <string>谈话录音功能</string>
 </dict>
 </plist>

+ 16 - 0
lib/base/base_page.dart

@@ -1,3 +1,5 @@
+import 'dart:ui';
+
 import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -14,6 +16,8 @@ abstract class BasePage<T extends BaseController> extends GetView<T> {
       value: SystemUiOverlayStyle.light.copyWith(
         statusBarColor: Colors.transparent, // 设置状态栏颜色为透明
         statusBarIconBrightness: _getStatusBarDarkFont(),
+        systemNavigationBarColor: navigationBarColor(),
+        systemNavigationBarIconBrightness: _getNavigationBarDarkFont(),
       ),
       child: GestureDetector(
         onTap: () {
@@ -37,10 +41,18 @@ abstract class BasePage<T extends BaseController> extends GetView<T> {
     return statusBarDarkFont() ? Brightness.dark : Brightness.light;
   }
 
+  Brightness _getNavigationBarDarkFont() {
+    return statusBarDarkFont() ? Brightness.dark : Brightness.light;
+  }
+
   bool statusBarDarkFont() {
     return true;
   }
 
+  bool navigationBarDarkFont() {
+    return true;
+  }
+
   bool immersive() {
     return false;
   }
@@ -50,4 +62,8 @@ abstract class BasePage<T extends BaseController> extends GetView<T> {
   Color backgroundColor() {
     return ColorName.bgColorPrimary;
   }
+
+  Color navigationBarColor() {
+    return ColorName.bgColorPrimary;
+  }
 }

+ 42 - 0
lib/data/api/atmob_api.dart

@@ -4,16 +4,27 @@ import 'package:electronic_assistant/base/base_response.dart';
 import 'package:electronic_assistant/data/api/network_module.dart';
 import 'package:electronic_assistant/data/api/request/agenda_request.dart';
 import 'package:electronic_assistant/data/api/request/agenda_status_request.dart';
+import 'package:electronic_assistant/data/api/request/agenda_todo_request.dart';
 import 'package:electronic_assistant/data/api/request/chat_history_request.dart';
 import 'package:electronic_assistant/data/api/request/login_request.dart';
+import 'package:electronic_assistant/data/api/request/talk_create_request.dart';
 import 'package:electronic_assistant/data/api/request/talk_delete_request.dart';
+import 'package:electronic_assistant/data/api/request/talk_generate_request.dart';
 import 'package:electronic_assistant/data/api/request/talk_rename_request.dart';
+import 'package:electronic_assistant/data/api/request/talk_request.dart';
 import 'package:electronic_assistant/data/api/request/user_info_update_request.dart';
 import 'package:electronic_assistant/data/api/request/verification_code_request.dart';
+import 'package:electronic_assistant/data/api/response/agenda_list_all_response.dart';
+import 'package:electronic_assistant/data/api/response/agenda_list_mine_response.dart';
 import 'package:electronic_assistant/data/api/response/agenda_response.dart';
 import 'package:electronic_assistant/data/api/response/chat_history_response.dart';
 import 'package:electronic_assistant/data/api/response/home_info_response.dart';
 import 'package:electronic_assistant/data/api/response/login_response.dart';
+import 'package:electronic_assistant/data/api/response/talk_check_electric_response.dart';
+import 'package:electronic_assistant/data/api/response/talk_info_response.dart';
+import 'package:electronic_assistant/data/bean/talk_info.dart';
+import 'package:electronic_assistant/data/api/response/talk_original_response.dart';
+import 'package:electronic_assistant/data/bean/talks.dart';
 import 'package:electronic_assistant/data/consts/constants.dart';
 import 'package:retrofit/http.dart';
 
@@ -23,16 +34,20 @@ part 'atmob_api.g.dart';
 abstract class AtmobApi {
   factory AtmobApi(Dio dio, {String baseUrl}) = _AtmobApi;
 
+  /// 获取验证码
   @POST("/project/secretary/v1/user/code")
   Future<BaseResponse> getVerificationCode(
       @Body() VerificationCodeRequest request);
 
+  /// 登录
   @POST("/project/secretary/v1/user/login")
   Future<BaseResponse<LoginResponse>> login(@Body() LoginRequest request);
 
+  /// 更新用户信息
   @POST("/project/secretary/v1/user/info/update")
   Future<BaseResponse> updateUserInfo(@Body() UserInfoUpdateRequest request);
 
+  /// 首页信息
   @POST("/project/secretary/v1/home/info")
   Future<BaseResponse<HomeInfoResponse>> homeInfo(
       @Body() AppBaseRequest request);
@@ -50,9 +65,36 @@ abstract class AtmobApi {
   @POST("/project/secretary/v1/agenda/complete")
   Future<BaseResponse> agendaFinish(@Body() AgendaStatusRequest request);
 
+  /// 聊天记录
   @POST("/project/secretary/v1/chat/page")
   Future<BaseResponse<ChatHistoryResponse>> chatHistory(
       @Body() ChatHistoryRequest request);
+
+  @POST("/project/secretary/v1/talk/info")
+  Future<BaseResponse<TalkInfoResponse>> talkInfo(@Body() TalkRequest request);
+
+  @POST("/project/secretary/v1/agenda/list/all")
+  Future<BaseResponse<AgendaListAllResponse>> agendaListAll(
+      @Body() TalkRequest request);
+
+  @POST("/project/secretary/v1/agenda/list/mime")
+  Future<BaseResponse<AgendaListMineResponse>> agendaListMine(
+      @Body() TalkRequest request);
+
+  @POST("/project/secretary/v1/talk/original")
+  Future<BaseResponse<TalkOriginalResponse>> talkOriginal(
+      @Body() TalkRequest request);
+
+  @POST("/project/secretary/v1/talk/check/electric")
+  Future<BaseResponse<TalkCheckElectricResponse>> checkElectric(
+      @Body() TalkGenerateRequest request);
+
+  @POST("/project/secretary/v1/agenda/todo")
+  Future<BaseResponse> agendaTodo(@Body() AgendaTodoRequest request);
+
+  /// 录音完成,创建谈话记录
+  @POST("/project/secretary/v1/talk/create")
+  Future<BaseResponse<TalkBean>> talkCreate(@Body() TalkCreateRequest request);
 }
 
 final atmobApi = AtmobApi(defaultDio, baseUrl: Constants.baseUrl);

+ 18 - 0
lib/data/api/request/agenda_todo_request.dart

@@ -0,0 +1,18 @@
+import 'package:electronic_assistant/base/app_base_request.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'agenda_todo_request.g.dart';
+
+@JsonSerializable()
+class AgendaTodoRequest extends AppBaseRequest {
+  @JsonKey(name: 'id')
+  int? id;
+
+  @JsonKey(name: 'isTodo')
+  bool? isTodo;
+
+  AgendaTodoRequest(this.id, this.isTodo);
+
+  @override
+  Map<String, dynamic> toJson() => _$AgendaTodoRequestToJson(this);
+}

+ 19 - 0
lib/data/api/request/talk_create_request.dart

@@ -0,0 +1,19 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../../base/app_base_request.dart';
+
+part 'talk_create_request.g.dart';
+
+@JsonSerializable()
+class TalkCreateRequest extends AppBaseRequest {
+  @JsonKey(name: 'duration')
+  final int duration;
+
+  @JsonKey(name: 'requestId')
+  final String requestId;
+
+  TalkCreateRequest(this.duration, this.requestId);
+
+  @override
+  Map<String, dynamic> toJson() => _$TalkCreateRequestToJson(this);
+}

+ 18 - 0
lib/data/api/request/talk_generate_request.dart

@@ -0,0 +1,18 @@
+import 'package:electronic_assistant/base/app_base_request.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'talk_generate_request.g.dart';
+
+@JsonSerializable()
+class TalkGenerateRequest extends AppBaseRequest {
+
+  @JsonKey(name: 'duration')
+  double duration;
+
+  TalkGenerateRequest(this.duration);
+
+
+  @override
+  Map<String, dynamic> toJson() => _$TalkGenerateRequestToJson(this);
+
+}

+ 15 - 0
lib/data/api/request/talk_request.dart

@@ -0,0 +1,15 @@
+import 'package:electronic_assistant/base/app_base_request.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'talk_request.g.dart';
+
+@JsonSerializable()
+class TalkRequest extends AppBaseRequest {
+  @JsonKey(name: 'talkId')
+  String? talkId;
+
+  TalkRequest(this.talkId);
+
+  @override
+  Map<String, dynamic> toJson() => _$TalkRequestToJson(this);
+}

+ 16 - 0
lib/data/api/response/agenda_list_all_response.dart

@@ -0,0 +1,16 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/agenda_list_all_bean.dart';
+
+part 'agenda_list_all_response.g.dart';
+
+@JsonSerializable()
+class AgendaListAllResponse {
+  @JsonKey(name: 'list')
+  List<AgendaListAllBean>? list;
+
+  AgendaListAllResponse({required this.list});
+
+  factory AgendaListAllResponse.fromJson(Map<String, dynamic> json) =>
+      _$AgendaListAllResponseFromJson(json);
+}

+ 15 - 0
lib/data/api/response/agenda_list_mine_response.dart

@@ -0,0 +1,15 @@
+import 'package:electronic_assistant/data/bean/agenda.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'agenda_list_mine_response.g.dart';
+
+@JsonSerializable()
+class AgendaListMineResponse {
+  @JsonKey(name: 'agendas')
+  List<Agenda>? list;
+
+  AgendaListMineResponse({required this.list});
+
+  factory AgendaListMineResponse.fromJson(Map<String, dynamic> json) =>
+      _$AgendaListMineResponseFromJson(json);
+}

+ 14 - 0
lib/data/api/response/talk_check_electric_response.dart

@@ -0,0 +1,14 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'talk_check_electric_response.g.dart';
+
+@JsonSerializable()
+class TalkCheckElectricResponse {
+  @JsonKey(name: 'enough')
+  bool enough;
+
+  TalkCheckElectricResponse({required this.enough});
+
+  factory TalkCheckElectricResponse.fromJson(Map<String, dynamic> json) =>
+      _$TalkCheckElectricResponseFromJson(json);
+}

+ 20 - 0
lib/data/api/response/talk_info_response.dart

@@ -0,0 +1,20 @@
+import 'package:electronic_assistant/data/bean/talk_info.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/template_bean.dart';
+
+part 'talk_info_response.g.dart';
+
+@JsonSerializable()
+class TalkInfoResponse {
+  @JsonKey(name: 'talkInfo')
+  TalkInfo? talkInfo;
+
+  @JsonKey(name: 'templates')
+  List<TemplateBean>? templateList;
+
+  TalkInfoResponse({this.talkInfo, this.templateList});
+
+  factory TalkInfoResponse.fromJson(Map<String, dynamic> json) =>
+      _$TalkInfoResponseFromJson(json);
+}

+ 16 - 0
lib/data/api/response/talk_original_response.dart

@@ -0,0 +1,16 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/talk_original.dart';
+
+part 'talk_original_response.g.dart';
+
+@JsonSerializable()
+class TalkOriginalResponse {
+  @JsonKey(name: 'list')
+  List<TalkOriginal>? list;
+
+  TalkOriginalResponse({this.list});
+
+  factory TalkOriginalResponse.fromJson(Map<String, dynamic> json) =>
+      _$TalkOriginalResponseFromJson(json);
+}

+ 3 - 0
lib/data/bean/agenda.dart

@@ -1,3 +1,4 @@
+import 'package:get/get.dart';
 import 'package:json_annotation/json_annotation.dart';
 
 part 'agenda.g.dart';
@@ -30,6 +31,8 @@ class Agenda {
 
   bool? isDone;
 
+  final RxBool isSetMine = false.obs;
+
   Agenda(
       {this.id,
       this.talkId,

+ 21 - 0
lib/data/bean/agenda_list_all_bean.dart

@@ -0,0 +1,21 @@
+import 'package:electronic_assistant/data/bean/agenda.dart';
+import 'package:get/get.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'agenda_list_all_bean.g.dart';
+
+@JsonSerializable()
+class AgendaListAllBean {
+  @JsonKey(name: 'name')
+  String? name;
+
+  @JsonKey(name: 'agendas')
+  List<Agenda>? list;
+
+  final isExpanded = false.obs;
+
+  AgendaListAllBean({this.name, this.list});
+
+  factory AgendaListAllBean.fromJson(Map<String, dynamic> json) =>
+      _$AgendaListAllBeanFromJson(json);
+}

+ 51 - 0
lib/data/bean/talk_info.dart

@@ -0,0 +1,51 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'talk_info.g.dart';
+
+@JsonSerializable()
+class TalkInfo {
+  @JsonKey(name: 'id')
+  String? id;
+
+  @JsonKey(name: 'taskId')
+  String? taskId;
+
+  @JsonKey(name: 'ssid')
+  String? ssid;
+
+  @JsonKey(name: 'audioUrl')
+  String? audioUrl;
+
+  @JsonKey(name: 'duration')
+  double? duration;
+
+  @JsonKey(name: 'status')
+  int? status;
+
+  @JsonKey(name: 'title')
+  String? title;
+
+  @JsonKey(name: 'summary')
+  String? summary;
+
+  @JsonKey(name: 'createTime')
+  String? createTime;
+
+  @JsonKey(name: 'example')
+  bool? example;
+
+  TalkInfo(
+      {this.id,
+      this.taskId,
+      this.ssid,
+      this.audioUrl,
+      this.duration,
+      this.status,
+      this.title,
+      this.summary,
+      this.createTime,
+      this.example});
+
+  factory TalkInfo.fromJson(Map<String, dynamic> json) =>
+      _$TalkInfoFromJson(json);
+}

+ 36 - 0
lib/data/bean/talk_original.dart

@@ -0,0 +1,36 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'talk_original.g.dart';
+
+@JsonSerializable()
+class TalkOriginal {
+  @JsonKey(name: 'endMs')
+  int? endMs;
+
+  @JsonKey(name: 'startMs')
+  int? startMs;
+
+  @JsonKey(name: 'duration')
+  double? duration;
+
+  @JsonKey(name: 'speakerId')
+  int? speakerId;
+
+  @JsonKey(name: 'speaker')
+  String? speaker;
+
+  @JsonKey(name: 'sentence')
+  String? sentence;
+
+  TalkOriginal({
+    this.endMs,
+    this.startMs,
+    this.duration,
+    this.speakerId,
+    this.speaker,
+    this.sentence,
+  });
+
+  factory TalkOriginal.fromJson(Map<String, dynamic> json) =>
+      _$TalkOriginalFromJson(json);
+}

+ 13 - 4
lib/data/bean/talks.dart

@@ -5,7 +5,7 @@ part 'talks.g.dart';
 @JsonSerializable()
 class TalkBean {
   @JsonKey(name: 'id')
-  String? id;
+  late final String id;
 
   @JsonKey(name: 'taskId')
   String? taskId;
@@ -37,10 +37,8 @@ class TalkBean {
   @JsonKey(name: 'example')
   bool? isExample;
 
-  bool? isAnalyseDone;
-
   TalkBean(
-      {this.id,
+      {required this.id,
       this.taskId,
       this.ssid,
       this.audioUrl,
@@ -55,3 +53,14 @@ class TalkBean {
   factory TalkBean.fromJson(Map<String, dynamic> json) =>
       _$TalkBeanFromJson(json);
 }
+
+class TalkStatus {
+  TalkStatus._();
+
+  //(0等待生成 1生成中,都处于生成中)  2成功 3失败 4未分析
+  static int analysing = 0;
+  static int waitAnalysis = 1;
+  static int analysisSuccess = 2;
+  static int analysisFail = 3;
+  static int notAnalysis = 4;
+}

+ 17 - 0
lib/data/bean/template_bean.dart

@@ -0,0 +1,17 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'template_bean.g.dart';
+
+@JsonSerializable()
+class TemplateBean {
+  @JsonKey(name: 'id')
+  int? id;
+
+  @JsonKey(name: 'name')
+  String? name;
+
+  TemplateBean({this.id, this.name});
+
+  factory TemplateBean.fromJson(Map<String, dynamic> json) =>
+      _$TemplateBeanFromJson(json);
+}

+ 35 - 0
lib/data/repositories/agenda_repository.dart

@@ -1,15 +1,50 @@
+import 'package:electronic_assistant/data/api/request/talk_request.dart';
+
 import '../../utils/http_handler.dart';
 import '../api/atmob_api.dart';
+import '../api/request/agenda_request.dart';
 import '../api/request/agenda_status_request.dart';
+import '../api/request/agenda_todo_request.dart';
+import '../api/response/agenda_list_all_response.dart';
+import '../api/response/agenda_list_mine_response.dart';
+import '../api/response/agenda_response.dart';
 
 class AgendaRepository {
   AgendaRepository._();
 
+  Future<AgendaResponse> agendaPage(int page, int pageSize,
+      {int? startTime, int? endTime, TaskStatus? completeStatus}) {
+    return atmobApi
+        .agendaPage(AgendaRequest(page, pageSize,
+            startTime: startTime,
+            endTime: endTime,
+            completeStatus: completeStatus?.value))
+        .then(HttpHandler.handle(true));
+  }
+
   Future<void> agendaFinish(int? id, bool complete) {
     return atmobApi
         .agendaFinish(AgendaStatusRequest(id, complete))
         .then(HttpHandler.handle(true));
   }
+
+  Future<AgendaListAllResponse> agendaListAll(String talkId) {
+    return atmobApi
+        .agendaListAll(TalkRequest(talkId))
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<AgendaListMineResponse> agendaListMine(String talkId) {
+    return atmobApi
+        .agendaListMine(TalkRequest(talkId))
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<void> agendaTodo(int id, bool isTodo) {
+    return atmobApi
+        .agendaTodo(AgendaTodoRequest(id, isTodo))
+        .then(HttpHandler.handle(true));
+  }
 }
 
 final agendaRepository = AgendaRepository._();

+ 1 - 6
lib/data/repositories/home_repository.dart

@@ -9,12 +9,7 @@ class HomeRepository {
 
   Future<HomeInfoResponse> homeInfo() {
     return atmobApi
-        .homeInfo(AppBaseRequest())
-        .then(HttpHandler.handle(true))
-        .then((data) {
-      data.talks.map((bean) => bean.isAnalyseDone = true);
-      return data;
-    });
+        .homeInfo(AppBaseRequest()).then(HttpHandler.handle(true));
   }
 }
 

+ 60 - 0
lib/data/repositories/talk_repository.dart

@@ -0,0 +1,60 @@
+import 'package:electronic_assistant/data/api/atmob_api.dart';
+import 'package:electronic_assistant/data/api/request/talk_create_request.dart';
+import 'package:electronic_assistant/data/api/request/talk_delete_request.dart';
+
+import '../../utils/http_handler.dart';
+import '../api/request/talk_generate_request.dart';
+import '../api/request/talk_rename_request.dart';
+import '../api/request/talk_request.dart';
+import '../api/response/talk_check_electric_response.dart';
+import '../api/response/talk_info_response.dart';
+import '../bean/talk_original.dart';
+import '../bean/talks.dart';
+
+class TalkRepository {
+  TalkRepository._();
+
+  Future<List<TalkOriginal>> talkOriginal(String? talkId) {
+    return atmobApi
+        .talkOriginal(TalkRequest(talkId))
+        .then(HttpHandler.handle(false))
+        .then((data) {
+      if (data.list != null) {
+        return data.list!;
+      } else {
+        return [];
+      }
+    });
+  }
+
+  // duration 音频时长,单位为秒
+  Future<TalkCheckElectricResponse> checkElectric(double duration) {
+    return atmobApi
+        .checkElectric(TalkGenerateRequest(duration))
+        .then(HttpHandler.handle(false));
+  }
+
+  Future<TalkInfoResponse> talkInfo(String id) {
+    return atmobApi.talkInfo(TalkRequest(id)).then(HttpHandler.handle(true));
+  }
+
+  Future<void> talkRename(String? id, String? title) {
+    return atmobApi
+        .talkRename(TalkRenameRequest(id, title))
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<void> talkDelete(String? id) {
+    return atmobApi
+        .talkDelete(TalkDeleteRequest(id))
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<TalkBean> talkCreate(String requestId, int duration) {
+    return atmobApi
+        .talkCreate(TalkCreateRequest(duration, requestId))
+        .then(HttpHandler.handle(true));
+  }
+}
+
+final talkRepository = TalkRepository._();

+ 0 - 35
lib/data/repositories/task_repository.dart

@@ -1,35 +0,0 @@
-import 'package:electronic_assistant/data/api/atmob_api.dart';
-import 'package:electronic_assistant/data/api/request/talk_delete_request.dart';
-
-import '../../utils/http_handler.dart';
-import '../api/request/agenda_request.dart';
-import '../api/request/talk_rename_request.dart';
-import '../api/response/agenda_response.dart';
-
-class TaskRepository {
-  TaskRepository._();
-
-  Future<AgendaResponse> agendaPage(int page, int pageSize,
-      {int? startTime, int? endTime, TaskStatus? completeStatus}) {
-    return atmobApi
-        .agendaPage(AgendaRequest(page, pageSize,
-            startTime: startTime,
-            endTime: endTime,
-            completeStatus: completeStatus?.value))
-        .then(HttpHandler.handle(true));
-  }
-
-  Future<void> talkRename(String? id, String? title) {
-    return atmobApi
-        .talkRename(TalkRenameRequest(id, title))
-        .then(HttpHandler.handle(true));
-  }
-
-  Future<void> talkDelete(String? id) {
-    return atmobApi
-        .talkDelete(TalkDeleteRequest(id))
-        .then(HttpHandler.handle(true));
-  }
-}
-
-final taskRepository = TaskRepository._();

lib/widget/alert_dialog.dart → lib/dialog/alert_dialog.dart


+ 6 - 1
lib/module/chat/view.dart

@@ -23,6 +23,11 @@ class ChatPage extends BasePage<ChatController> {
   }
 
   @override
+  Color navigationBarColor() {
+    return "#F6F6F6".color;
+  }
+
+  @override
   Widget buildBody(BuildContext context) {
     // 第一次启动时弹出定制窗口
     controller.showStartSheet(context);
@@ -35,7 +40,7 @@ class ChatPage extends BasePage<ChatController> {
           backgroundColor: Colors.transparent,
           appBar: AppBar(
             leading: IconButton(
-              icon: const Icon(Icons.arrow_back_ios),
+              icon: const Icon(Icons.arrow_back_ios_new_rounded),
               onPressed: () {
                 Navigator.pop(context);
               },

+ 3 - 3
lib/module/home/controller.dart

@@ -1,7 +1,7 @@
 import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:electronic_assistant/data/bean/talks.dart';
 import 'package:electronic_assistant/data/repositories/agenda_repository.dart';
-import 'package:electronic_assistant/data/repositories/task_repository.dart';
+import 'package:electronic_assistant/data/repositories/talk_repository.dart';
 import 'package:electronic_assistant/module/home/view.dart';
 import 'package:electronic_assistant/module/main/controller.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
@@ -64,7 +64,7 @@ class HomePageController extends BaseController {
   }
 
   void requestName(String? newName, TalkBean bean) {
-    taskRepository.talkRename(bean.id, newName).then((data) {
+    talkRepository.talkRename(bean.id, newName).then((data) {
       bean.title = newName;
       int index = taskList.indexOf(bean);
       if (index != -1) {
@@ -85,7 +85,7 @@ class HomePageController extends BaseController {
   }
 
   void requestDelete(TalkBean item) {
-    taskRepository.talkDelete(item.id).then((data) {
+    talkRepository.talkDelete(item.id).then((data) {
       requestHomeData();
     }).catchError((error) {
       ErrorHandler.toastError(error, message: StringName.talkDeleteFail.tr);

+ 1 - 3
lib/module/home/view.dart

@@ -254,9 +254,7 @@ class HomePage extends BasePage<HomePageController> {
   SliverToBoxAdapter buildGoRecordView() {
     return SliverToBoxAdapter(
         child: GestureDetector(
-      onTap: () {
-        showUnfinishedRecordPopup();
-      },
+      onTap: () => Get.toNamed(RoutePath.record),
       child: Container(
         margin: EdgeInsets.only(right: 8.w),
         decoration: BoxDecoration(

+ 1 - 1
lib/module/login/controller.dart

@@ -4,7 +4,7 @@ import 'package:electronic_assistant/popup/talk_popup.dart';
 import 'package:electronic_assistant/utils/error_handler.dart';
 import 'package:electronic_assistant/utils/expand.dart';
 import 'package:electronic_assistant/utils/toast_util.dart';
-import 'package:electronic_assistant/widget/alert_dialog.dart';
+import 'package:electronic_assistant/dialog/alert_dialog.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';

+ 58 - 0
lib/module/record/constants.dart

@@ -0,0 +1,58 @@
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
+
+import '../../resource/assets.gen.dart';
+import '../../resource/string.gen.dart';
+
+enum RecordStatus {
+  pending,
+  recording,
+  paused,
+}
+
+extension RecordStatusExtension on RecordStatus {
+  String get desc {
+    switch (this) {
+      case RecordStatus.pending:
+        return StringName.recordStatusPending.tr;
+      case RecordStatus.recording:
+        return StringName.recordStatusRecording.tr;
+      case RecordStatus.paused:
+        return StringName.recordStatusPaused.tr;
+    }
+  }
+
+  ImageProvider get actionButtonImage {
+    switch (this) {
+      case RecordStatus.pending:
+        return Assets.images.iconRecordStart.provider();
+      case RecordStatus.recording:
+        return Assets.images.iconRecordPause.provider();
+      case RecordStatus.paused:
+        return Assets.images.iconRecordResume.provider();
+    }
+  }
+
+  ImageProvider get saveButtonImage {
+    return this == RecordStatus.pending
+        ? Assets.images.iconRecordSaveDisable.provider()
+        : Assets.images.iconRecordSaveEnable.provider();
+  }
+
+  ImageProvider get cancelButtonImage {
+    return this == RecordStatus.pending
+        ? Assets.images.iconRecordCancelDisable.provider()
+        : Assets.images.iconRecordCancelEnable.provider();
+  }
+
+  RecordStatus get nextStatus {
+    switch (this) {
+      case RecordStatus.pending:
+        return RecordStatus.recording;
+      case RecordStatus.recording:
+        return RecordStatus.paused;
+      case RecordStatus.paused:
+        return RecordStatus.recording;
+    }
+  }
+}

+ 195 - 0
lib/module/record/controller.dart

@@ -1,8 +1,203 @@
+import 'dart:io';
+import 'dart:typed_data';
+
 import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:electronic_assistant/data/repositories/talk_repository.dart';
+import 'package:electronic_assistant/dialog/alert_dialog.dart';
+import 'package:electronic_assistant/module/record/constants.dart';
+import 'package:electronic_assistant/module/talk/view.dart';
+import 'package:electronic_assistant/utils/mmkv_util.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:record/record.dart';
+import 'package:uuid/uuid.dart';
+
+import '../../utils/pcm_wav_converter.dart';
+import '../../widget/frame_animation_view.dart';
 
 class RecordController extends BaseController {
+  static const String keyLastRecordId = "last_record_id";
+
+  final FrameAnimationController frameAnimationController =
+      FrameAnimationController(autoPlay: false);
+  final Rx<RecordStatus> currentStatus = RecordStatus.pending.obs;
+  final RxDouble currentDuration = 0.0.obs;
+  final AudioRecorder record = AudioRecorder();
+  final RecordConfig recordConfig = const RecordConfig(
+      encoder: AudioEncoder.pcm16bits,
+      bitRate: 128000,
+      sampleRate: 44100,
+      numChannels: 2);
+  late final String lastRecordId;
+
   @override
   void onInit() {
     super.onInit();
+    _initLastRecordId();
+    _initLastRecordStatus();
+  }
+
+  void _initLastRecordId() {
+    String? lastRecordId = KVUtil.getString(keyLastRecordId, null);
+    if (lastRecordId == null || lastRecordId.isEmpty) {
+      this.lastRecordId = const Uuid().v4();
+      KVUtil.putString(keyLastRecordId, this.lastRecordId);
+    } else {
+      this.lastRecordId = lastRecordId;
+    }
+  }
+
+  Future<void> _initLastRecordStatus() async {
+    var currentRecordFile = await _getCurrentRecordFile();
+    var fileLength = currentRecordFile.lengthSync();
+    if (currentRecordFile.existsSync() && fileLength > 0) {
+      _changeRecordStatus(RecordStatus.paused);
+      currentDuration.value = await _getPcmDuration(
+          fileLength, recordConfig.sampleRate, 16, recordConfig.numChannels);
+    }
+  }
+
+  void addShortcut() {}
+
+  void onBackClick() {
+    if (currentStatus.value == RecordStatus.pending ||
+        currentStatus.value == RecordStatus.paused) {
+      Get.back();
+    } else {
+      EAAlertDialog.show(
+        title: "是否保存当前录音?",
+        confirmText: "确定",
+        cancelText: "取消",
+        confirmOnTap: () {
+          _saveCurrentRecord();
+          EAAlertDialog.dismiss();
+        },
+        cancelOnTap: () {
+          EAAlertDialog.dismiss();
+          _stopRecord().then((_) => Get.back());
+        },
+      );
+    }
+  }
+
+  void onActionClick() {
+    RecordStatus nextStatus = currentStatus.value.nextStatus;
+    if (nextStatus == RecordStatus.recording) {
+      _startOrContinueRecord();
+    } else {
+      _stopRecord();
+    }
+  }
+
+  void onCancelClick() {
+    if (currentStatus.value == RecordStatus.pending) {
+      return;
+    }
+    EAAlertDialog.show(
+      title: "是否删除当前录音?",
+      confirmText: "删除",
+      cancelText: "取消",
+      confirmOnTap: () {
+        _deleteCurrentRecord();
+        EAAlertDialog.dismiss();
+      },
+      cancelOnTap: () {
+        EAAlertDialog.dismiss();
+      },
+    );
+  }
+
+  void onSaveClick() {
+    if (currentStatus.value == RecordStatus.pending) {
+      return;
+    }
+    _saveCurrentRecord();
+  }
+
+  Future<void> _startOrContinueRecord() async {
+    bool hasPermission = await record.hasPermission();
+    if (!hasPermission) {
+      _onRecordPermissionDenied();
+      return;
+    }
+    File targetFile = await _getCurrentRecordFile();
+    Stream<Uint8List> recordStream = await record.startStream(recordConfig);
+    _changeRecordStatus(RecordStatus.recording);
+    recordStream.listen((data) async {
+      targetFile.writeAsBytesSync(data, mode: FileMode.append);
+      currentDuration.value = currentDuration.value +
+          await _getPcmDuration(data.length, recordConfig.sampleRate, 16,
+              recordConfig.numChannels);
+    }, onDone: () {
+      _changeRecordStatus(RecordStatus.paused);
+    }, onError: (error) {
+      _changeRecordStatus(RecordStatus.paused);
+    });
+  }
+
+  _onRecordPermissionDenied() {}
+
+  Future<void> _stopRecord() {
+    return record.stop().then((_) => _changeRecordStatus(RecordStatus.paused));
+  }
+
+  Future<File> _getCurrentRecordFile() async {
+    Directory documentDir = await getApplicationDocumentsDirectory();
+    File file = File("${documentDir.path}/.atmob/record/$lastRecordId");
+    if (!file.existsSync()) {
+      file.createSync(recursive: true);
+    }
+    return file;
+  }
+
+  Future<double> _getPcmDuration(
+      int fileSize, int sampleRate, int bitDepth, int channels) async {
+    final bytesPerSecond = sampleRate * (bitDepth / 8) * channels;
+    final durationInSeconds = fileSize / bytesPerSecond;
+    return durationInSeconds;
+  }
+
+  Future<void> _deleteCurrentRecord() async {
+    await _stopRecord();
+    _getCurrentRecordFile().then((file) {
+      if (file.existsSync()) {
+        file.deleteSync();
+      }
+    }).then((_) {
+      currentDuration.value = 0;
+      _changeRecordStatus(RecordStatus.pending);
+    });
+  }
+
+  Future<void> _saveCurrentRecord() async {
+    await _stopRecord();
+    talkRepository
+        .talkCreate(lastRecordId, currentDuration.value.toInt())
+        .then((talkInfo) async {
+      File pcmFile = await _getCurrentRecordFile();
+      if (pcmFile.existsSync()) {
+        File wavFile = await getRecordFile(talkInfo.id);
+        PcmWavConverter.convert(pcmFile, wavFile, recordConfig.sampleRate,
+            recordConfig.numChannels, 16);
+        pcmFile.delete();
+        Get.back();
+        TalkPage.start(talkInfo);
+      } else {
+        throw Exception("pcm file not found");
+      }
+    }).catchError((error) {
+      debugPrint(error);
+    });
+  }
+
+  void _changeRecordStatus(RecordStatus status) {
+    currentStatus.value = status;
+    status == RecordStatus.recording ? frameAnimationController.play() : null;
+  }
+
+  static Future<File> getRecordFile(String talkId) async {
+    Directory documentDir = await getApplicationDocumentsDirectory();
+    return File("${documentDir.path}/.atmob/record/$talkId.wav");
   }
 }

+ 215 - 2
lib/module/record/view.dart

@@ -1,12 +1,225 @@
 import 'package:electronic_assistant/base/base_page.dart';
+import 'package:electronic_assistant/module/record/constants.dart';
 import 'package:electronic_assistant/module/record/controller.dart';
+import 'package:electronic_assistant/resource/colors.gen.dart';
+import 'package:electronic_assistant/utils/expand.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+
+import '../../resource/assets.gen.dart';
+import '../../widget/frame_animation_view.dart';
 
 class RecordPage extends BasePage<RecordController> {
   const RecordPage({super.key});
 
   @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  bool statusBarDarkFont() {
+    return false;
+  }
+
+  @override
+  Color backgroundColor() {
+    return ColorName.recordBackgroundColor;
+  }
+
+  @override
+  Color navigationBarColor() {
+    return "#4A4F67".color;
+  }
+
+  @override
   Widget buildBody(BuildContext context) {
-    throw UnimplementedError();
+    return Stack(alignment: Alignment.bottomCenter, children: [
+      _buildBottomGradient(),
+      Scaffold(
+        appBar: AppBar(
+          leading: IconButton(
+            icon: const Icon(Icons.arrow_back_ios_new_rounded),
+            color: ColorName.white,
+            onPressed: () {
+              controller.onBackClick();
+            },
+          ),
+          scrolledUnderElevation: 0,
+          backgroundColor: ColorName.transparent,
+          systemOverlayStyle: SystemUiOverlayStyle.light,
+          actions: [
+            _buildAddShortcut(false),
+          ],
+        ),
+        backgroundColor: ColorName.transparent,
+        body: Flex(
+          direction: Axis.vertical,
+          children: [
+            _buildRecordStatus(),
+            const Spacer(flex: 271),
+            _buildRecordAnim(),
+            const Spacer(flex: 407),
+            _buildRecordControl(),
+          ],
+        ),
+      ),
+    ]);
+  }
+
+  Widget _buildAddShortcut(bool visible) {
+    return GestureDetector(
+      onTap: () {
+        controller.addShortcut();
+      },
+      child: Visibility(
+        visible: visible,
+        child: Row(
+          children: [
+            Image(
+                image: Assets.images.iconRecordAddShortcut.provider(),
+                width: 24.w,
+                height: 24.w),
+            Padding(
+              padding: EdgeInsets.only(left: 8.w, right: 16.w),
+              child: Text(
+                '添加到桌面',
+                style: TextStyle(color: ColorName.white, fontSize: 14.w),
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildRecordStatus() {
+    return Container(
+      padding: EdgeInsets.symmetric(horizontal: 16.w),
+      margin: EdgeInsets.only(top: 20.w),
+      child: Row(
+        children: [
+          Container(
+            margin: EdgeInsets.only(right: 8.w),
+            child: Image(
+                image: Assets.images.iconRecordLogo.provider(),
+                width: 45.w,
+                height: 48.w),
+          ),
+          Obx(() {
+            return Text(
+              controller.currentStatus.value.desc,
+              style: TextStyle(color: ColorName.white, fontSize: 17.w),
+            );
+          }),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildRecordAnim() {
+    return Obx(() {
+      return AnimatedOpacity(
+        opacity:
+            controller.currentStatus.value == RecordStatus.recording ? 1 : 0,
+        duration: const Duration(milliseconds: 520),
+        child: FrameAnimationView(
+          controller: controller.frameAnimationController,
+          framePath: 'assets/anim/anim_recording.zip',
+          speed: 2,
+          width: 360.w,
+          height: 180.w,
+        ),
+      );
+    });
+  }
+
+  Widget _buildRecordControl() {
+    return Stack(
+      alignment: Alignment.bottomCenter,
+      children: [
+        Container(
+          padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 30.w),
+          decoration: BoxDecoration(
+            color: "#4A4F67".color,
+            borderRadius: BorderRadius.only(
+                topLeft: Radius.circular(40.w),
+                topRight: Radius.circular(40.w)),
+          ),
+          child: Row(
+            children: [
+              GestureDetector(
+                onTap: controller.onCancelClick,
+                child: Obx(() {
+                  return Image(
+                      image: controller.currentStatus.value.cancelButtonImage,
+                      width: 56.w,
+                      height: 56.w);
+                }),
+              ),
+              const Spacer(),
+              GestureDetector(
+                onTap: controller.onSaveClick,
+                child: Obx(() {
+                  return Image(
+                      image: controller.currentStatus.value.saveButtonImage,
+                      width: 56.w,
+                      height: 56.w);
+                }),
+              ),
+            ],
+          ),
+        ),
+        Column(
+          children: [
+            GestureDetector(
+                onTap: () {
+                  controller.onActionClick();
+                },
+                child: Obx(
+                  () => Image(
+                      image: controller.currentStatus.value.actionButtonImage,
+                      width: 92.w,
+                      height: 92.w),
+                )),
+            Padding(
+              padding: EdgeInsets.only(top: 10.w, bottom: 35.w),
+              child: Obx(() => Text(
+                    formatDuration(controller.currentDuration.value),
+                    style: TextStyle(
+                      color: ColorName.white,
+                      fontSize: 16.w,
+                    ),
+                  )),
+            )
+          ],
+        )
+      ],
+    );
+  }
+
+  Widget _buildBottomGradient() {
+    return Container(
+      height: 0.38.sh,
+      decoration: BoxDecoration(
+        gradient: LinearGradient(
+          begin: Alignment.topCenter,
+          end: Alignment.bottomCenter,
+          colors: [
+            "#006177F2".color,
+            "#806177F2".color,
+          ],
+        ),
+      ),
+    );
+  }
+
+  String formatDuration(double value) {
+    int hour = (value / 3600).floor();
+    int minute = ((value - hour * 3600) / 60).floor();
+    int second = (value - hour * 3600 - minute * 60).floor();
+    return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}:${second.toString().padLeft(2, '0')}';
   }
-}
+}

+ 1 - 1
lib/module/splash/view.dart

@@ -17,7 +17,7 @@ class SplashPage extends BasePage {
       Get.offNamed(RoutePath.mainTab);
     });
     return GestureDetector(
-        child: Center(
+        child: const Center(
             child: Text(
       '启屏页',
       style: TextStyle(fontSize: 36),

+ 225 - 0
lib/module/talk/common_view.dart

@@ -0,0 +1,225 @@
+import 'package:electronic_assistant/data/bean/agenda_list_all_bean.dart';
+import 'package:electronic_assistant/utils/expand.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+
+import '../../data/bean/agenda.dart';
+import '../../resource/assets.gen.dart';
+import '../../resource/colors.gen.dart';
+import '../../resource/string.gen.dart';
+
+Widget getTalkLoadingView() {
+  return SizedBox(
+    width: double.infinity,
+    child: Column(
+      children: [
+        SizedBox(height: 138.h),
+        SizedBox(
+            width: 100.w,
+            height: 100.w,
+            child: Assets.anim.talkAnalyse.image()),
+        SizedBox(height: 12.h),
+        Text(StringName.talkAnalyzing.tr,
+            style:
+                TextStyle(fontSize: 14.sp, color: ColorName.secondaryTextColor))
+      ],
+    ),
+  );
+}
+
+Widget getTalkFailView() {
+  return SizedBox(
+    width: double.infinity,
+    child: Column(
+      children: [
+        SizedBox(height: 111.h),
+        SizedBox(
+            width: 100.w,
+            height: 100.w,
+            child: Assets.images.iconTalkAnalyseFail.image()),
+        SizedBox(height: 4.h),
+        Text(StringName.talkAnalyseFail.tr,
+            style:
+                TextStyle(fontSize: 15.sp, color: ColorName.primaryTextColor)),
+        SizedBox(height: 2.h),
+      ],
+    ),
+  );
+}
+
+typedef TodoItemBuilder = List<Widget> Function(List<Agenda>? list);
+
+Widget getTodoItemView(AgendaListAllBean agenda, TodoItemBuilder builder) {
+  return Container(
+    decoration: BoxDecoration(
+      color: ColorName.colorPrimary,
+      borderRadius: BorderRadius.circular(6),
+    ),
+    child: Builder(builder: (context) {
+      return Theme(
+        data: Theme.of(context).copyWith(
+          splashFactory: NoSplash.splashFactory,
+        ),
+        child: Container(
+          margin: EdgeInsets.only(left: 4.w),
+          decoration: BoxDecoration(
+            color: "#F6F5F8".toColor(),
+            borderRadius: const BorderRadius.only(
+              topRight: Radius.circular(6),
+              bottomRight: Radius.circular(6),
+            ),
+          ),
+          child: ExpansionTile(
+            onExpansionChanged: (value) {
+              agenda.isExpanded.value = value;
+            },
+            shape: RoundedRectangleBorder(
+              borderRadius: BorderRadius.circular(4.0),
+              side: const BorderSide(color: Colors.transparent),
+            ),
+            collapsedShape: RoundedRectangleBorder(
+              borderRadius: BorderRadius.circular(4.0),
+              side: const BorderSide(color: Colors.transparent),
+            ),
+            minTileHeight: 46.h,
+            showTrailingIcon: false,
+            childrenPadding: EdgeInsets.zero,
+            tilePadding: EdgeInsets.zero,
+            title: Row(
+              children: [
+                SizedBox(width: 12.w),
+                Text(agenda.name.orEmpty,
+                    style: TextStyle(
+                        fontSize: 15.sp,
+                        color: ColorName.primaryTextColor,
+                        fontWeight: FontWeight.bold)),
+                const Spacer(),
+                Obx(() {
+                  return Row(
+                    children: [
+                      Text(
+                        '${agenda.list?.length}${StringName.talkTodoItem.tr}',
+                        style: TextStyle(
+                            fontSize: 15.sp,
+                            color: ColorName.secondaryTextColor),
+                      ),
+                      SizedBox(width: 3.w),
+                      SizedBox(
+                          width: 16.w,
+                          height: 16.w,
+                          child: agenda.isExpanded.value
+                              ? Assets.images.iconTalkExpand.image()
+                              : Assets.images.iconTalkCollapse.image()),
+                    ],
+                  );
+                }),
+                SizedBox(width: 12.w),
+              ],
+            ),
+            children: builder(agenda.list),
+          ),
+        ),
+      );
+    }),
+  );
+}
+
+typedef TodoItemClick = void Function(Agenda? agenda);
+
+List<Widget> getTalkAgendaSettingList(List<Agenda>? list,
+    {TodoItemClick? itemClick}) {
+  return list?.map((agenda) {
+        return Padding(
+          padding:
+              EdgeInsets.only(left: 12.w, right: 12.w, top: 2.h, bottom: 10.h),
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              Expanded(
+                child: Row(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Container(
+                        width: 8.w,
+                        height: 8.w,
+                        margin: EdgeInsets.only(top: 7.h),
+                        decoration: BoxDecoration(
+                          color: ColorName.colorPrimary.withOpacity(0.5),
+                          shape: BoxShape.circle,
+                        ),
+                      ),
+                      SizedBox(width: 4.w),
+                      Expanded(
+                        child: Text(agenda.content.orEmpty,
+                            style: TextStyle(
+                                fontSize: 14.sp,
+                                color: ColorName.primaryTextColor)),
+                      ),
+                    ]),
+              ),
+              SizedBox(width: 20.w),
+              GestureDetector(
+                onTap: () {
+                  itemClick?.call(agenda);
+                },
+                child: Obx(() {
+                  return Container(
+                    decoration: BoxDecoration(
+                      color: agenda.isSetMine.value
+                          ? "#E7E9F6".toColor()
+                          : ColorName.colorPrimary,
+                      borderRadius: BorderRadius.circular(6),
+                    ),
+                    padding:
+                        EdgeInsets.symmetric(horizontal: 9.w, vertical: 5.w),
+                    child: Text(
+                      agenda.isSetMine.value
+                          ? StringName.talkTodoCancelMine.tr
+                          : StringName.talkTodoSetMine.tr,
+                      style: TextStyle(
+                          fontSize: 13.sp,
+                          color: agenda.isSetMine.value
+                              ? ColorName.colorPrimary
+                              : Colors.white),
+                    ),
+                  );
+                }),
+              )
+            ],
+          ),
+        );
+      }).toList() ??
+      [];
+}
+
+List<Widget> getTalkAgendaNormalList(List<Agenda>? list) {
+  return list?.map((agenda) {
+        return Padding(
+          padding:
+              EdgeInsets.only(left: 12.w, right: 12.w, top: 2.h, bottom: 10.h),
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Container(
+                width: 8.w,
+                height: 8.w,
+                margin: EdgeInsets.only(top: 7.h),
+                decoration: BoxDecoration(
+                  color: ColorName.colorPrimary.withOpacity(0.5),
+                  shape: BoxShape.circle,
+                ),
+              ),
+              SizedBox(width: 4.w),
+              Expanded(
+                child: Text(agenda.content.orEmpty,
+                    style: TextStyle(
+                        fontSize: 14.sp, color: ColorName.primaryTextColor)),
+              ),
+            ],
+          ),
+        );
+      }).toList() ??
+      [];
+}

+ 87 - 8
lib/module/talk/controller.dart

@@ -1,14 +1,29 @@
-import 'dart:ui';
+import 'dart:async';
 
 import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:electronic_assistant/data/repositories/talk_repository.dart';
+import 'package:electronic_assistant/module/talk/summary/view.dart';
+import 'package:electronic_assistant/module/talk/todo/view.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
+import 'package:electronic_assistant/utils/toast_util.dart';
 import 'package:get/get.dart';
-import 'package:get/get_core/src/get_main.dart';
 
+import '../../data/bean/agenda_list_all_bean.dart';
 import '../../data/bean/talks.dart';
+import '../../data/repositories/agenda_repository.dart';
+import 'original/view.dart';
 
 class TalkController extends BaseController {
-  late TalkBean? talkBean;
+  final Rxn<TalkBean> talkBean = Rxn();
+
+  // final isOriginalAnalysed = false.obs;
+  final isShowElectricLow = false.obs;
+
+  final isAudioPlaying = false.obs;
+
+  final audioProgressValue = 0.0.obs;
+
+  final agendaAllList = <AgendaListAllBean>[].obs;
 
   final List<String> tabBeans = [
     StringName.talkTabSummary.tr,
@@ -16,13 +31,77 @@ class TalkController extends BaseController {
     StringName.talkTabOriginal.tr
   ];
 
+  late StreamSubscription<TalkBean?> _talkBeanListener;
+
+  final pages = [const SummaryView(), const TodoView(), const OriginalView()];
+
+  @override
+  void onReady() {
+    super.onReady();
+    _initListener();
+    _getArguments();
+  }
+
+  void _initListener() {
+    _talkBeanListener = talkBean.listen((bean) {
+      _dealTalkUpdate(bean);
+    });
+  }
+
+  void _dealTalkUpdate(TalkBean? bean) {}
+
+  void _getArguments() {
+    if (Get.arguments is TalkBean) {
+      talkBean.value = Get.arguments as TalkBean;
+    }
+  }
+
+  void checkCanAnalyze() {
+    String? id = talkBean.value?.id;
+    double? duration = talkBean.value?.duration;
+    if (id == null || duration == null) {
+      return;
+    }
+    talkRepository.checkElectric(duration).then((data) {
+      if (data.enough) {
+        //提交分析
+        _requestAnalyze();
+      } else {
+        ToastUtil.showToast(StringName.talkAnalyseLowToast.tr);
+        isShowElectricLow.value = true;
+      }
+    }).catchError((error) {
+      ToastUtil.showToast(error);
+    });
+  }
+
+  void _requestAnalyze() {
+    //提交文件
+  }
+
+  void goElectricStore() {
+    //TODO 跳转至商店页
+  }
+
+  void refreshAgendaAllData() {
+    String? id = talkBean.value?.id;
+    if (id == null || agendaAllList.isNotEmpty) {
+      return;
+    }
+    agendaRepository.agendaListAll(id).then((agenda) {
+      if (agenda.list != null) {
+        agendaAllList.value = agenda.list!;
+      }
+    });
+  }
+
   @override
-  void onInit() {
-    super.onInit();
-    getArguments();
+  void onClose() {
+    super.onClose();
+    _talkBeanListener?.cancel();
   }
 
-  void getArguments() {
-    Get.arguments is TalkBean ? Get.arguments as TalkBean : null;
+  void updateProgress(double value) {
+    audioProgressValue.value = value;
   }
 }

+ 44 - 1
lib/module/talk/original/controller.dart

@@ -1,4 +1,47 @@
+import 'dart:async';
+
 import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:electronic_assistant/data/repositories/talk_repository.dart';
 import 'package:get/get.dart';
 
-class OriginalController extends BaseController {}
+import '../../../data/bean/talk_original.dart';
+import '../../../data/bean/talks.dart';
+import '../controller.dart';
+
+class OriginalController extends BaseController {
+  TalkController talkController = Get.find();
+
+  final originalList = <TalkOriginal>[].obs;
+
+  StreamSubscription? _talkBeanListener;
+
+  @override
+  void onReady() {
+    super.onReady();
+    _talkBeanListener = talkController.talkBean.listen((bean) {
+      int? status = bean?.status;
+      if (status == null) {
+        return;
+      }
+      if (status == TalkStatus.analysisSuccess) {
+        requestOriginal();
+      }
+    });
+    requestOriginal();
+  }
+
+  void requestOriginal() {
+    if (originalList.isNotEmpty) {
+      return;
+    }
+    talkRepository.talkOriginal(talkController.talkBean.value?.id).then((value) {
+      originalList.value = value;
+    });
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+    _talkBeanListener?.cancel();
+  }
+}

+ 79 - 1
lib/module/talk/original/view.dart

@@ -1,6 +1,11 @@
 import 'package:electronic_assistant/base/base_page.dart';
+import 'package:electronic_assistant/resource/colors.gen.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
 
+import '../../../data/bean/talk_original.dart';
+import '../common_view.dart';
 import 'controller.dart';
 
 class OriginalView extends BasePage<OriginalController> {
@@ -8,6 +13,79 @@ class OriginalView extends BasePage<OriginalController> {
 
   @override
   Widget buildBody(BuildContext context) {
-    return Container();
+    return buildOriginalContentView();
+  }
+
+  Widget buildOriginalContentView() {
+    return Obx(() {
+      if (controller.originalList.isEmpty) {
+        return getTalkLoadingView();
+      } else {
+        return ListView.builder(
+          padding: EdgeInsets.only(bottom: 70.h),
+          itemBuilder: _buildOriginalItem,
+          itemCount: controller.originalList.length,
+        );
+      }
+    });
+  }
+
+  Widget _buildOriginalItem(BuildContext context, int index) {
+    TalkOriginal item = controller.originalList[index];
+    return Padding(
+      padding:
+          EdgeInsets.only(left: 12.w, right: 12.w, top: 11.h, bottom: 13.h),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Row(
+            children: [
+              Container(
+                decoration: const BoxDecoration(
+                  color: ColorName.colorPrimary,
+                  shape: BoxShape.circle,
+                ),
+                width: 20.w,
+                height: 20.w,
+                child: Center(
+                  child: Text(
+                    item.speakerId.toString(),
+                    style: TextStyle(fontSize: 12.sp, color: Colors.white),
+                  ),
+                ),
+              ),
+              SizedBox(width: 6.w),
+              Text(item.speaker.toString(),
+                  style: TextStyle(
+                      fontSize: 14.sp, color: ColorName.secondaryTextColor)),
+              SizedBox(width: 4.w),
+              Text(formatMilliseconds(item.startMs),
+                  style: TextStyle(
+                      fontSize: 12.sp, color: ColorName.tertiaryTextColor)),
+            ],
+          ),
+          SizedBox(height: 12.h),
+          Text(item.sentence.toString(),
+              style: TextStyle(
+                  fontSize: 14.sp, color: ColorName.primaryTextColor)),
+        ],
+      ),
+    );
+  }
+
+  String formatMilliseconds(int? totalMilliseconds) {
+    if (totalMilliseconds == null) {
+      return '';
+    }
+    int totalSeconds = (totalMilliseconds / 1000).round();
+    int hours = totalSeconds ~/ 3600;
+    int minutes = (totalSeconds % 3600) ~/ 60;
+    int seconds = totalSeconds % 60;
+
+    if (hours > 0) {
+      return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
+    } else {
+      return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
+    }
   }
 }

+ 40 - 4
lib/module/talk/summary/controller.dart

@@ -1,15 +1,51 @@
+import 'dart:async';
+
 import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:electronic_assistant/data/bean/talks.dart';
+import 'package:electronic_assistant/data/repositories/talk_repository.dart';
 import 'package:get/get.dart';
-
+import '../../../data/bean/agenda_list_all_bean.dart';
+import '../../../data/bean/talk_info.dart';
 import '../controller.dart';
 
 class SummaryController extends BaseController {
-  TalkController talkController = Get.find();
+  final TalkController talkController = Get.find();
+
+  final summaryBean = TalkInfo().obs;
+
+  StreamSubscription? _talkBeanListener;
+
+  RxList<AgendaListAllBean> get agendaAllList => talkController.agendaAllList;
 
   @override
   void onReady() {
     super.onReady();
+    _talkBeanListener = talkController.talkBean.listen((bean) {
+      _dealTalkUpdate(bean);
+    });
+    _dealTalkUpdate(talkController.talkBean.value);
   }
-}
 
-// enum Summar
+  void _dealTalkUpdate(TalkBean? bean) {
+    refreshSummaryData();
+  }
+
+  void refreshSummaryData() {
+    String? id = talkController.talkBean.value?.id;
+    if (id == null) {
+      return;
+    }
+    talkRepository.talkInfo(id).then((data) {
+      if (data.talkInfo != null) {
+        summaryBean.value = data.talkInfo!;
+      }
+    });
+    talkController.refreshAgendaAllData();
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+    _talkBeanListener?.cancel();
+  }
+}

+ 76 - 40
lib/module/talk/summary/view.dart

@@ -1,14 +1,13 @@
-import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:electronic_assistant/base/base_page.dart';
-import 'package:electronic_assistant/dialog/rename_dialog.dart';
-import 'package:electronic_assistant/resource/assets.gen.dart';
 import 'package:electronic_assistant/resource/colors.gen.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
+import 'package:electronic_assistant/utils/expand.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
-
-import '../../../utils/common_style.dart';
+import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
+import '../../../data/bean/talks.dart';
+import '../common_view.dart';
 import 'controller.dart';
 
 class SummaryView extends BasePage<SummaryController> {
@@ -16,41 +15,78 @@ class SummaryView extends BasePage<SummaryController> {
 
   @override
   Widget buildBody(BuildContext context) {
-    return Stack(
-      children: [
-        SizedBox(
-          width: double.infinity,
-          child: Column(
-            children: [
-              SizedBox(height: 119.h),
-              SizedBox(
-                  width: 100.w,
-                  height: 100.w,
-                  child: Assets.images.iconTalkSummaryUnanalyzed.image()),
-              SizedBox(height: 4.h),
-              Text(StringName.talkUnAnalyzed.tr,
-                  style: TextStyle(
-                      fontSize: 15.sp, color: ColorName.primaryTextColor)),
-              SizedBox(height: 2.h),
-              Text(StringName.talkUnAnalyzedTips.tr,
-                  style: TextStyle(
-                      fontSize: 12.sp, color: ColorName.secondaryTextColor)),
-              SizedBox(height: 24.h),
-              Container(
-                decoration: getPrimaryBtnDecoration(8),
-                width: 240.w,
-                height: 48.w,
-                child: Center(
-                  child: Text(
-                    StringName.talkAnalyzedBtnTxt.tr,
-                    style: TextStyle(fontSize: 16.sp, color: ColorName.white),
-                  ),
-                ),
-              )
-            ],
-          ),
-        ),
-      ],
+    return Obx(() {
+      return buildSummaryStatusView();
+    });
+  }
+
+  Widget buildSummaryView() {
+    return Padding(
+        padding:
+            EdgeInsets.only(left: 12.w, right: 12.w, top: 16.h, bottom: 20.h),
+        child: Obx(() {
+          return Text(
+            controller.summaryBean.value.summary.orEmpty,
+            style:
+                TextStyle(fontSize: 14.sp, color: ColorName.primaryTextColor),
+          );
+        }));
+  }
+
+  Widget buildTemplateView() {
+    return Container();
+  }
+
+  buildAllTaskView() {
+    return Padding(
+      padding:
+          EdgeInsets.only(left: 12.w, right: 12.w, top: 20.h, bottom: 16.h),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(StringName.homeTalkTodoTitle.tr,
+              style: TextStyle(
+                  fontWeight: FontWeight.bold,
+                  color: ColorName.primaryTextColor,
+                  fontSize: 15.sp)),
+          SizedBox(height: 12.h),
+          buildTodoView()
+        ],
+      ),
     );
   }
+
+  Widget buildTodoView() {
+    return Obx(() {
+      return Column(
+          children: controller.agendaAllList.map((agenda) {
+        return Padding(
+            padding: EdgeInsets.only(bottom: 8.h),
+            child: getTodoItemView(
+                agenda, (list) => getTalkAgendaNormalList(list)));
+      }).toList());
+    });
+  }
+
+  Widget buildSummaryStatusView() {
+    if (controller.summaryBean.value.status == TalkStatus.analysisFail) {
+      return getTalkFailView();
+    } else if (controller.summaryBean.value.status ==
+        TalkStatus.analysisSuccess) {
+      return ListView(padding: EdgeInsets.only(bottom: 150.h), children: [
+        // buildTemplateView(),
+        buildSummaryView(),
+        Container(
+          height: 6.h,
+          color: "#F6F6F6".toColor(),
+        ),
+        buildAllTaskView(),
+      ]);
+    } else if (controller.summaryBean.value.status == TalkStatus.analysing ||
+        controller.summaryBean.value.status == TalkStatus.waitAnalysis) {
+      return getTalkLoadingView();
+    } else {
+      return Container();
+    }
+  }
 }

+ 75 - 1
lib/module/talk/todo/controller.dart

@@ -1,4 +1,78 @@
+import 'dart:async';
+
 import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:electronic_assistant/data/bean/agenda.dart';
+import 'package:electronic_assistant/data/repositories/agenda_repository.dart';
+import 'package:electronic_assistant/utils/error_handler.dart';
 import 'package:get/get.dart';
 
-class TodoController extends BaseController {}
+import '../../../data/bean/agenda_list_all_bean.dart';
+import '../../../data/bean/talks.dart';
+import '../controller.dart';
+
+class TodoController extends BaseController {
+  final TalkController _talkController = Get.find();
+
+  StreamSubscription? _talkBeanListener;
+
+  final agendaMineList = <Agenda>[].obs;
+
+  final mineAgendaIsExpanded = false.obs;
+
+  RxList<AgendaListAllBean> get agendaAllList => _talkController.agendaAllList;
+
+  Rxn<TalkBean> get talkBean => _talkController.talkBean;
+
+  @override
+  void onReady() {
+    super.onReady();
+    _talkBeanListener = _talkController.talkBean.listen((bean) {
+      _dealTalkUpdate(bean);
+    });
+    _dealTalkUpdate(_talkController.talkBean.value);
+  }
+
+  void _dealTalkUpdate(TalkBean? bean) {
+    int? status = bean?.status;
+    if (status == null) {
+      return;
+    }
+    if (status == TalkStatus.analysisSuccess) {
+      _talkController.refreshAgendaAllData();
+      requestMineTodoData();
+    }
+  }
+
+  void requestMineTodoData() {
+    String? id = _talkController.talkBean.value?.id;
+    if (id == null) {
+      return;
+    }
+    agendaRepository.agendaListMine(id).then((data) {
+      agendaMineList.value = data.list ?? [];
+    });
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+    _talkBeanListener?.cancel();
+  }
+
+  void agendaTodo(Agenda? agenda) {
+    if (agenda == null || agenda.id == null) {
+      return;
+    }
+    bool isSetTodo = !agenda.isSetMine.value;
+    agendaRepository.agendaTodo(agenda.id!, isSetTodo).then((value) {
+      agenda.isSetMine.value = isSetTodo;
+      requestMineTodoData();
+    }).catchError((error) {
+      ErrorHandler.toastError(error);
+    });
+  }
+
+  void onClickThinking(Agenda agenda) {
+    //TODO 跳转至小听聊天界面
+  }
+}

+ 167 - 1
lib/module/talk/todo/view.dart

@@ -1,6 +1,19 @@
+import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:electronic_assistant/base/base_page.dart';
+import 'package:electronic_assistant/dialog/rename_dialog.dart';
+import 'package:electronic_assistant/resource/colors.gen.dart';
+import 'package:electronic_assistant/resource/string.gen.dart';
+import 'package:electronic_assistant/utils/expand.dart';
+import 'package:electronic_assistant/utils/toast_util.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
 
+import '../../../data/bean/agenda.dart';
+import '../../../data/bean/talks.dart';
+import '../../../resource/assets.gen.dart';
+import '../../../utils/common_style.dart';
+import '../common_view.dart';
 import 'controller.dart';
 
 class TodoView extends BasePage<TodoController> {
@@ -8,6 +21,159 @@ class TodoView extends BasePage<TodoController> {
 
   @override
   Widget buildBody(BuildContext context) {
-    return Container();
+    return Obx(() {
+      return _buildTodoStatusView();
+    });
+  }
+
+  _buildAllTaskView() {
+    return Padding(
+      padding: EdgeInsets.only(top: 6.h, bottom: 16.h),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(StringName.talkTodoAll.tr,
+              style: TextStyle(
+                  fontSize: 14.sp, color: ColorName.secondaryTextColor)),
+          SizedBox(height: 12.h),
+          buildAllTodoView(),
+        ],
+      ),
+    );
+  }
+
+  Widget buildAllTodoView() {
+    return Obx(() {
+      return Column(
+          children: controller.agendaAllList.map((agenda) {
+        return Padding(
+          padding: EdgeInsets.only(bottom: 8.h),
+          child: getTodoItemView(
+              agenda,
+              (list) => getTalkAgendaSettingList(list, itemClick: (agenda) {
+                    controller.agendaTodo(agenda);
+                  })),
+        );
+      }).toList());
+    });
+  }
+
+  Widget _buildMineTodoList() {
+    return Builder(builder: (context) {
+      return Theme(
+        data: Theme.of(context).copyWith(
+          splashFactory: NoSplash.splashFactory,
+        ),
+        child: Obx(() {
+          return ExpansionTile(
+            onExpansionChanged: (value) {
+              controller.mineAgendaIsExpanded.value = value;
+            },
+            shape: RoundedRectangleBorder(
+              borderRadius: BorderRadius.circular(4.0),
+              side: const BorderSide(color: Colors.transparent),
+            ),
+            collapsedShape: RoundedRectangleBorder(
+              borderRadius: BorderRadius.circular(4.0),
+              side: const BorderSide(color: Colors.transparent),
+            ),
+            minTileHeight: 46.h,
+            showTrailingIcon: false,
+            childrenPadding: EdgeInsets.zero,
+            tilePadding: EdgeInsets.zero,
+            title: Row(
+              children: [
+                Text(StringName.talkTabMyTask.tr,
+                    style: TextStyle(
+                        fontSize: 14.sp, color: ColorName.secondaryTextColor)),
+                const Spacer(),
+                Obx(() {
+                  return Row(
+                    children: [
+                      Text(
+                        '${controller.agendaMineList.length}${StringName.talkTodoItem.tr}',
+                        style: TextStyle(
+                            fontSize: 15.sp,
+                            color: ColorName.secondaryTextColor),
+                      ),
+                      SizedBox(width: 3.w),
+                      SizedBox(
+                          width: 16.w,
+                          height: 16.w,
+                          child: controller.mineAgendaIsExpanded.value
+                              ? Assets.images.iconTalkExpand.image()
+                              : Assets.images.iconTalkCollapse.image()),
+                    ],
+                  );
+                }),
+                SizedBox(width: 12.w),
+              ],
+            ),
+            children: controller.agendaMineList.map((agenda) {
+              return _buildMineItem(agenda);
+            }).toList(),
+          );
+        }),
+      );
+    });
+  }
+
+  Widget _buildMineItem(Agenda agenda) {
+    return Container(
+      margin: EdgeInsets.only(bottom: 8.w),
+      decoration: BoxDecoration(
+        borderRadius: BorderRadius.circular(8.0),
+        border: Border.all(
+          color: '#F6F6F6'.toColor(),
+          width: 2,
+        ),
+      ),
+      padding: EdgeInsets.symmetric(vertical: 17.w, horizontal: 12.w),
+      child: Row(
+        children: [
+          Expanded(
+            child: Padding(
+              padding: const EdgeInsets.only(right: 12).w,
+              child: Text(agenda.content ?? '',
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
+                  style: TextStyle(
+                      fontSize: 15.sp,
+                      fontWeight: FontWeight.bold,
+                      color: ColorName.primaryTextColor)),
+            ),
+          ),
+          GestureDetector(
+            onTap: () {
+              controller.onClickThinking(agenda);
+            },
+            child: Container(
+              decoration: getPrimaryBtnDecoration(6),
+              padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4).w,
+              child: Text(
+                StringName.homeTalkThinking.tr,
+                style: TextStyle(fontSize: 13.sp, color: ColorName.white),
+              ),
+            ),
+          )
+        ],
+      ),
+    );
+  }
+
+  Widget _buildTodoStatusView() {
+    if (controller.talkBean.value?.status == TalkStatus.analysisFail) {
+      return getTalkFailView();
+    } else if (controller.talkBean.value?.status == TalkStatus.analysing ||
+        controller.talkBean.value?.status == TalkStatus.waitAnalysis) {
+      return getTalkLoadingView();
+    } else if (controller.talkBean.value?.status == TalkStatus.analysisSuccess) {
+      return ListView(
+        padding: EdgeInsets.only(left: 12.w, right: 12.w),
+        children: [_buildMineTodoList(), _buildAllTaskView()],
+      );
+    } else {
+      return Container();
+    }
   }
 }

+ 334 - 78
lib/module/talk/view.dart

@@ -1,40 +1,35 @@
 import 'package:electronic_assistant/base/base_page.dart';
 import 'package:electronic_assistant/module/talk/controller.dart';
-import 'package:electronic_assistant/module/talk/original/view.dart';
-import 'package:electronic_assistant/module/talk/summary/view.dart';
-import 'package:electronic_assistant/module/talk/todo/view.dart';
 import 'package:electronic_assistant/resource/colors.gen.dart';
 import 'package:electronic_assistant/utils/expand.dart';
 import 'package:electronic_assistant/utils/fixed_size_tab_indicator.dart';
-import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
-import 'package:get/get_core/src/get_main.dart';
-
 import '../../data/bean/talks.dart';
 import '../../resource/assets.gen.dart';
+import '../../resource/string.gen.dart';
 import '../../router/app_pages.dart';
+import '../../utils/common_style.dart';
 
 class TalkPage extends BasePage<TalkController> {
-  TalkPage({super.key});
+  const TalkPage({super.key});
 
   static void start(TalkBean item) {
     Get.toNamed(RoutePath.talkDetail, arguments: item);
   }
 
-  final pages = [SummaryView(), TodoView(), OriginalView()];
-
   @override
   Widget buildBody(BuildContext context) {
-    return Stack(
-      children: [
-        buildTopGradient(),
-        Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            AppBar(
+    return DefaultTabController(
+      length: controller.tabBeans.length,
+      child: Stack(
+        children: [
+          buildTopGradient(),
+          Scaffold(
+            backgroundColor: Colors.transparent,
+            appBar: AppBar(
               systemOverlayStyle: SystemUiOverlayStyle.dark,
               backgroundColor: Colors.transparent,
               leading: IconButton(
@@ -48,70 +43,70 @@ class TalkPage extends BasePage<TalkController> {
                 },
               ),
             ),
-            Padding(
-              padding: EdgeInsets.symmetric(horizontal: 12.w),
-              child: Column(
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  SizedBox(height: 8.h),
-                  Text('8月16日10时53分录音',
-                      style: TextStyle(
-                          fontSize: 22.sp,
-                          fontWeight: FontWeight.bold,
-                          color: ColorName.primaryTextColor)),
-                  SizedBox(height: 4.h),
-                  Text('2024-07-25  16:31',
-                      style: TextStyle(
-                          fontSize: 12.sp,
-                          color: ColorName.secondaryTextColor)),
-                  SizedBox(height: 14.h),
-                ],
-              ),
-            ),
-            Expanded(
-                child: DefaultTabController(
-                    length: controller.tabBeans.length,
-                    child: Column(
+            body: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Padding(
+                  padding: EdgeInsets.symmetric(horizontal: 12.w),
+                  child: Obx(() {
+                    return Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
                       children: [
-                        Container(
-                          decoration: const BoxDecoration(
-                              color: Colors.white,
-                              borderRadius: BorderRadius.only(
-                                topLeft: Radius.circular(12),
-                                topRight: Radius.circular(12),
-                              )),
-                          child: TabBar(
-                              labelStyle: TextStyle(
-                                  fontSize: 16.sp, fontWeight: FontWeight.bold),
-                              unselectedLabelStyle: TextStyle(fontSize: 14.sp),
-                              labelColor: ColorName.primaryTextColor,
-                              unselectedLabelColor:
-                                  ColorName.secondaryTextColor,
-                              labelPadding: EdgeInsets.only(top: 4.h),
-                              dividerHeight: 0,
-                              indicator: FixedSizeTabIndicator(
-                                  width: 16.w,
-                                  height: 3.w,
-                                  radius: 3,
-                                  color: ColorName.colorPrimary),
-                              tabs: controller.tabBeans
-                                  .map((txt) => Tab(text: txt))
-                                  .toList()),
-                        ),
-                        Divider(
-                            height: 1,
-                            color: const Color(0xFFf6f6f6),
-                            indent: 12.w,
-                            endIndent: 12.w),
-                        Expanded(
-                            child: TabBarView(
-                          children: pages,
-                        ))
+                        SizedBox(height: 8.h),
+                        Text(controller.talkBean.value?.title ?? '',
+                            style: TextStyle(
+                                fontSize: 22.sp,
+                                fontWeight: FontWeight.bold,
+                                color: ColorName.primaryTextColor)),
+                        SizedBox(height: 4.h),
+                        Text(controller.talkBean.value?.createTime ?? '',
+                            style: TextStyle(
+                                fontSize: 12.sp,
+                                color: ColorName.secondaryTextColor)),
+                        SizedBox(height: 14.h),
                       ],
-                    )))
-          ],
-        )
-      ],
+                    );
+                  }),
+                ),
+                buildTabBar(),
+                Divider(
+                    height: 1,
+                    color: const Color(0xFFf6f6f6),
+                    indent: 12.w,
+                    endIndent: 12.w),
+                SizedBox(height: 8.h),
+                buildTalkContentView()
+              ],
+            ),
+          ),
+          buildBottomView(),
+          buildAIAnalysisView()
+        ],
+      ),
+    );
+  }
+
+  Container buildTabBar() {
+    return Container(
+      decoration: const BoxDecoration(
+          color: Colors.white,
+          borderRadius: BorderRadius.only(
+            topLeft: Radius.circular(12),
+            topRight: Radius.circular(12),
+          )),
+      child: TabBar(
+          labelStyle: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
+          unselectedLabelStyle: TextStyle(fontSize: 14.sp),
+          labelColor: ColorName.primaryTextColor,
+          unselectedLabelColor: ColorName.secondaryTextColor,
+          labelPadding: EdgeInsets.only(top: 4.h),
+          dividerHeight: 0,
+          indicator: FixedSizeTabIndicator(
+              width: 16.w,
+              height: 3.w,
+              radius: 3,
+              color: ColorName.colorPrimary),
+          tabs: controller.tabBeans.map((txt) => Tab(text: txt)).toList()),
     );
   }
 
@@ -133,4 +128,265 @@ class TalkPage extends BasePage<TalkController> {
           ),
         ));
   }
+
+  Widget buildTalkContentView() {
+    return Obx(() {
+      if (controller.talkBean.value?.status == TalkStatus.notAnalysis) {
+        if (controller.isShowElectricLow.value) {
+          return buildElectricLowView();
+        } else {
+          return buildNotAnalysisView();
+        }
+      } else {
+        return buildTabContentView();
+      }
+    });
+  }
+
+  Widget buildTabContentView() {
+    return Expanded(
+      child: TabBarView(
+        children: controller.pages,
+      ),
+    );
+  }
+
+  Widget buildNotAnalysisView() {
+    return SizedBox(
+      width: double.infinity,
+      child: Column(
+        children: [
+          SizedBox(height: 119.h),
+          SizedBox(
+              width: 100.w,
+              height: 100.w,
+              child: Assets.images.iconTalkSummaryUnanalyzed.image()),
+          SizedBox(height: 4.h),
+          Text(StringName.talkUnAnalyzed.tr,
+              style: TextStyle(
+                  fontSize: 15.sp, color: ColorName.primaryTextColor)),
+          SizedBox(height: 2.h),
+          Text(StringName.talkUnAnalyzedTips.tr,
+              style: TextStyle(
+                  fontSize: 12.sp, color: ColorName.secondaryTextColor)),
+          SizedBox(height: 24.h),
+          GestureDetector(
+            onTap: () {
+              controller.checkCanAnalyze();
+            },
+            child: Container(
+              decoration: getPrimaryBtnDecoration(8),
+              width: 240.w,
+              height: 48.w,
+              child: Center(
+                child: Text(
+                  StringName.talkAnalyzedBtnTxt.tr,
+                  style: TextStyle(fontSize: 16.sp, color: ColorName.white),
+                ),
+              ),
+            ),
+          )
+        ],
+      ),
+    );
+  }
+
+  Widget buildElectricLowView() {
+    return Container(
+      color: const Color(0xFFDFE4FC),
+      padding: EdgeInsets.only(left: 16.w, right: 12.w, top: 8.h, bottom: 8.h),
+      child: Row(
+        children: [
+          SizedBox(
+              width: 46.w,
+              height: 56.w,
+              child: Assets.images.iconTalkElectricLow.image()),
+          SizedBox(width: 10.w),
+          IntrinsicHeight(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                SizedBox(
+                    width: 90.w,
+                    height: 21.w,
+                    child: Assets.images.iconTalkElectricLowTxt.image()),
+                SizedBox(width: 1.w),
+                Text(StringName.talkElectricLow.tr,
+                    style: TextStyle(
+                        fontSize: 12.sp, color: ColorName.secondaryTextColor)),
+              ],
+            ),
+          ),
+          const Spacer(),
+          GestureDetector(
+            onTap: () {
+              controller.goElectricStore();
+            },
+            child: Container(
+                decoration: getPrimaryBtnDecoration(8),
+                width: 100.w,
+                height: 36.w,
+                child: Center(
+                  child: Text(StringName.talkGoStore.tr,
+                      style:
+                          TextStyle(fontSize: 16.sp, color: ColorName.white)),
+                )),
+          )
+        ],
+      ),
+    );
+  }
+
+  Widget buildBottomView() {
+    return Align(
+      alignment: Alignment.bottomCenter,
+      child: Container(
+        margin: EdgeInsets.only(bottom: 20.h), // 设置底部偏移距离
+        child: IntrinsicHeight(
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.end,
+            children: [
+              Container(
+                  margin: EdgeInsets.only(right: 8.w),
+                  width: 64.w,
+                  height: 64.w,
+                  child: Assets.images.iconTalkLogo.image()),
+              SizedBox(height: 24.h),
+              buildAudioView()
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  buildAIAnalysisView() {
+    return Container();
+  }
+
+  buildAudioView() {
+    return Container(
+      decoration: BoxDecoration(
+        color: Colors.white,
+        borderRadius: BorderRadius.circular(100),
+        boxShadow: [
+          BoxShadow(
+            color: ColorName.black5.withOpacity(0.1),
+            spreadRadius: 2,
+            blurRadius: 12,
+            offset: const Offset(0, 3),
+          ),
+        ],
+      ),
+      margin: EdgeInsets.symmetric(horizontal: 12.w),
+      padding: EdgeInsets.symmetric(vertical: 6.w),
+      child: Row(
+        children: [
+          SizedBox(width: 9.w),
+          GestureDetector(
+            onTap: () {
+              // controller.playAudio();
+            },
+            child: Obx(() {
+              return SizedBox(
+                  width: 36.w,
+                  height: 36.w,
+                  child: (controller.isAudioPlaying.value)
+                      ? Assets.images.iconTalkAudioPlaying.image()
+                      : Assets.images.iconTalkAudioPause.image());
+            }),
+          ),
+          SizedBox(width: 15.w),
+          Builder(builder: (context) {
+            return SizedBox(
+              width: 226.w,
+              height: 18.w,
+              child: Obx(() {
+                return SliderTheme(
+                  data: SliderTheme.of(context).copyWith(
+                    thumbColor: Colors.white,
+                    overlayShape: SliderComponentShape.noOverlay,
+                    trackHeight: 8,
+                    activeTrackColor: "#8A89E9".toColor(),
+                    inactiveTrackColor: "#F6F5F8".toColor(),
+                    trackShape: CustomTrackShape(),
+                  ),
+                  child: Slider(
+                    value: controller.audioProgressValue.value,
+                    min: 0.0,
+                    max: 1.0,
+                    onChanged: (value) {
+                      controller.updateProgress(value);
+                    },
+                  ),
+                );
+              }),
+            );
+          }),
+          SizedBox(width: 11.w),
+          Text('3:21',
+              style: TextStyle(
+                  fontSize: 10.sp, color: ColorName.secondaryTextColor))
+        ],
+      ),
+    );
+  }
 }
+
+class CustomTrackShape extends RoundedRectSliderTrackShape {
+  @override
+  Rect getPreferredRect({
+    required RenderBox parentBox,
+    Offset offset = Offset.zero,
+    required SliderThemeData sliderTheme,
+    bool isEnabled = false,
+    bool isDiscrete = false,
+  }) {
+    final trackHeight = sliderTheme.trackHeight;
+    final trackLeft = offset.dx;
+    final trackTop = offset.dy + (parentBox.size.height - trackHeight!) / 2;
+    final trackWidth = parentBox.size.width;
+    return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
+  }
+}
+
+// class EnhancedShadowSliderThumbShape extends RoundSliderThumbShape {
+//   final double thumbRadius;
+//   final double elevation;
+//   final double shadowOffsetY;
+//
+//   EnhancedShadowSliderThumbShape({
+//     required this.thumbRadius,
+//     this.elevation = 8.0,
+//     this.shadowOffsetY = 0, // 阴影向下偏移量
+//   });
+//
+//   @override
+//   void paint(
+//       PaintingContext context,
+//       Offset center, {
+//         required Animation<double> activationAnimation,
+//         required Animation<double> enableAnimation,
+//         required bool isDiscrete,
+//         required TextPainter labelPainter,
+//         required RenderBox parentBox,
+//         required SliderThemeData sliderTheme,
+//         required TextDirection textDirection,
+//         required double value,
+//         required double textScaleFactor,
+//         required Size sizeWithOverflow,
+//       }) {
+//     final Canvas canvas = context.canvas;
+//     final Paint shadowPaint = Paint()
+//       ..color = Colors.black.withOpacity(0.25)
+//       ..maskFilter = MaskFilter.blur(BlurStyle.normal, elevation);
+//
+//     final Paint thumbPaint = Paint()
+//       ..color = sliderTheme.thumbColor!
+//       ..style = PaintingStyle.fill;
+//
+//     // 调整阴影的绘制位置,使其向下偏移
+//     canvas.drawCircle(center.translate(0, shadowOffsetY), thumbRadius + 2, shadowPaint);
+//     canvas.drawCircle(center, thumbRadius, thumbPaint);
+//   }
+// }

+ 1 - 1
lib/module/task/controller.dart

@@ -1,5 +1,5 @@
 import 'package:electronic_assistant/base/base_controller.dart';
-import 'package:electronic_assistant/data/repositories/task_repository.dart';
+import 'package:electronic_assistant/data/repositories/talk_repository.dart';
 
 class TaskController extends BaseController {
   String get filterTxt => '展示近两周待办';

+ 1 - 1
lib/module/task/view.dart

@@ -190,7 +190,7 @@ class TaskPage extends BasePage<TaskController> {
               SizedBox(
                   width: 16.w,
                   height: 16.h,
-                  child: Assets.images.iconTaskArrow.image())
+                  child: Assets.images.iconTalkCollapse.image())
             ],
           )
         ],

+ 8 - 0
lib/router/app_pages.dart

@@ -2,6 +2,8 @@ import 'package:electronic_assistant/module/main/controller.dart';
 import 'package:electronic_assistant/module/store/controller.dart';
 import 'package:electronic_assistant/module/store/view.dart';
 import 'package:electronic_assistant/module/talk/controller.dart';
+import 'package:electronic_assistant/module/talk/summary/controller.dart';
+import 'package:electronic_assistant/module/talk/todo/controller.dart';
 import 'package:electronic_assistant/module/talk/view.dart';
 import 'package:electronic_assistant/module/record/controller.dart';
 import 'package:electronic_assistant/module/task/search/task_search.dart';
@@ -18,6 +20,7 @@ import '../module/login/view.dart';
 import '../module/main/view.dart';
 import '../module/record/view.dart';
 import '../module/splash/view.dart';
+import '../module/talk/original/controller.dart';
 import '../module/task/controller.dart';
 
 abstract class AppPage {
@@ -60,6 +63,9 @@ class AppBinding extends Bindings {
     lazyPut(() => ChatController());
     lazyPut(() => TalkController());
     lazyPut(() => RecordController());
+    lazyPut(() => OriginalController());
+    lazyPut(() => SummaryController());
+    lazyPut(() => TodoController());
     lazyPut(() => StoreController());
   }
 
@@ -78,6 +84,8 @@ final generalPages = [
   GetPage(name: RoutePath.task, page: () => const TaskPage()),
   GetPage(name: RoutePath.taskSearch, page: () => const TaskSearchPage()),
   GetPage(name: RoutePath.record, page: () => const RecordPage()),
+  GetPage(name: RoutePath.talkDetail, page: () => const TalkPage()),
+  GetPage(name: RoutePath.record, page: () =>  const RecordPage()),
   GetPage(name: RoutePath.talkDetail, page: () => TalkPage()),
   GetPage(name: RoutePath.store, page: () => const StorePage()),
 ];

+ 3 - 0
lib/utils/common_utils.dart

@@ -0,0 +1,3 @@
+bool getBoolValue(bool? value) {
+  return value ?? false;
+}

+ 8 - 3
lib/utils/expand.dart

@@ -1,9 +1,14 @@
 import 'dart:ui';
 
 extension HexColor on String {
+  Color get color => toColor();
+
   Color toColor() {
-    final hexCode = replaceAll('#', '');
-    return Color(int.parse('FF$hexCode', radix: 16));
+    String hex = replaceAll('#', '');
+    if (hex.length == 6) {
+      hex = 'FF$hex';
+    }
+    return Color(int.parse(hex, radix: 16));
   }
 }
 
@@ -48,4 +53,4 @@ extension StringExtensions on String {
     }
     return result;
   }
-}
+}

+ 63 - 0
lib/utils/pcm_wav_converter.dart

@@ -0,0 +1,63 @@
+import 'dart:io';
+
+class PcmWavConverter {
+  static convert(File pcmFile, File wavFile, int sampleRate, int channels,
+      int bitDepth) {
+    int dataLength = pcmFile.lengthSync();
+    List<int> wavHeader = _createWavHeader(dataLength, sampleRate, channels, bitDepth);
+    wavFile.writeAsBytesSync(wavHeader, mode: FileMode.write);
+    wavFile.writeAsBytesSync(pcmFile.readAsBytesSync(), mode: FileMode.append);
+  }
+
+  static List<int> _createWavHeader(int dataLength, int sampleRate,
+      int channels, int bitDepth) {
+    int byteRate = sampleRate * channels * bitDepth ~/ 8;
+    int wavSize = dataLength + 36;
+    List<int> header = List<int>.filled(44, 0);
+    header[0] = 'R'.codeUnitAt(0);
+    header[1] = 'I'.codeUnitAt(0);
+    header[2] = 'F'.codeUnitAt(0);
+    header[3] = 'F'.codeUnitAt(0);
+    header[4] = wavSize & 0xff;
+    header[5] = ((wavSize) >> 8) & 0xff;
+    header[6] = ((wavSize) >> 16) & 0xff;
+    header[7] = ((wavSize) >> 24) & 0xff;
+    header[8] = 'W'.codeUnitAt(0);
+    header[9] = 'A'.codeUnitAt(0);
+    header[10] = 'V'.codeUnitAt(0);
+    header[11] = 'E'.codeUnitAt(0);
+    header[12] = 'f'.codeUnitAt(0);
+    header[13] = 'm'.codeUnitAt(0);
+    header[14] = 't'.codeUnitAt(0);
+    header[15] = ' '.codeUnitAt(0);
+    header[16] = 16;
+    header[17] = 0;
+    header[18] = 0;
+    header[19] = 0;
+    header[20] = 1;
+    header[21] = 0;
+    header[22] = channels & 0xff;
+    header[23] = (channels >> 8) & 0xff;
+    header[24] = sampleRate & 0xff;
+    header[25] = (sampleRate >> 8) & 0xff;
+    header[26] = (sampleRate >> 16) & 0xff;
+    header[27] = (sampleRate >> 24) & 0xff;
+    header[28] = byteRate & 0xff;
+    header[29] = (byteRate >> 8) & 0xff;
+    header[30] = (byteRate >> 16) & 0xff;
+    header[31] = (byteRate >> 24) & 0xff;
+    header[32] = (channels * bitDepth ~/ 8) & 0xff;
+    header[33] = 0;
+    header[34] = bitDepth;
+    header[35] = 0;
+    header[36] = 'd'.codeUnitAt(0);
+    header[37] = 'a'.codeUnitAt(0);
+    header[38] = 't'.codeUnitAt(0);
+    header[39] = 'a'.codeUnitAt(0);
+    header[40] = dataLength & 0xff;
+    header[41] = (dataLength >> 8) & 0xff;
+    header[42] = (dataLength >> 16) & 0xff;
+    header[43] = (dataLength >> 24) & 0xff;
+    return header;
+  }
+}

+ 0 - 0
lib/utils/popup_util.dart


+ 18 - 4
lib/widget/frame_animation_view.dart

@@ -38,10 +38,11 @@ class FrameAnimationView extends StatefulWidget {
 }
 
 class FrameAnimationViewState extends State<FrameAnimationView> {
-  int _currentFrame = 0;
-  Timer? _timer;
+  int _currentFrame = -1;
   List<File> imageFiles = [];
   List<ui.Image> images = [];
+  Timer? _timer;
+  StreamSubscription? _precacheStreamSubscription;
 
   @override
   void initState() {
@@ -60,11 +61,20 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
   void dispose() {
     super.dispose();
     _timer?.cancel();
+    _precacheStreamSubscription?.cancel();
+    imageFiles.clear();
+    for (var image in images) {
+      image.dispose();
+    }
+    images.clear();
   }
 
   @override
   Widget build(BuildContext context) {
-    if (images.isEmpty) {
+    if (_timer == null ||
+        _currentFrame < 0 ||
+        images.isEmpty ||
+        _currentFrame >= images.length) {
       return SizedBox(width: widget.width, height: widget.height);
     }
     return RawImage(
@@ -191,6 +201,9 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
 
     _timer = Timer.periodic(
         Duration(milliseconds: 1000 ~/ (widget.frameRate * speed)), (_) {
+      if (images.isEmpty) {
+        return;
+      }
       setState(() {
         int targetFrame = (_currentFrame + 1) % imageFiles.length;
         _currentFrame =
@@ -218,7 +231,8 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
   }
 
   precacheImageFiles() {
-    Stream<File>.fromIterable(imageFiles).asyncMap((file) async {
+    _precacheStreamSubscription =
+        Stream<File>.fromIterable(imageFiles).asyncMap((file) async {
       final Uint8List bytes = await file.readAsBytes();
       final ui.Codec codec = await ui.instantiateImageCodec(bytes);
       final ui.FrameInfo frameInfo = await codec.getNextFrame();

+ 89 - 12
pubspec.lock

@@ -315,6 +315,11 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
   frontend_server_client:
     dependency: transitive
     description:
@@ -431,18 +436,18 @@ packages:
     dependency: transitive
     description:
       name: leak_tracker
-      sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
+      sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
       url: "https://pub.dev"
     source: hosted
-    version: "10.0.4"
+    version: "10.0.5"
   leak_tracker_flutter_testing:
     dependency: transitive
     description:
       name: leak_tracker_flutter_testing
-      sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
+      sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.3"
+    version: "3.0.5"
   leak_tracker_testing:
     dependency: transitive
     description:
@@ -487,18 +492,18 @@ packages:
     dependency: transitive
     description:
       name: material_color_utilities
-      sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
+      sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
       url: "https://pub.dev"
     source: hosted
-    version: "0.8.0"
+    version: "0.11.1"
   meta:
     dependency: transitive
     description:
       name: meta
-      sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
+      sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
       url: "https://pub.dev"
     source: hosted
-    version: "1.12.0"
+    version: "1.15.0"
   mime:
     dependency: transitive
     description:
@@ -691,6 +696,62 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.0.0"
+  record:
+    dependency: "direct main"
+    description:
+      name: record
+      sha256: "4a5cf4d083d1ee49e0878823c4397d073f8eb0a775f31215d388e2bc47a9e867"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.1.2"
+  record_android:
+    dependency: transitive
+    description:
+      name: record_android
+      sha256: d7af0b3119725a0f561817c72b5f5eca4d7a76d441deef519ae04e4824c0734c
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.6"
+  record_darwin:
+    dependency: transitive
+    description:
+      name: record_darwin
+      sha256: fe90d302acb1f3cee1ade5df9c150ca5cee33b48d8cdf1cf433bf577d7f00134
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.2"
+  record_linux:
+    dependency: transitive
+    description:
+      name: record_linux
+      sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.2"
+  record_platform_interface:
+    dependency: transitive
+    description:
+      name: record_platform_interface
+      sha256: "11f8b03ea8a0e279b0e306571dbe0db0202c0b8e866495c9fa1ad2281d5e4c15"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  record_web:
+    dependency: transitive
+    description:
+      name: record_web
+      sha256: "656b7a865f90651fab997c2a563364f5fd60a0b527d5dadbb915d62d84fc3867"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.3"
+  record_windows:
+    dependency: transitive
+    description:
+      name: record_windows
+      sha256: e653555aa3fda168aded7c34e11bd82baf0c6ac84e7624553def3c77ffefd36f
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.3"
   retrofit:
     dependency: "direct main"
     description:
@@ -752,6 +813,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.10.0"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
   stack_trace:
     dependency: transitive
     description:
@@ -796,10 +865,10 @@ packages:
     dependency: transitive
     description:
       name: test_api
-      sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
+      sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
       url: "https://pub.dev"
     source: hosted
-    version: "0.7.0"
+    version: "0.7.2"
   time:
     dependency: transitive
     description:
@@ -832,6 +901,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.2"
+  uuid:
+    dependency: "direct main"
+    description:
+      name: uuid
+      sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.5.0"
   vector_graphics_codec:
     dependency: transitive
     description:
@@ -860,10 +937,10 @@ packages:
     dependency: transitive
     description:
       name: vm_service
-      sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
+      sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
       url: "https://pub.dev"
     source: hosted
-    version: "14.2.1"
+    version: "14.2.4"
   watcher:
     dependency: transitive
     description:

+ 6 - 0
pubspec.yaml

@@ -63,6 +63,12 @@ dependencies:
   #解压
   archive: ^3.6.1
 
+  #录音
+  record: ^5.1.2
+
+  #uuid
+  uuid: ^4.5.0
+
 dev_dependencies:
   flutter_test:
     sdk: flutter