فهرست منبع

[new]谈话原文增加原文翻译、点击字段播放录音

zk 1 سال پیش
والد
کامیت
7bc6161f54

BIN
assets/images/icon_talk_translate.webp


BIN
assets/images/icon_talk_translate_small.webp


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

@@ -139,4 +139,5 @@
     <string name="add_desktop_shortcut_btn_txt">立即添加</string>
     <string name="desktop_shortcut_record_name">小听快听</string>
     <string name="dialog_send_friend">发送给朋友</string>
+    <string name="translating">正在翻译...</string>
 </resources>

+ 6 - 3
lib/data/api/atmob_api.dart

@@ -13,12 +13,12 @@ import 'package:electronic_assistant/data/api/request/order_pay_request.dart';
 import 'package:electronic_assistant/data/api/request/order_status_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_export_request.dart';
 import 'package:electronic_assistant/data/api/request/talk_generate_request.dart';
 import 'package:electronic_assistant/data/api/request/talk_paginate_request.dart';
 import 'package:electronic_assistant/data/api/request/talk_query_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/talk_translate_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';
@@ -33,16 +33,15 @@ import 'package:electronic_assistant/data/api/response/order_pay_response.dart';
 import 'package:electronic_assistant/data/api/response/order_status_response.dart';
 import 'package:electronic_assistant/data/api/response/store_index_response.dart';
 import 'package:electronic_assistant/data/api/response/talk_check_electric_response.dart';
-import 'package:electronic_assistant/data/api/response/talk_export_response.dart';
 import 'package:electronic_assistant/data/api/response/talk_info_response.dart';
 import 'package:electronic_assistant/data/api/response/talk_original_response.dart';
 import 'package:electronic_assistant/data/api/response/talk_paginate_response.dart';
 import 'package:electronic_assistant/data/api/response/talk_query_response.dart';
+import 'package:electronic_assistant/data/api/response/talk_translate_response.dart';
 import 'package:electronic_assistant/data/api/response/tasks_running_response.dart';
 import 'package:electronic_assistant/data/api/response/user_info_response.dart';
 import 'package:electronic_assistant/data/bean/talks.dart';
 import 'package:electronic_assistant/data/consts/constants.dart';
-import 'package:retrofit/dio.dart';
 import 'package:retrofit/http.dart';
 
 part 'atmob_api.g.dart';
@@ -158,6 +157,10 @@ abstract class AtmobApi {
 
   @POST("/project/secretary/v1/confs")
   Future<BaseResponse<ConfigResponse>> configs(@Body() ConfigRequest request);
+
+  @POST("/project/secretary/v1/talk/translate")
+  Future<BaseResponse<TalkTranslateResponse>> talkTranslate(
+      @Body() TalkTranslateRequest request);
 }
 
 final atmobApi = AtmobApi(defaultDio, baseUrl: Constants.baseUrl);

+ 3 - 2
lib/data/api/network_module.dart

@@ -1,4 +1,5 @@
 import 'package:dio/dio.dart';
+import 'package:electronic_assistant/data/consts/build_config.dart';
 import 'package:electronic_assistant/utils/stream_dio_log_interceptor.dart';
 import 'package:pretty_dio_logger/pretty_dio_logger.dart';
 
@@ -110,7 +111,7 @@ class _NetworkModule {
       requestBody: true,
       responseBody: true,
       responseHeader: true,
-      enabled: Constants.env != Constants.envProd,
+      enabled: BuildConfig.isDebug,
     ));
     return dio;
   }
@@ -132,7 +133,7 @@ class _NetworkModule {
       requestBody: true,
       responseBody: true,
       responseHeader: true,
-      enabled: Constants.env != Constants.envProd,
+      enabled: BuildConfig.isDebug,
     ));
     return dio;
   }

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

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

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

@@ -0,0 +1,14 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'talk_translate_response.g.dart';
+
+@JsonSerializable()
+class TalkTranslateResponse {
+  @JsonKey(name: 'translation')
+  String translation;
+
+  TalkTranslateResponse(this.translation);
+
+  factory TalkTranslateResponse.fromJson(Map<String, dynamic> json) =>
+      _$TalkTranslateResponseFromJson(json);
+}

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

@@ -1,3 +1,4 @@
+import 'package:get/get.dart';
 import 'package:json_annotation/json_annotation.dart';
 
 part 'talk_original.g.dart';
@@ -22,6 +23,27 @@ class TalkOriginal {
   @JsonKey(name: 'sentence')
   String? sentence;
 
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  final Rxn<bool> _isSelected = Rxn<bool>();
+
+  bool isSelected() => _isSelected.value ?? false;
+
+  setSelected(bool value) => _isSelected.value = value;
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  final Rx<TalkTranslate> _translateStatus = TalkTranslate.normal.obs;
+
+  TalkTranslate getTranslateStatus() => _translateStatus.value;
+
+  setTranslateStatus(TalkTranslate value) => _translateStatus.value = value;
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  final Rxn<String> _translatedSentence = Rxn<String>();
+
+  String? getTranslatedSentence() => _translatedSentence.value;
+
+  setTranslatedSentence(String value) => _translatedSentence.value = value;
+
   TalkOriginal({
     this.endMs,
     this.startMs,
@@ -34,3 +56,10 @@ class TalkOriginal {
   factory TalkOriginal.fromJson(Map<String, dynamic> json) =>
       _$TalkOriginalFromJson(json);
 }
+
+enum TalkTranslate {
+  normal,
+  translating,
+  translated,
+  translateFail,
+}

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

@@ -8,6 +8,7 @@ import 'package:electronic_assistant/data/api/atmob_stream_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 'package:electronic_assistant/data/api/request/talk_file_request.dart';
+import 'package:electronic_assistant/data/api/request/talk_translate_request.dart';
 import 'package:flutter_foreground_task/flutter_foreground_task.dart';
 import 'package:get/get.dart';
 import 'package:get/get_connect/http/src/request/request.dart';
@@ -24,6 +25,7 @@ import '../api/request/talk_request.dart';
 import '../api/response/talk_check_electric_response.dart';
 import '../api/response/talk_info_response.dart';
 import '../api/response/talk_paginate_response.dart';
+import '../api/response/talk_translate_response.dart';
 import '../bean/talk_original.dart';
 import '../bean/talks.dart';
 import 'account_repository.dart';
@@ -39,6 +41,12 @@ class TalkRepository {
 
   RxList<TalkBean> get talkList => _talkList;
 
+  Future<TalkTranslateResponse> talkTranslate(String content) {
+    return atmobApi
+        .talkTranslate(TalkTranslateRequest(content))
+        .then(HttpHandler.handle(false));
+  }
+
   bool isUploadingTalk(String talkId) {
     return _uploadingTalkIds.contains(talkId);
   }

+ 12 - 2
lib/module/talk/controller.dart

@@ -114,6 +114,8 @@ class TalkController extends BaseController {
 
   bool isLocalFileHas = false;
 
+  final Rxn<Duration> playingDuration = Rxn();
+
   @override
   void onReady() {
     super.onReady();
@@ -163,10 +165,11 @@ class TalkController extends BaseController {
       }
     });
 
-    _audioPlayer.positionStream.listen((position) {
+    _audioPlayer.positionStream.listen((duration) {
+      playingDuration.value = duration;
       if (audioDuration.value.inMilliseconds > 0) {
         audioProgressValue.value =
-            (position.inMilliseconds / audioDuration.value.inMilliseconds)
+            (duration.inMilliseconds / audioDuration.value.inMilliseconds)
                 .clamp(0.0, sliderMax);
       }
     });
@@ -606,4 +609,11 @@ class TalkController extends BaseController {
     _agendaContentController?.dispose();
     _agendaNameController?.dispose();
   }
+
+  void seekTo(int? startMs) {
+    if (startMs == null) {
+      return;
+    }
+    _audioPlayer.seek(Duration(milliseconds: startMs));
+  }
 }

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

@@ -3,10 +3,14 @@ import 'dart:async';
 import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:electronic_assistant/data/consts/event_report_id.dart';
 import 'package:electronic_assistant/data/repositories/talk_repository.dart';
+import 'package:electronic_assistant/resource/string.gen.dart';
+import 'package:electronic_assistant/utils/error_handler.dart';
+import 'package:electronic_assistant/utils/toast_util.dart';
 import 'package:get/get.dart';
 
 import '../../../data/bean/talk_original.dart';
 import '../../../data/bean/talks.dart';
+import '../../../data/repositories/account_repository.dart';
 import '../controller.dart';
 
 class OriginalController extends BaseController {
@@ -15,6 +19,7 @@ class OriginalController extends BaseController {
   final originalList = <TalkOriginal>[].obs;
 
   StreamSubscription? _talkStatusListener;
+  StreamSubscription? _audioPlayingListener;
 
   @override
   void onReady() {
@@ -26,6 +31,26 @@ class OriginalController extends BaseController {
       }
     });
     requestOriginal();
+    setPlayAutoSelection();
+  }
+
+  void setPlayAutoSelection() {
+    _audioPlayingListener = talkController.playingDuration.listen((duration) {
+      if (duration == null) {
+        return;
+      }
+      int time = duration.inMilliseconds;
+      for (var item in originalList) {
+        if (item.startMs == null || item.endMs == null) {
+          continue;
+        }
+        if (item.startMs! <= time && item.endMs! >= time) {
+          item.setSelected(true);
+        } else {
+          item.setSelected(false);
+        }
+      }
+    });
   }
 
   void eventReport(String eventId, {Map<String, dynamic>? params}) {
@@ -43,14 +68,40 @@ class OriginalController extends BaseController {
     if (originalList.isNotEmpty) {
       return;
     }
-    talkRepository.talkOriginal(talkController.talkBean.value?.id).then((value) {
+    talkRepository
+        .talkOriginal(talkController.talkBean.value?.id)
+        .then((value) {
       originalList.value = value;
     });
   }
 
+  void talkTranslateClick(TalkOriginal item) {
+    if (!accountRepository.isLogin.value) {
+      ToastUtil.showToast(StringName.errorCodeNoLogin.tr);
+      return;
+    }
+    if (item.getTranslateStatus() == TalkTranslate.translating ||
+        item.getTranslateStatus() == TalkTranslate.translated) {
+      return;
+    }
+    if (item.sentence == null || item.sentence!.isEmpty) {
+      ToastUtil.showToast('翻译内容为空');
+      return;
+    }
+    item.setTranslateStatus(TalkTranslate.translating);
+    talkRepository.talkTranslate(item.sentence!).then((result) {
+      item.setTranslateStatus(TalkTranslate.translated);
+      item.setTranslatedSentence(result.translation);
+    }).catchError((error) {
+      item.setTranslateStatus(TalkTranslate.normal);
+      ErrorHandler.toastError(error);
+    });
+  }
+
   @override
   void onClose() {
     super.onClose();
     _talkStatusListener?.cancel();
+    _audioPlayingListener?.cancel();
   }
 }

+ 106 - 34
lib/module/talk/original/view.dart

@@ -1,6 +1,10 @@
 import 'package:electronic_assistant/base/base_page.dart';
+import 'package:electronic_assistant/data/bean/store_item.dart';
 import 'package:electronic_assistant/data/bean/talks.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';
@@ -37,43 +41,111 @@ class OriginalView extends BasePage<OriginalController> {
 
   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),
+    return GestureDetector(
+      onTap: () {
+        controller.talkController.seekTo(item.startMs);
+      },
+      child: Padding(
+        padding:
+            EdgeInsets.only(left: 12.w, right: 12.w, top: 11.h, bottom: 13.h),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            SizedBox(
+              height: 24.w,
+              child: 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)),
+                  const Spacer(),
+                  Obx(() {
+                    return Visibility(
+                      visible: item.isSelected(),
+                      child: GestureDetector(
+                        onTap: () {
+                          controller.talkTranslateClick(item);
+                        },
+                        child: Container(
+                            margin: EdgeInsets.only(right: 12.w),
+                            child: Assets.images.iconTalkTranslate
+                                .image(width: 24.w, height: 24.w)),
+                      ),
+                    );
+                  })
+                ],
               ),
-              SizedBox(width: 6.w),
-              Text(item.speaker.toString(),
-                  style: TextStyle(
-                      fontSize: 14.sp, color: ColorName.secondaryTextColor)),
-              SizedBox(width: 4.w),
-              Text(formatMilliseconds(item.startMs),
+            ),
+            SizedBox(height: 12.h),
+            Obx(() {
+              return Text(item.sentence.toString(),
                   style: TextStyle(
-                      fontSize: 12.sp, color: ColorName.tertiaryTextColor)),
-            ],
-          ),
-          SizedBox(height: 12.h),
-          Text(item.sentence.toString(),
-              style: TextStyle(
-                  fontSize: 14.sp, color: ColorName.primaryTextColor)),
-        ],
+                      fontSize: 14.sp,
+                      color: item.isSelected()
+                          ? ColorName.colorPrimary
+                          : ColorName.primaryTextColor));
+            }),
+            Obx(() {
+              return Visibility(
+                visible:
+                    item.getTranslateStatus() == TalkTranslate.translating ||
+                        item.getTranslateStatus() == TalkTranslate.translated,
+                child: Container(
+                  width: double.infinity,
+                  margin: EdgeInsets.only(top: 8.w),
+                  decoration: BoxDecoration(
+                    color: '#FAF9FB'.toColor(),
+                    border: Border.all(color: '#F2EFF5'.toColor(), width: 1.w),
+                    borderRadius: BorderRadius.circular(6.w),
+                  ),
+                  padding:
+                      EdgeInsets.symmetric(vertical: 11.w, horizontal: 10.w),
+                  child: RichText(
+                    text: TextSpan(
+                      children: [
+                        WidgetSpan(
+                          alignment: PlaceholderAlignment.middle,
+                          child: Container(
+                            margin: EdgeInsets.only(right: 6.w),
+                            child: Assets.images.iconTalkTranslateSmall
+                                .image(width: 22.w, height: 22.w),
+                          ),
+                        ),
+                        TextSpan(
+                          text: item.getTranslatedSentence() ??
+                              StringName.translating.tr,
+                          style: TextStyle(
+                              fontSize: 14.sp,
+                              color: ColorName.secondaryTextColor),
+                        ),
+                      ],
+                    ),
+                  ),
+                ),
+              );
+            })
+          ],
+        ),
       ),
     );
   }

+ 3 - 3
pubspec.lock

@@ -1054,10 +1054,10 @@ packages:
     dependency: "direct main"
     description:
       name: photo_manager
-      sha256: "2b5c0d02014b64b971e57aa840ae7d7ffd81b0482a082579eb17ff83e5dc408e"
-      url: "https://pub.flutter-io.cn"
+      sha256: "70159eee32203e8162d49d588232f0299ed3f383c63eef1e899cb6b83dee6b26"
+      url: "https://pub.dev"
     source: hosted
-    version: "3.4.0"
+    version: "3.5.1"
   platform:
     dependency: transitive
     description: